diff --git a/src/api/Synapse.Api.Application/Services/CloudEventPublisher.cs b/src/api/Synapse.Api.Application/Services/CloudEventPublisher.cs index 21d997610..9510e7e08 100644 --- a/src/api/Synapse.Api.Application/Services/CloudEventPublisher.cs +++ b/src/api/Synapse.Api.Application/Services/CloudEventPublisher.cs @@ -84,14 +84,21 @@ public class CloudEventPublisher(ILogger logger, IConnectio protected bool SupportsStreaming { get; private set; } /// - public virtual Task StartAsync(CancellationToken cancellationToken) + public virtual async Task StartAsync(CancellationToken cancellationToken) { if (options.Value.CloudEvents.Endpoint == null) logger.LogWarning("No endpoint configured for cloud events. Events will not be published."); else _ = this.PublishEnqueuedEventsAsync(); var version = ((string)(this.Database.Execute("INFO", "server"))!).Split('\n').FirstOrDefault(line => line.StartsWith("redis_version:"))?[14..]?.Trim() ?? "undetermined"; try { - this.Database.StreamInfo(SynapseDefaults.CloudEvents.Bus.StreamName); + try + { + await this.Database.StreamInfoAsync(SynapseDefaults.CloudEvents.Bus.StreamName).ConfigureAwait(false); + } + catch (RedisServerException ex) when (ex.Message.StartsWith("ERR no such key")) + { + this.Logger.LogWarning("The cloud event stream is currently unavailable, but it should be created when cloud events are published or when correlators are activated"); + } this.SupportsStreaming = true; this.Logger.LogInformation("Redis server version '{version}' supports streaming commands. Streaming feature is enabled", version); } @@ -100,7 +107,10 @@ public virtual Task StartAsync(CancellationToken cancellationToken) this.SupportsStreaming = false; this.Logger.LogInformation("Redis server version '{version}' does not support streaming commands. Streaming feature is emulated using lists", version); } - return Task.CompletedTask; + catch(Exception ex) + { + this.Logger.LogWarning("An error occurred while starting the cloud event publisher, possibly affecting the server's ability to publish cloud events to correlators: {ex}", ex); + } } /// diff --git a/src/api/Synapse.Api.Application/Services/WorkflowDatabaseInitializer.cs b/src/api/Synapse.Api.Application/Services/DatabaseProvisioner .cs similarity index 51% rename from src/api/Synapse.Api.Application/Services/WorkflowDatabaseInitializer.cs rename to src/api/Synapse.Api.Application/Services/DatabaseProvisioner .cs index 9e2b480e0..48021b5ab 100644 --- a/src/api/Synapse.Api.Application/Services/WorkflowDatabaseInitializer.cs +++ b/src/api/Synapse.Api.Application/Services/DatabaseProvisioner .cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Neuroglia.Serialization; using ServerlessWorkflow.Sdk.IO; using ServerlessWorkflow.Sdk.Models; using Synapse.Api.Application.Configuration; @@ -28,9 +29,11 @@ namespace Synapse.Api.Application.Services; /// /// The current /// The service used to perform logging +/// The service used to serialize/deserialize data to/from JSON +/// The service used to serialize/deserialize data to/from YAML /// The service used to read s /// The service used to access the current -public class WorkflowDatabaseInitializer(IServiceProvider serviceProvider, ILogger logger, IWorkflowDefinitionReader workflowDefinitionReader, IOptions options) +public class WorkflowDatabaseInitializer(IServiceProvider serviceProvider, ILogger logger, IJsonSerializer jsonSerializer, IYamlSerializer yamlSerializer, IWorkflowDefinitionReader workflowDefinitionReader, IOptions options) : IHostedService { @@ -44,6 +47,16 @@ public class WorkflowDatabaseInitializer(IServiceProvider serviceProvider, ILogg /// protected ILogger Logger { get; } = logger; + /// + /// Gets the service used to serialize/deserialize data to/from JSON + /// + protected IJsonSerializer JsonSerializer { get; } = jsonSerializer; + + /// + /// Gets the service used to serialize/deserialize data to/from YAML + /// + protected IYamlSerializer YamlSerializer { get; } = yamlSerializer; + /// /// Gets the service used to read s /// @@ -80,15 +93,80 @@ public virtual async Task StartAsync(CancellationToken cancellationToken) this.Logger.LogWarning("The directory '{directory}' does not exist or cannot be found. Skipping static resource import", directory.FullName); return; } - this.Logger.LogInformation("Starting importing static resources from directory '{directory}'...", directory.FullName); + await this.ProvisionNamespacesAsync(resources, cancellationToken).ConfigureAwait(false); + await this.ProvisionWorkflowsAsync(resources, cancellationToken).ConfigureAwait(false); + await this.ProvisionFunctionsAsync(resources, cancellationToken).ConfigureAwait(false); + } + + /// + /// Provisions namespaces from statis resource files + /// + /// The used to manage s + /// A + /// A new awaitable + protected virtual async Task ProvisionNamespacesAsync(IResourceRepository resources, CancellationToken cancellationToken) + { + var stopwatch = new Stopwatch(); + var directory = new DirectoryInfo(Path.Combine(this.Options.Seeding.Directory, "namespaces")); + if (!directory.Exists) return; + this.Logger.LogInformation("Starting importing namespaces from directory '{directory}'...", directory.FullName); var files = directory.GetFiles(this.Options.Seeding.FilePattern, SearchOption.AllDirectories).Where(f => f.FullName.EndsWith(".json", StringComparison.OrdinalIgnoreCase) || f.FullName.EndsWith(".yml", StringComparison.OrdinalIgnoreCase) || f.FullName.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)); if (!files.Any()) { - this.Logger.LogWarning("No static resource files matching search pattern '{pattern}' found in directory '{directory}'. Skipping import.", this.Options.Seeding.FilePattern, directory.FullName); + this.Logger.LogWarning("No namespace static resource files matching search pattern '{pattern}' found in directory '{directory}'. Skipping import.", this.Options.Seeding.FilePattern, directory.FullName); return; } + stopwatch.Restart(); var count = 0; + foreach (var file in files) + { + try + { + var extension = file.FullName.Split('.', StringSplitOptions.RemoveEmptyEntries).LastOrDefault(); + var serializer = extension?.ToLowerInvariant() switch + { + "json" => (ITextSerializer)this.JsonSerializer, + "yml" or "yaml" => this.YamlSerializer, + _ => throw new NotSupportedException($"The specified extension '{extension}' is not supported for static resource files") + }; + using var stream = file.OpenRead(); + using var streamReader = new StreamReader(stream); + var text = await streamReader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + var ns = serializer.Deserialize(text)!; + await resources.AddAsync(ns, false, cancellationToken).ConfigureAwait(false); + this.Logger.LogInformation("Successfully imported namespace '{namespace}' from file '{file}'", $"{ns.Metadata.Name}", file.FullName); + count++; + } + catch (Exception ex) + { + this.Logger.LogError("An error occurred while reading a namespace from file '{file}': {ex}", file.FullName, ex); + continue; + } + } + stopwatch.Stop(); + this.Logger.LogInformation("Completed importing {count} namespaces in {ms} milliseconds", count, stopwatch.Elapsed.TotalMilliseconds); + } + + /// + /// Provisions workflows from statis resource files + /// + /// The used to manage s + /// A + /// A new awaitable + protected virtual async Task ProvisionWorkflowsAsync(IResourceRepository resources, CancellationToken cancellationToken) + { + var stopwatch = new Stopwatch(); + var directory = new DirectoryInfo(Path.Combine(this.Options.Seeding.Directory, "workflows")); + if (!directory.Exists) return; + this.Logger.LogInformation("Starting importing workflows from directory '{directory}'...", directory.FullName); + var files = directory.GetFiles(this.Options.Seeding.FilePattern, SearchOption.AllDirectories).Where(f => f.FullName.EndsWith(".json", StringComparison.OrdinalIgnoreCase) || f.FullName.EndsWith(".yml", StringComparison.OrdinalIgnoreCase) || f.FullName.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)); + if (!files.Any()) + { + this.Logger.LogWarning("No workflow static resource files matching search pattern '{pattern}' found in directory '{directory}'. Skipping import.", this.Options.Seeding.FilePattern, directory.FullName); + return; + } stopwatch.Restart(); + var count = 0; foreach (var file in files) { try @@ -110,11 +188,6 @@ public virtual async Task StartAsync(CancellationToken cancellationToken) Versions = [workflowDefinition] } }; - if (await resources.GetAsync(workflow.GetNamespace()!, cancellationToken: cancellationToken).ConfigureAwait(false) == null) - { - await resources.AddAsync(new Namespace() { Metadata = new() { Name = workflow.GetNamespace()! } }, false, cancellationToken).ConfigureAwait(false); - this.Logger.LogInformation("Successfully created namespace '{namespace}'", workflow.GetNamespace()); - } await resources.AddAsync(workflow, false, cancellationToken).ConfigureAwait(false); } else @@ -138,14 +211,63 @@ public virtual async Task StartAsync(CancellationToken cancellationToken) this.Logger.LogInformation("Successfully imported workflow '{workflow}' from file '{file}'", $"{workflowDefinition.Document.Name}.{workflowDefinition.Document.Namespace}:{workflowDefinition.Document.Version}", file.FullName); count++; } - catch(Exception ex) + catch (Exception ex) { this.Logger.LogError("An error occurred while reading a workflow definition from file '{file}': {ex}", file.FullName, ex); continue; } } stopwatch.Stop(); - this.Logger.LogInformation("Completed importing {count} static resources in {ms} milliseconds", count, stopwatch.Elapsed.TotalMilliseconds); + this.Logger.LogInformation("Completed importing {count} workflows in {ms} milliseconds", count, stopwatch.Elapsed.TotalMilliseconds); + } + + /// + /// Provisions functions from statis resource files + /// + /// The used to manage s + /// A + /// A new awaitable + protected virtual async Task ProvisionFunctionsAsync(IResourceRepository resources, CancellationToken cancellationToken) + { + var stopwatch = new Stopwatch(); + var directory = new DirectoryInfo(Path.Combine(this.Options.Seeding.Directory, "functions")); + if (!directory.Exists) return; + this.Logger.LogInformation("Starting importing custom functions from directory '{directory}'...", directory.FullName); + var files = directory.GetFiles(this.Options.Seeding.FilePattern, SearchOption.AllDirectories).Where(f => f.FullName.EndsWith(".json", StringComparison.OrdinalIgnoreCase) || f.FullName.EndsWith(".yml", StringComparison.OrdinalIgnoreCase) || f.FullName.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase)); + if (!files.Any()) + { + this.Logger.LogWarning("No custom function static resource files matching search pattern '{pattern}' found in directory '{directory}'. Skipping import.", this.Options.Seeding.FilePattern, directory.FullName); + return; + } + stopwatch.Restart(); + var count = 0; + foreach (var file in files) + { + try + { + var extension = file.FullName.Split('.', StringSplitOptions.RemoveEmptyEntries).LastOrDefault(); + var serializer = extension?.ToLowerInvariant() switch + { + "json" => (ITextSerializer)this.JsonSerializer, + "yml" or "yaml" => this.YamlSerializer, + _ => throw new NotSupportedException($"The specified extension '{extension}' is not supported for static resource files") + }; + using var stream = file.OpenRead(); + using var streamReader = new StreamReader(stream); + var text = await streamReader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + var func = serializer.Deserialize(text)!; + await resources.AddAsync(func, false, cancellationToken).ConfigureAwait(false); + this.Logger.LogInformation("Successfully imported custom function '{customFunction}' from file '{file}'", func.GetQualifiedName(), file.FullName); + count++; + } + catch (Exception ex) + { + this.Logger.LogError("An error occurred while reading a custom function from file '{file}': {ex}", file.FullName, ex); + continue; + } + } + stopwatch.Stop(); + this.Logger.LogInformation("Completed importing {count} custom functions in {ms} milliseconds", count, stopwatch.Elapsed.TotalMilliseconds); } /// diff --git a/src/api/Synapse.Api.Application/Synapse.Api.Application.csproj b/src/api/Synapse.Api.Application/Synapse.Api.Application.csproj index 378937987..8a4b4c1ae 100644 --- a/src/api/Synapse.Api.Application/Synapse.Api.Application.csproj +++ b/src/api/Synapse.Api.Application/Synapse.Api.Application.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/api/Synapse.Api.Client.Core/Services/ISynapseApiClient.cs b/src/api/Synapse.Api.Client.Core/Services/ISynapseApiClient.cs index 4f7722fad..f63b04d38 100644 --- a/src/api/Synapse.Api.Client.Core/Services/ISynapseApiClient.cs +++ b/src/api/Synapse.Api.Client.Core/Services/ISynapseApiClient.cs @@ -31,6 +31,11 @@ public interface ISynapseApiClient /// INamespacedResourceApiClient Correlators { get; } + /// + /// Gets the Synapse API used to manage s + /// + INamespacedResourceApiClient CustomFunctions { get; } + /// /// Gets the Synapse API used to manage s /// diff --git a/src/api/Synapse.Api.Client.Core/Synapse.Api.Client.Core.csproj b/src/api/Synapse.Api.Client.Core/Synapse.Api.Client.Core.csproj index 5f023195d..6f5c81398 100644 --- a/src/api/Synapse.Api.Client.Core/Synapse.Api.Client.Core.csproj +++ b/src/api/Synapse.Api.Client.Core/Synapse.Api.Client.Core.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/api/Synapse.Api.Client.Http/Services/SynapseHttpApiClient.cs b/src/api/Synapse.Api.Client.Http/Services/SynapseHttpApiClient.cs index 4380e5ce7..b7fe8cee1 100644 --- a/src/api/Synapse.Api.Client.Http/Services/SynapseHttpApiClient.cs +++ b/src/api/Synapse.Api.Client.Http/Services/SynapseHttpApiClient.cs @@ -74,6 +74,9 @@ public SynapseHttpApiClient(IServiceProvider serviceProvider, ILoggerFactory log /// public INamespacedResourceApiClient Correlators { get; private set; } = null!; + /// + public INamespacedResourceApiClient CustomFunctions { get; private set; } = null!; + /// public IDocumentApiClient Documents { get; } diff --git a/src/api/Synapse.Api.Client.Http/Synapse.Api.Client.Http.csproj b/src/api/Synapse.Api.Client.Http/Synapse.Api.Client.Http.csproj index db04e87d1..b22247211 100644 --- a/src/api/Synapse.Api.Client.Http/Synapse.Api.Client.Http.csproj +++ b/src/api/Synapse.Api.Client.Http/Synapse.Api.Client.Http.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/api/Synapse.Api.Http/Controllers/CustomFunctionsController.cs b/src/api/Synapse.Api.Http/Controllers/CustomFunctionsController.cs new file mode 100644 index 000000000..8d0b9205a --- /dev/null +++ b/src/api/Synapse.Api.Http/Controllers/CustomFunctionsController.cs @@ -0,0 +1,28 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Synapse.Api.Http.Controllers; + +/// +/// Represents the used to manage s +/// +/// The service used to mediate calls +/// The service used to serialize/deserialize objects to/from JSON +[Route("api/v1/custom-functions")] +public class CustomFunctionsController(IMediator mediator, IJsonSerializer jsonSerializer) + : NamespacedResourceController(mediator, jsonSerializer) +{ + + + +} diff --git a/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj b/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj index 50431cf9a..9d4129989 100644 --- a/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj +++ b/src/api/Synapse.Api.Http/Synapse.Api.Http.csproj @@ -8,7 +8,7 @@ Library True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/api/Synapse.Api.Server/Synapse.Api.Server.csproj b/src/api/Synapse.Api.Server/Synapse.Api.Server.csproj index 82c4defe8..99e443a8d 100644 --- a/src/api/Synapse.Api.Server/Synapse.Api.Server.csproj +++ b/src/api/Synapse.Api.Server/Synapse.Api.Server.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/cli/Synapse.Cli/Synapse.Cli.csproj b/src/cli/Synapse.Cli/Synapse.Cli.csproj index 6d73834b1..b12db2220 100644 --- a/src/cli/Synapse.Cli/Synapse.Cli.csproj +++ b/src/cli/Synapse.Cli/Synapse.Cli.csproj @@ -8,7 +8,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/core/Synapse.Core.Infrastructure.Containers.Docker/Synapse.Core.Infrastructure.Containers.Docker.csproj b/src/core/Synapse.Core.Infrastructure.Containers.Docker/Synapse.Core.Infrastructure.Containers.Docker.csproj index bd4525a60..58c33bb4d 100644 --- a/src/core/Synapse.Core.Infrastructure.Containers.Docker/Synapse.Core.Infrastructure.Containers.Docker.csproj +++ b/src/core/Synapse.Core.Infrastructure.Containers.Docker/Synapse.Core.Infrastructure.Containers.Docker.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Synapse.Core.Infrastructure.Containers.Kubernetes.csproj b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Synapse.Core.Infrastructure.Containers.Kubernetes.csproj index e494716c0..25949e7a6 100644 --- a/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Synapse.Core.Infrastructure.Containers.Kubernetes.csproj +++ b/src/core/Synapse.Core.Infrastructure.Containers.Kubernetes/Synapse.Core.Infrastructure.Containers.Kubernetes.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/core/Synapse.Core.Infrastructure/Services/DatabaseInitializer.cs b/src/core/Synapse.Core.Infrastructure/Services/DatabaseInitializer.cs index 5fca07f9c..d26667dfa 100644 --- a/src/core/Synapse.Core.Infrastructure/Services/DatabaseInitializer.cs +++ b/src/core/Synapse.Core.Infrastructure/Services/DatabaseInitializer.cs @@ -28,6 +28,16 @@ public class DatabaseInitializer(ILoggerFactory loggerFactory, IServiceProvider : Neuroglia.Data.Infrastructure.ResourceOriented.Services.DatabaseInitializer(loggerFactory, serviceProvider) { + /// + public override async Task StartAsync(CancellationToken stoppingToken) + { + try + { + await this.InitializeAsync(stoppingToken).ConfigureAwait(false); + } + catch (ProblemDetailsException ex) when (ex.Problem.Status == (int)HttpStatusCode.Conflict || (ex.Problem.Status == (int)HttpStatusCode.BadRequest && ex.Problem.Title == "Conflict")) { } + } + /// protected override async Task SeedAsync(CancellationToken cancellationToken) { diff --git a/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj b/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj index 7e6355eb4..622e5c9ae 100644 --- a/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj +++ b/src/core/Synapse.Core.Infrastructure/Synapse.Core.Infrastructure.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/core/Synapse.Core/Extensions/TaskDefinitionVersionMapExtensions.cs b/src/core/Synapse.Core/Extensions/TaskDefinitionVersionMapExtensions.cs new file mode 100644 index 000000000..4abd4bace --- /dev/null +++ b/src/core/Synapse.Core/Extensions/TaskDefinitionVersionMapExtensions.cs @@ -0,0 +1,46 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Semver; + +namespace Synapse; + +/// +/// Defines extensions for instances that contains version mappings +/// +public static class TaskDefinitionVersionMapExtensions +{ + + /// + /// Gets the latest version of the + /// + /// An containing the s to get the latest of + /// The latest + public static TaskDefinition GetLatest(this Map definitions) => definitions.OrderByDescending(kvp => SemVersion.Parse(kvp.Key, SemVersionStyles.Strict)).First().Value; + + /// + /// Gets the latest version of the + /// + /// An containing the s to get the latest of + /// The latest version + public static string GetLatestVersion(this Map definitions) => definitions.OrderByDescending(kvp => SemVersion.Parse(kvp.Key, SemVersionStyles.Strict)).First().Key; + + /// + /// Gets the specified version + /// + /// An containing the s to get the specified version from + /// The version of the to get + /// The with the specified version, if any + public static TaskDefinition? Get(this Map definitions, string version) => definitions.FirstOrDefault(kvp => kvp.Key == version)?.Value; + +} \ No newline at end of file diff --git a/src/core/Synapse.Core/Resources/CertificateValidationStrategyDefinition.cs b/src/core/Synapse.Core/Resources/CertificateValidationStrategyDefinition.cs index 0f4c92a93..912687912 100644 --- a/src/core/Synapse.Core/Resources/CertificateValidationStrategyDefinition.cs +++ b/src/core/Synapse.Core/Resources/CertificateValidationStrategyDefinition.cs @@ -11,8 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.ComponentModel; - namespace Synapse.Resources; /// @@ -25,8 +23,7 @@ public record CertificateValidationStrategyDefinition /// /// Gets/sets a boolean indicating whether or not to validate certificates when performing requests /// - [DefaultValue(true)] [DataMember(Order = 1, Name = "validate"), JsonPropertyOrder(1), JsonPropertyName("validate"), YamlMember(Order = 1, Alias = "validate")] - public virtual bool Validate { get; set; } = true; + public virtual bool? Validate { get; set; } } \ No newline at end of file diff --git a/src/core/Synapse.Core/Resources/CorrelatorSpec.cs b/src/core/Synapse.Core/Resources/CorrelatorSpec.cs index c24d72a71..da94b2532 100644 --- a/src/core/Synapse.Core/Resources/CorrelatorSpec.cs +++ b/src/core/Synapse.Core/Resources/CorrelatorSpec.cs @@ -14,7 +14,7 @@ namespace Synapse.Resources; /// -/// Represents the object used to configure the desired state of an +/// Represents the object used to configure the desired state of a /// [DataContract] public record CorrelatorSpec diff --git a/src/core/Synapse.Core/Resources/CustomFunction.cs b/src/core/Synapse.Core/Resources/CustomFunction.cs new file mode 100644 index 000000000..724d0007a --- /dev/null +++ b/src/core/Synapse.Core/Resources/CustomFunction.cs @@ -0,0 +1,37 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Data.Infrastructure.ResourceOriented; + +namespace Synapse.Resources; + +/// +/// Represents the resource used to describe and configure a custom, callable task +/// +[DataContract] +public record CustomFunction + : Resource +{ + + /// + /// Gets the 's resource type + /// + public static readonly ResourceDefinitionInfo ResourceDefinition = new CustomFunctionResourceDefinition()!; + + /// + public CustomFunction() : base(ResourceDefinition) { } + + /// + public CustomFunction(ResourceMetadata metadata, CustomFunctionSpec spec) : base(ResourceDefinition, metadata, spec) { } + +} diff --git a/src/core/Synapse.Core/Resources/CustomFunction.yaml b/src/core/Synapse.Core/Resources/CustomFunction.yaml new file mode 100644 index 000000000..e6f5dccf5 --- /dev/null +++ b/src/core/Synapse.Core/Resources/CustomFunction.yaml @@ -0,0 +1,29 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: custom-functions.synapse.io +spec: + scope: Namespaced + group: synapse.io + names: + plural: custom-functions + singular: custom-function + kind: CustomFunction + shortNames: + - cf + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + description: Configures the Synapse Custom Function + properties: {} #todo + status: + type: object + required: + - spec \ No newline at end of file diff --git a/src/core/Synapse.Core/Resources/CustomFunctionResourceDefinition.cs b/src/core/Synapse.Core/Resources/CustomFunctionResourceDefinition.cs new file mode 100644 index 000000000..8667e8a39 --- /dev/null +++ b/src/core/Synapse.Core/Resources/CustomFunctionResourceDefinition.cs @@ -0,0 +1,64 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Neuroglia.Data.Infrastructure.ResourceOriented; +using Neuroglia.Serialization.Yaml; + +namespace Synapse.Resources; + +/// +/// Represents the definition of a +/// +[DataContract] +public record CustomFunctionResourceDefinition + : ResourceDefinition +{ + + /// + /// Gets the definition of s + /// + public static new ResourceDefinition Instance { get; set; } + /// + /// Gets/sets the group resource definitions belong to + /// + public static new string ResourceGroup { get; set; } + /// + /// Gets/sets the resource version of resource definitions + /// + public static new string ResourceVersion { get; set; } + /// + /// Gets/sets the plural name of resource definitions + /// + public static new string ResourcePlural { get; set; } + /// + /// Gets/sets the kind of resource definitions + /// + public static new string ResourceKind { get; set; } + + static CustomFunctionResourceDefinition() + { + using var stream = typeof(Workflow).Assembly.GetManifestResourceStream($"{typeof(CustomFunction).Namespace}.{nameof(CustomFunction)}.yaml")!; + using var streamReader = new StreamReader(stream); + Instance = YamlSerializer.Default.Deserialize(streamReader.ReadToEnd())!; + ResourceGroup = Instance.Spec.Group; + ResourceVersion = Instance.Spec.Versions.Last().Name; + ResourcePlural = Instance.Spec.Names.Plural; + ResourceKind = Instance.Spec.Names.Kind; + } + + /// + /// Initializes a new + /// + public CustomFunctionResourceDefinition() : base(Instance) { } + +} diff --git a/src/core/Synapse.Core/Resources/CustomFunctionSpec.cs b/src/core/Synapse.Core/Resources/CustomFunctionSpec.cs new file mode 100644 index 000000000..f00f5d3ab --- /dev/null +++ b/src/core/Synapse.Core/Resources/CustomFunctionSpec.cs @@ -0,0 +1,30 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Synapse.Resources; + +/// +/// Represents the object used to configure the desired state of a +/// +[DataContract] +public record CustomFunctionSpec +{ + + /// + /// Gets/sets the versions of the configured custom function + /// + [Required, MinLength(1)] + [DataMember(Name = "versions", Order = 1), JsonPropertyName("versions"), JsonPropertyOrder(1), YamlMember(Alias = "versions", Order = 1)] + public virtual Map Versions { get; set; } = null!; + +} diff --git a/src/core/Synapse.Core/Synapse.Core.csproj b/src/core/Synapse.Core/Synapse.Core.csproj index c16774726..324a1aab3 100644 --- a/src/core/Synapse.Core/Synapse.Core.csproj +++ b/src/core/Synapse.Core/Synapse.Core.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors @@ -46,6 +46,7 @@ + @@ -54,6 +55,7 @@ + diff --git a/src/core/Synapse.Core/SynapseDefaults.cs b/src/core/Synapse.Core/SynapseDefaults.cs index f40ad3bd9..ba93f3d40 100644 --- a/src/core/Synapse.Core/SynapseDefaults.cs +++ b/src/core/Synapse.Core/SynapseDefaults.cs @@ -998,6 +998,11 @@ public static class Definitions /// public static ResourceDefinition Correlator { get; } = new CorrelatorResourceDefinition(); + /// + /// Gets the definition of Custom Function resources + /// + public static ResourceDefinition CustomFunctions { get; } = new CustomFunctionResourceDefinition(); + /// /// Gets the definition of Operator resources /// @@ -1026,6 +1031,7 @@ public static IEnumerable AsEnumerable() { yield return Correlation; yield return Correlator; + yield return CustomFunctions; yield return Operator; yield return ServiceAccount; yield return Workflow; @@ -1076,6 +1082,27 @@ public static class Labels public static class Tasks { + /// + /// Exposes constants about custom functions + /// + public static class CustomFunctions + { + + /// + /// Exposes constants about custom function catalogs + /// + public static class Catalogs + { + + /// + /// Gets the name of the default custom function catalog + /// + public const string Default = "default"; + + } + + } + /// /// Exposes Synapse task metadata properties /// diff --git a/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj b/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj index e2a54c316..2779a322c 100644 --- a/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj +++ b/src/correlator/Synapse.Correlator/Synapse.Correlator.csproj @@ -8,7 +8,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/dashboard/Synapse.Dashboard/Components/Breadcrumb/Breadcrumbs.cs b/src/dashboard/Synapse.Dashboard/Components/Breadcrumb/Breadcrumbs.cs index 091e41174..c39960058 100644 --- a/src/dashboard/Synapse.Dashboard/Components/Breadcrumb/Breadcrumbs.cs +++ b/src/dashboard/Synapse.Dashboard/Components/Breadcrumb/Breadcrumbs.cs @@ -41,6 +41,10 @@ public static class Breadcrumbs /// public static BreadcrumbItem[] Namespaces = [new("Namespaces", "/operators")]; /// + /// Holds the breadcrumb items for related routes + /// + public static BreadcrumbItem[] Functions = [new("Functions", "/functions")]; + /// /// Holds the breadcrumb items for related routes /// public static BreadcrumbItem[] Correlators = [new("Correlators", "/correlators")]; @@ -49,6 +53,10 @@ public static class Breadcrumbs /// public static BreadcrumbItem[] Correlations = [new("Correlations", "/correlations")]; /// + /// Holds the breadcrumb items for related routes + /// + public static BreadcrumbItem[] ServiceAccounts = [new("Service Accounts", "/service-accounts")]; + /// /// Holds the breadcrumb items for about related routes /// public static BreadcrumbItem[] About = [new("About", "/about")]; diff --git a/src/dashboard/Synapse.Dashboard/Components/CustomFunctionDetails/CustomFunctionDetails.razor b/src/dashboard/Synapse.Dashboard/Components/CustomFunctionDetails/CustomFunctionDetails.razor new file mode 100644 index 000000000..2b8eb76c8 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/CustomFunctionDetails/CustomFunctionDetails.razor @@ -0,0 +1,100 @@ +@* + Copyright © 2024-Present The Synapse Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*@ + +@namespace Synapse.Dashboard.Components + +@if (resource != null) +{ + +
+
+ + + + + + + + + + + + + + + @if (resource.IsNamespaced() == true) + { + + + + + } + + + + + + + + + @if (resource.Metadata.Labels?.Any() == true) + { + + + + + } + + + + + + + + +
API Version@resource.ApiVersion
Kind@resource.Kind
Name@resource.GetName()
Namespace@resource.GetNamespace()
Creation Time@resource.Metadata.CreationTimestamp?.ToString("R")
Generation@resource.Metadata.Generation
Labels + @foreach (var label in resource.Metadata.Labels) + { + @label.Key: @label.Value + } +
Latest version@resource.Spec.Versions.GetLatestVersion()
+ Latest definition +
+ +
+
+
+} + +@code { + + CustomFunction? resource; + /// + /// Gets/sets the resource to display details about + /// + [Parameter] public CustomFunction? Resource { get; set; } + + /// + protected override Task OnParametersSetAsync() + { + if(this.resource != this.Resource) + { + this.resource = this.Resource; + } + return base.OnParametersSetAsync(); + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Components/ResourceEditor/Store.cs b/src/dashboard/Synapse.Dashboard/Components/ResourceEditor/Store.cs index 53ef9a661..0e791ab69 100644 --- a/src/dashboard/Synapse.Dashboard/Components/ResourceEditor/Store.cs +++ b/src/dashboard/Synapse.Dashboard/Components/ResourceEditor/Store.cs @@ -302,10 +302,7 @@ public async Task UpdateResourceAsync() this.SetProblemDetails(null); this.SetSaving(true); var resource = this.Get(state => state.Resource); - if (resource == null) - { - return; - } + if (resource == null) return; var textEditorValue = this.Get(state => state.TextEditorValue); if (monacoEditorHelper.PreferredLanguage == PreferredLanguage.YAML) textEditorValue = yamlSerializer.ConvertToJson(textEditorValue); var jsonPatch = JsonPatch.FromDiff(jsonSerializer.SerializeToElement(resource)!.Value, jsonSerializer.SerializeToElement(jsonSerializer.Deserialize(textEditorValue))!.Value); diff --git a/src/dashboard/Synapse.Dashboard/Components/ResourceManagement/NamespacedResourceManagementComponentStore.cs b/src/dashboard/Synapse.Dashboard/Components/ResourceManagement/NamespacedResourceManagementComponentStore.cs index cc782259b..976e6c972 100644 --- a/src/dashboard/Synapse.Dashboard/Components/ResourceManagement/NamespacedResourceManagementComponentStore.cs +++ b/src/dashboard/Synapse.Dashboard/Components/ResourceManagement/NamespacedResourceManagementComponentStore.cs @@ -67,7 +67,7 @@ public void SetNamespace(string? @namespace) /// Lists all available s /// /// A new awaitable - public virtual async Task ListNamespaceAsync() + public virtual async Task ListNamespacesAsync() { var namespaceList = new EquatableList(await (await this.ApiClient.Namespaces.ListAsync().ConfigureAwait(false)).OrderBy(ns => ns.GetQualifiedName()).ToListAsync().ConfigureAwait(false)); this.Reduce(s => s with @@ -85,7 +85,7 @@ public override async Task DeleteResourceAsync(TResource resource) /// public override async Task InitializeAsync() { - await this.ListNamespaceAsync().ConfigureAwait(false); + await this.ListNamespacesAsync().ConfigureAwait(false); await base.InitializeAsync(); } diff --git a/src/dashboard/Synapse.Dashboard/Components/ResourceManagement/ResourceManagementComponent.cs b/src/dashboard/Synapse.Dashboard/Components/ResourceManagement/ResourceManagementComponent.cs index ac037a69f..ebeff4e1c 100644 --- a/src/dashboard/Synapse.Dashboard/Components/ResourceManagement/ResourceManagementComponent.cs +++ b/src/dashboard/Synapse.Dashboard/Components/ResourceManagement/ResourceManagementComponent.cs @@ -40,7 +40,7 @@ public abstract class ResourceManagementComponent [Inject] - protected JSInterop jsInterop { get; set; } = default!; + protected JSInterop JsInterop { get; set; } = default!; /// /// Gets the service used to serialize/deserialize objects to/from JSON @@ -118,15 +118,15 @@ protected override async Task OnInitializedAsync() if (selectedResourceNames.Count == 0) { - await this.jsInterop.SetCheckboxStateAsync(this.CheckboxAll.Value, CheckboxState.Unchecked); + await this.JsInterop.SetCheckboxStateAsync(this.CheckboxAll.Value, CheckboxState.Unchecked); } else if (selectedResourceNames.Count == (resources?.Count ?? 0)) { - await this.jsInterop.SetCheckboxStateAsync(this.CheckboxAll.Value, CheckboxState.Checked); + await this.JsInterop.SetCheckboxStateAsync(this.CheckboxAll.Value, CheckboxState.Checked); } else { - await this.jsInterop.SetCheckboxStateAsync(this.CheckboxAll.Value, CheckboxState.Indeterminate); + await this.JsInterop.SetCheckboxStateAsync(this.CheckboxAll.Value, CheckboxState.Indeterminate); } } }, cancellationToken: this.CancellationTokenSource.Token); @@ -222,12 +222,12 @@ protected async Task OnDeleteSelectedResourcesAsync() /// Opens the targeted 's details /// /// The to show the details for - protected Task OnShowResourceDetailsAsync(TResource resource) + protected virtual Task OnShowResourceDetailsAsync(TResource resource) { if (this.DetailsOffCanvas == null) return Task.CompletedTask; var parameters = new Dictionary { - { nameof(ResourceEditor.Resource), resource } + { nameof(ResourceDetails.Resource), resource } }; return this.DetailsOffCanvas.ShowAsync>(title: typeof(TResource).Name + " details", parameters: parameters); } diff --git a/src/dashboard/Synapse.Dashboard/Layout/MainLayout.razor b/src/dashboard/Synapse.Dashboard/Layout/MainLayout.razor index 80f761773..0d77d9702 100644 --- a/src/dashboard/Synapse.Dashboard/Layout/MainLayout.razor +++ b/src/dashboard/Synapse.Dashboard/Layout/MainLayout.razor @@ -49,6 +49,11 @@ + diff --git a/src/dashboard/Synapse.Dashboard/Pages/Functions/Create/State.cs b/src/dashboard/Synapse.Dashboard/Pages/Functions/Create/State.cs new file mode 100644 index 000000000..9cb13d7a1 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Pages/Functions/Create/State.cs @@ -0,0 +1,106 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Semver; +using ServerlessWorkflow.Sdk.Models; +using Synapse.Resources; + +namespace Synapse.Dashboard.Pages.Functions.Create; + +/// +/// The of the workflow editor +/// +[Feature] +public record CreateFunctionViewState +{ + /// + /// Gets a that contains all s + /// + public EquatableList? Namespaces { get; set; } + + /// + /// Gets/sets the 's namespace + /// + public string? Namespace { get; set; } + + /// + /// Gets/sets the 's name + /// + public string? Name { get; set; } + + /// + /// Gets/sets the 's namespace, when the user is creating one + /// + public string? ChosenNamespace { get; set; } + + /// + /// Gets/sets the 's name, when the user is creating one + /// + public string? ChosenName { get; set; } + + /// + /// Gets/sets the function text representation + /// + public SemVersion Version { get; set; } = new SemVersion(1, 0, 0); + + /// + /// Gets/sets the definition of the workflow to create + /// + public TaskDefinition? Function { get; set; } = null; + + /// + /// Gets/sets the function text representation + /// + public string? FunctionText { get; set; } + + /// + /// Gets/sets a boolean indicating whether or not the state is being loaded + /// + public bool Loading { get; set; } = false; + + /// + /// Defines if the function is being updated + /// + public bool Updating { get; set; } = false; + + /// + /// Defines if the function is being saved + /// + public bool Saving { get; set; } = false; + + /// + /// Gets/sets the type that occurred when trying to save the resource, if any + /// + public Uri? ProblemType { get; set; } = null; + + /// + /// Gets/sets the title that occurred when trying to save the resource, if any + /// + public string ProblemTitle { get; set; } = string.Empty; + + /// + /// Gets/sets the details that occurred when trying to save the resource, if any + /// + public string ProblemDetail { get; set; } = string.Empty; + + /// + /// Gets/sets the status that occurred when trying to save the resource, if any + /// + public int ProblemStatus { get; set; } = 0; + + /// + /// Gets/sets the list of errors that occurred when trying to save the resource, if any + /// + public IDictionary ProblemErrors { get; set; } = new Dictionary(); + +} diff --git a/src/dashboard/Synapse.Dashboard/Pages/Functions/Create/Store.cs b/src/dashboard/Synapse.Dashboard/Pages/Functions/Create/Store.cs new file mode 100644 index 000000000..a2b7d9fd2 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Pages/Functions/Create/Store.cs @@ -0,0 +1,628 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using JsonCons.Utilities; +using Neuroglia.Data; +using Semver; +using ServerlessWorkflow.Sdk.Models; +using Synapse.Api.Client.Services; +using Synapse.Resources; + +namespace Synapse.Dashboard.Pages.Functions.Create; + +/// +/// Represents the +/// +/// The service used to perform logging +/// The service used to interact with the Synapse API +/// The service used to help handling Monaco editors +/// The service used to serialize/deserialize data to/from JSON +/// The service used to serialize/deserialize data to/from YAML +/// The service used for JS interop +/// The service used to provides an abstraction for querying and managing URI navigation +/// The service used to download the specification schemas +/// The service to build a bridge with the monaco interop extension +public class CreateFunctionViewStore( + ILogger logger, + ISynapseApiClient apiClient, + IMonacoEditorHelper monacoEditorHelper, + IJsonSerializer jsonSerializer, + IYamlSerializer yamlSerializer, + IJSRuntime jsRuntime, + NavigationManager navigationManager, + SpecificationSchemaManager specificationSchemaManager, + MonacoInterop monacoInterop +) + : ComponentStore(new()) +{ + + private TextModel? _textModel = null; + private string _textModelUri = string.Empty; + private bool _disposed = false; + private bool _processingVersion = false; + + /// + /// Gets the service used to perform logging + /// + protected ILogger Logger { get; } = logger; + + /// + /// Gets the service used to interact with the Synapse API + /// + protected ISynapseApiClient ApiClient { get; } = apiClient; + + /// + /// Gets the service used to help handling Monaco editors + /// + protected IMonacoEditorHelper MonacoEditorHelper { get; } = monacoEditorHelper; + + /// + /// Gets the service used to serialize/deserialize data to/from JSON + /// + protected IJsonSerializer JsonSerializer { get; } = jsonSerializer; + + /// + /// Gets the service used to serialize/deserialize data to/from YAML + /// + protected IYamlSerializer YamlSerializer { get; } = yamlSerializer; + + /// + /// Gets the service used for JS interop + /// + protected IJSRuntime JSRuntime { get; } = jsRuntime; + + /// + /// Gets the service used to provides an abstraction for querying and managing URI navigation + /// + protected NavigationManager NavigationManager { get; } = navigationManager; + + /// + /// Gets the service used to download the specification schemas + /// + protected SpecificationSchemaManager SpecificationSchemaManager { get; } = specificationSchemaManager; + + /// + /// Gets the service to build a bridge with the monaco interop extension + /// + protected MonacoInterop MonacoInterop { get; } = monacoInterop; + + /// + /// The provider function + /// + public Func StandaloneEditorConstructionOptions = monacoEditorHelper.GetStandaloneEditorConstructionOptions(string.Empty, false, monacoEditorHelper.PreferredLanguage); + + /// + /// The reference + /// + public StandaloneCodeEditor? TextEditor { get; set; } + + /// + /// Gets an used to observe s + /// + public IObservable?> Namespaces => this.Select(s => s.Namespaces).DistinctUntilChanged(); + + #region Selectors + /// + /// Gets an used to observe changes + /// + public IObservable Namespace => this.Select(state => state.Namespace).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable Name => this.Select(state => state.Name).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable ChosenNamespace => this.Select(state => state.ChosenNamespace).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable ChosenName => this.Select(state => state.ChosenName).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes to the state's property + /// + public IObservable Function => this.Select(state => state.Function).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes to the state's property + /// + public IObservable Version => this.Select(state => state.Version).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes to the state's property + /// + public IObservable FunctionText => this.Select(state => state.FunctionText).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes to the state's property + /// + public IObservable Loading => this.Select(state => state.Loading).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes to the state's property + /// + public IObservable Saving => this.Select(state => state.Saving).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable ProblemType => this.Select(state => state.ProblemType).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable ProblemTitle => this.Select(state => state.ProblemTitle).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable ProblemDetail => this.Select(state => state.ProblemDetail).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable ProblemStatus => this.Select(state => state.ProblemStatus).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable> ProblemErrors => this.Select(state => state.ProblemErrors).DistinctUntilChanged(); + + /// + /// Gets an used to observe computed + /// + public IObservable ProblemDetails => Observable.CombineLatest( + this.ProblemType, + this.ProblemTitle, + this.ProblemStatus, + this.ProblemDetail, + this.ProblemErrors, + (type, title, status, details, errors) => + { + if (string.IsNullOrWhiteSpace(title)) + { + return null; + } + return new ProblemDetails(type ?? new Uri("unknown://"), title, status, details, null, errors, null); + } + ); + #endregion + + #region Setters + /// + /// Sets the state's + /// + /// The new value + public void SetNamespace(string? ns) + { + this.Reduce(state => state with + { + Namespace = ns, + Loading = true + }); + } + + /// + /// Sets the state's + /// + /// The new value + public void SetName(string? name) + { + this.Reduce(state => state with + { + Name = name, + Loading = true + }); + } + /// + /// Sets the state's + /// + /// The new value + public void SetChosenNamespace(string? ns) + { + this.Reduce(state => state with + { + ChosenNamespace = ns + }); + } + + /// + /// Sets the state's + /// + /// The new value + public void SetChosenName(string? name) + { + this.Reduce(state => state with + { + ChosenName = name + }); + } + + /// + /// Sets the state's 's related data + /// + /// The to populate the data with + public void SetProblemDetails(ProblemDetails? problem) + { + this.Reduce(state => state with + { + ProblemType = problem?.Type, + ProblemTitle = problem?.Title ?? string.Empty, + ProblemStatus = problem?.Status ?? 0, + ProblemDetail = problem?.Detail ?? string.Empty, + ProblemErrors = problem?.Errors?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? [] + }); + } + #endregion + + #region Actions + /// + /// Gets the for the specified namespace and name + /// + /// The namespace the to create a new version of belongs to + /// The name of the to create a new version of + /// A new awaitable + public async Task GetCustomFunctionAsync(string @namespace, string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(@namespace); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + var resources = await this.ApiClient.CustomFunctions.GetAsync(name, @namespace) ?? throw new NullReferenceException($"Failed to find the specified function '{name}.{@namespace}'"); + var version = resources.Spec.Versions.GetLatestVersion(); + var function = resources.Spec.Versions.GetLatest(); + var nextVersion = SemVersion.Parse(version, SemVersionStyles.Strict); + nextVersion = nextVersion.WithPatch(nextVersion.Patch + 1); + this.Reduce(s => s with + { + Function = function, + Version = nextVersion, + Loading = false + }); + } + + /// + /// Handles changed of the text editor's language + /// + /// + /// + public async Task ToggleTextBasedEditorLanguageAsync(string _) + { + if (this.TextEditor == null) + { + return; + } + var language = this.MonacoEditorHelper.PreferredLanguage; + try + { + var document = await this.TextEditor.GetValue(); + if (document == null) + { + return; + } + document = language == PreferredLanguage.YAML ? + this.YamlSerializer.ConvertFromJson(document) : + this.YamlSerializer.ConvertToJson(document); + this.Reduce(state => state with + { + FunctionText = document + }); + await this.OnTextBasedEditorInitAsync(); + } + catch (Exception ex) + { + this.Logger.LogError("Unable to change text editor language: {exception}", ex.ToString()); + await this.MonacoEditorHelper.ChangePreferredLanguageAsync(language == PreferredLanguage.YAML ? PreferredLanguage.JSON : PreferredLanguage.YAML); + } + } + + /// + /// Handles initialization of the text editor + /// + /// + public async Task OnTextBasedEditorInitAsync() + { + await this.SetTextBasedEditorLanguageAsync(); + await this.SetTextEditorValueAsync(); + } + + /// + /// Sets the language of the text editor + /// + /// + public async Task SetTextBasedEditorLanguageAsync() + { + if (this.TextEditor == null) + { + return; + } + try + { + var language = this.MonacoEditorHelper.PreferredLanguage; + this._textModel = await Global.GetModel(this.JSRuntime, this._textModelUri); + this._textModel ??= await Global.CreateModel(this.JSRuntime, "", language, this._textModelUri); + await Global.SetModelLanguage(this.JSRuntime, this._textModel, language); + await this.TextEditor!.SetModel(this._textModel); + } + catch (Exception ex) + { + this.Logger.LogError("Unable to set text editor language: {exception}", ex.ToString()); + } + } + + /// + /// Changes the value of the text editor + /// + /// + async Task SetTextEditorValueAsync() + { + var document = this.Get(state => state.FunctionText); + if (this.TextEditor == null || string.IsNullOrWhiteSpace(document)) + { + return; + } + await this.TextEditor.SetValue(document); + try + { + await this.TextEditor.Trigger("", "editor.action.formatDocument"); + } + catch (Exception ex) + { + this.Logger.LogError("Unable to set text editor value: {exception}", ex.ToString()); + } + } + + /// + /// Handles text editor content changes + /// + /// The + /// An awaitable task + public async Task OnDidChangeModelContent(ModelContentChangedEvent e) + { + if (this.TextEditor == null) return; + var document = await this.TextEditor.GetValue(); + this.Reduce(state => state with + { + FunctionText = document + }); + } + + /// + /// Saves the by posting it to the Synapse API + /// + /// A new awaitable + public async Task SaveCustomFunctionAsync() + { + if (this.TextEditor == null) + { + this.Reduce(state => state with + { + ProblemTitle = "Text editor", + ProblemDetail = "The text editor must be initialized." + }); + return; + } + var functionText = await this.TextEditor.GetValue(); + if (string.IsNullOrWhiteSpace(functionText)) + { + this.Reduce(state => state with + { + ProblemTitle = "Invalid function", + ProblemDetail = "The function cannot be empty." + }); + return; + } + try + { + var function = this.MonacoEditorHelper.PreferredLanguage == PreferredLanguage.JSON ? + this.JsonSerializer.Deserialize(functionText)! : + this.YamlSerializer.Deserialize(functionText)!; + var @namespace = this.Get(state => state.Namespace) ?? this.Get(state => state.ChosenNamespace); + var name = this.Get(state => state.Name) ?? this.Get(state => state.ChosenName); + var version = this.Get(state => state.Version).ToString(); + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(@namespace)) + { + this.Reduce(state => state with + { + ProblemTitle = "Invalid function", + ProblemDetail = "The name or namespace cannot be empty." + }); + return; + } + this.Reduce(s => s with + { + Saving = true + }); + CustomFunction? resource = null; + try + { + resource = await this.ApiClient.CustomFunctions.GetAsync(name, @namespace); + } + catch + { + // Assume 404, might need actual handling + } + if (resource == null) + { + resource = await this.ApiClient.CustomFunctions.CreateAsync(new() + { + Metadata = new() + { + Namespace = @namespace, + Name = name + }, + Spec = new() + { + Versions = [new(version, function)] + } + }); + } + else + { + var updatedResource = resource.Clone()!; + updatedResource.Spec.Versions.Add(new(version, function)); + var jsonPatch = JsonPatch.FromDiff(this.JsonSerializer.SerializeToElement(resource)!.Value, this.JsonSerializer.SerializeToElement(updatedResource)!.Value); + var patch = this.JsonSerializer.Deserialize(jsonPatch.RootElement); + if (patch != null) + { + var resourcePatch = new Patch(PatchType.JsonPatch, jsonPatch); + await this.ApiClient.ManageNamespaced().PatchAsync(name, @namespace, resourcePatch, null, this.CancellationTokenSource.Token); + } + } + this.NavigationManager.NavigateTo($"/functions/{@namespace}/{name}"); + } + catch (ProblemDetailsException ex) + { + this.SetProblemDetails(ex.Problem); + } + catch (YamlDotNet.Core.YamlException ex) + { + this.Reduce(state => state with + { + ProblemTitle = "Serialization error", + ProblemDetail = "The function definition cannot be serialized.", + ProblemErrors = new Dictionary() + { + {"Message", [ex.Message] }, + {"Start", [ex.Start.ToString()] }, + {"End", [ex.End.ToString()] } + } + }); + } + catch (System.Text.Json.JsonException ex) + { + this.Reduce(state => state with + { + ProblemTitle = "Serialization error", + ProblemDetail = "The function definition cannot be serialized.", + ProblemErrors = new Dictionary() + { + {"Message", [ex.Message] }, + {"LineNumber", [ex.LineNumber?.ToString()??""] } + } + }); + } + catch (Exception ex) + { + this.Logger.LogError("Unable to save function definition: {exception}", ex.ToString()); + } + finally + { + this.Reduce(s => s with + { + Saving = false + }); + } + } + #endregion + + /// + public override async Task InitializeAsync() + { + await this.ListNamespacesAsync().ConfigureAwait(false); + this.Function.SubscribeAsync(async definition => { + string document = ""; + if (definition != null) + { + document = this.MonacoEditorHelper.PreferredLanguage == PreferredLanguage.JSON ? + this.JsonSerializer.SerializeToText(definition) : + this.YamlSerializer.SerializeToText(definition); + } + this.Reduce(state => state with + { + FunctionText = document + }); + await this.OnTextBasedEditorInitAsync(); + }, cancellationToken: this.CancellationTokenSource.Token); + Observable.CombineLatest( + this.Namespace.Where(ns => !string.IsNullOrWhiteSpace(ns)), + this.Name.Where(name => !string.IsNullOrWhiteSpace(name)), + (ns, name) => (ns!, name!) + ).SubscribeAsync(async ((string ns, string name) function) => + { + await this.GetCustomFunctionAsync(function.ns, function.name); + }, cancellationToken: this.CancellationTokenSource.Token); + await this.SetValidationSchema(); + await base.InitializeAsync(); + } + + /// + /// Adds validation for the specification of the specified version + /// + /// The version of the spec to add the validation for + /// An awaitable task + protected async Task SetValidationSchema(string? version = null) + { + version ??= await this.SpecificationSchemaManager.GetLatestVersion(); + if (this._processingVersion) + { + return; + } + this.SetProblemDetails(null); + this._processingVersion = true; + try + { + var schema = $"https://raw.githubusercontent.com/serverlessworkflow/specification/{version}/schema/workflow.yaml#/$defs/task"; + var type = $"create_{typeof(CustomFunction).Name.ToLower()}_{version}_schema"; + await this.MonacoInterop.AddValidationSchemaAsync(schema, $"https://synapse.io/schemas/{type}.json", $"{type}*").ConfigureAwait(false); + this._textModelUri = this.MonacoEditorHelper.GetResourceUri(type); + } + catch (Exception ex) + { + this.Logger.LogError("Unable to set the validation schema: {exception}", ex.ToString()); + this.SetProblemDetails(new ProblemDetails(new Uri("about:blank"), "Unable to set the validation schema", 404, $"Unable to set the validation schema for the specification version '{version}'. Make sure the version exists.")); + } + this._processingVersion = false; + } + + /// + /// Lists all available s + /// + /// A new awaitable + public virtual async Task ListNamespacesAsync() + { + var namespaceList = new EquatableList(await (await this.ApiClient.Namespaces.ListAsync().ConfigureAwait(false)).OrderBy(ns => ns.GetQualifiedName()).ToListAsync().ConfigureAwait(false)); + this.Reduce(s => s with + { + Namespaces = namespaceList + }); + } + + /// + /// Disposes of the store + /// + /// A boolean indicating whether or not the dispose of the store + protected override void Dispose(bool disposing) + { + if (!this._disposed) + { + if (disposing) + { + if (this._textModel != null) + { + this._textModel.DisposeModel(); + this._textModel = null; + } + if (this.TextEditor != null) + { + this.TextEditor.Dispose(); + this.TextEditor = null; + } + } + this._disposed = true; + } + } + +} diff --git a/src/dashboard/Synapse.Dashboard/Pages/Functions/Create/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Functions/Create/View.razor new file mode 100644 index 000000000..f3c692d99 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Pages/Functions/Create/View.razor @@ -0,0 +1,156 @@ +@* + Copyright © 2024-Present The Synapse Authors +

+ Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at +

+ http://www.apache.org/licenses/LICENSE-2.0 +

+ Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*@ + +@page "/functions/new" +@page "/functions/new/{namespace}/{name}" +@using ServerlessWorkflow.Sdk.Models +@using Synapse.Api.Client.Services +@inherits StatefulComponent +@inject IBreadcrumbManager BreadcrumbManager +@inject NavigationManager NavigationManager + +New function + +

New Function @((!string.IsNullOrEmpty(ns) || !string.IsNullOrEmpty(name) || !string.IsNullOrEmpty(chosenName) || !string.IsNullOrEmpty(chosenNamespace)) ? $"({name??chosenName}.{ns??chosenNamespace}:{version})" : "")

+ +@if (loading) +{ +
+ +
+} +else if (string.IsNullOrEmpty(ns) && string.IsNullOrEmpty(name) && string.IsNullOrEmpty(chosenName) && string.IsNullOrEmpty(chosenNamespace)) +{ +
+ + + +
+} +else +{ +
+ +
+ + @if (problemDetails != null) + { +
+ +

@problemDetails.Detail

+ + @if (problemDetails.Errors != null && problemDetails.Errors.Any()) + { + foreach (KeyValuePair errorContainer in problemDetails.Errors) + { + @errorContainer.Key: +
    + @foreach (string error in errorContainer.Value) + { +
  • @error
  • + } +
+ } + } +
+
+ } + +} + +@code { + + string? ns; + string? name; + string? version; + string? chosenNamespace; + string? chosenName; + string? nameInputValue; + string? namespaceSelectValue; + bool loading; + bool saving; + ProblemDetails? problemDetails = null; + + [Parameter] public string? Namespace { get; set; } + [Parameter] public string? Name { get; set; } + public EquatableList? Namespaces { get; set; } + + /// + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + BreadcrumbManager.Use(Breadcrumbs.Functions); + BreadcrumbManager.Add(new($"New", $"/functions/new")); + Store.Namespace.Subscribe(value => OnStateChanged(_ => ns = value), token: CancellationTokenSource.Token); + Store.Name.Subscribe(value => OnStateChanged(_ => name = value), token: CancellationTokenSource.Token); + Store.ChosenNamespace.Subscribe(value => OnStateChanged(_ => chosenNamespace = value), token: CancellationTokenSource.Token); + Store.ChosenName.Subscribe(value => OnStateChanged(_ => chosenName = value), token: CancellationTokenSource.Token); + Store.Version.Subscribe(value => OnStateChanged(_ => version = value.ToString()), token: CancellationTokenSource.Token); + Store.Namespaces.Subscribe(value => this.OnStateChanged(_ => Namespaces = value), token: this.CancellationTokenSource.Token); + Store.Loading.Subscribe(value => OnStateChanged(_ => loading = value), token: CancellationTokenSource.Token); + Store.Saving.Subscribe(value => OnStateChanged(_ => saving = value), token: CancellationTokenSource.Token); + Store.ProblemDetails.Subscribe(problemDetails => OnStateChanged(cmp => cmp.problemDetails = problemDetails), token: CancellationTokenSource.Token); + } + + /// + protected override void OnParametersSet() + { + if (Namespace != ns) + { + Store.SetNamespace(Namespace); + loading = true; + } + if (Name != name) + { + Store.SetName(Name); + } + } + + protected void SetNameAndNamespace() + { + if (string.IsNullOrEmpty(nameInputValue) || string.IsNullOrEmpty(namespaceSelectValue)) + { + return; + } + Store.SetChosenNamespace(namespaceSelectValue); + Store.SetChosenName(nameInputValue); + } + +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Pages/Functions/List/State.cs b/src/dashboard/Synapse.Dashboard/Pages/Functions/List/State.cs new file mode 100644 index 000000000..c2e1fdbff --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Pages/Functions/List/State.cs @@ -0,0 +1,27 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Synapse.Resources; + +namespace Synapse.Dashboard.Pages.Functions.List; + +/// +/// Represents the 's state +/// +public record FunctionListState + : NamespacedResourceManagementComponentState +{ + + + +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Pages/Functions/List/Store.cs b/src/dashboard/Synapse.Dashboard/Pages/Functions/List/Store.cs new file mode 100644 index 000000000..44108b507 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Pages/Functions/List/Store.cs @@ -0,0 +1,31 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Synapse.Api.Client.Services; +using Synapse.Resources; + +namespace Synapse.Dashboard.Pages.Functions.List; + +/// +/// Represents the 's store +/// +/// The service used to perform logging +/// The service used to interact with the Synapse API +/// The hub used to watch resource events +public class FunctionListComponentStore(ILogger logger, ISynapseApiClient apiClient, ResourceWatchEventHubClient resourceEventHub) + : NamespacedResourceManagementComponentStore(logger, apiClient, resourceEventHub) +{ + + + +} diff --git a/src/dashboard/Synapse.Dashboard/Pages/Functions/List/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Functions/List/View.razor new file mode 100644 index 000000000..05aa462ec --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Pages/Functions/List/View.razor @@ -0,0 +1,139 @@ +@* + Copyright © 2024-Present The Synapse Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*@ + +@page "/functions/{namespace?}/{name?}" +@attribute [Authorize] +@namespace Synapse.Dashboard.Pages.Functions.List +@using BlazorBootstrap +@inherits NamespacedResourceManagementComponent +@inject IBreadcrumbManager BreadcrumbManager +@inject NavigationManager NavigationManager + +Custom Functions + +
+ @if (Loading) + { + + } +
+

Custom Functions

+ @(Resources?.Count ?? 0) items +
+ + + +
+
+ + + + + + + + + + + + + + @if (Resources != null && Resources.Count > 0) + { + + + + + + + + + + + + } + +
NameNamespaceCreation TimeVersionsLatest + +
@resource.Metadata.Name + @resource.Metadata.Namespace + + @resource.Metadata.CreationTimestamp?.RelativeFormat()@resource.Spec.Versions.Count@resource.Spec.Versions.GetLatestVersion() + + + +
+
+ + + + + + +@code { + + [Parameter] public string? FunctionName { get; set; } + + RenderFragment Title() => __builder => + { +

Custom Functions

+ }; + + void OnCreateFunction() => this.NavigationManager.NavigateTo("/functions/new"); + + void OnCreateFunctionVersion(string ns, string name) => this.NavigationManager.NavigateTo($"/functions/new/{ns}/{name}"); + + /// + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + BreadcrumbManager.Use(Breadcrumbs.Functions); + } + + /// + /// Opens the targeted 's details + /// + /// The to show the details for + protected override Task OnShowResourceDetailsAsync(CustomFunction resource) + { + if (this.DetailsOffCanvas == null) return Task.CompletedTask; + var parameters = new Dictionary + { + { nameof(CustomFunctionDetails.Resource), resource } + }; + return this.DetailsOffCanvas.ShowAsync(title: "Custom function details", parameters: parameters); + } +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Pages/ServiceAccounts/List/View.razor b/src/dashboard/Synapse.Dashboard/Pages/ServiceAccounts/List/View.razor new file mode 100644 index 000000000..2c4b88ec9 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Pages/ServiceAccounts/List/View.razor @@ -0,0 +1,105 @@ +@* + Copyright © 2024-Present The Synapse Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*@ + +@page "/service-accounts/{namespace?}/{name?}" +@attribute [Authorize] +@namespace Synapse.Dashboard.Pages.ServiceAccounts.List +@inherits NamespacedResourceManagementComponent +@inject IBreadcrumbManager BreadcrumbManager + +Service Accounts + +
+ @if (Loading) + { + + } +
+

Service Accounts

+ @(Resources?.Count ?? 0) items +
+ + + +
+
+ + + + + + + + + + + + @if (Resources != null && Resources.Any()) + { + + + + + + + + + + } + +
NamespaceNameCreation Time + +
@resource.Metadata.Namespace@resource.Metadata.Name@resource.Metadata.CreationTimestamp?.RelativeFormat() + + + + +
+
+ + + + + + + + + +@code { + /// + protected override void OnInitialized() + { + base.OnInitialized(); + BreadcrumbManager.Use(Breadcrumbs.ServiceAccounts); + } +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor index 0e9e8a65f..7220ced8a 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor @@ -144,10 +144,6 @@ - - - - @code diff --git a/src/dashboard/Synapse.Dashboard/Services/MonacoInterop.cs b/src/dashboard/Synapse.Dashboard/Services/MonacoInterop.cs index 315fb4b3f..56506589a 100644 --- a/src/dashboard/Synapse.Dashboard/Services/MonacoInterop.cs +++ b/src/dashboard/Synapse.Dashboard/Services/MonacoInterop.cs @@ -27,7 +27,7 @@ public class MonacoInterop(IJSRuntime jsRuntime) /// /// A reference to the js interop module /// - readonly Lazy> moduleTask = new(() => jsRuntime.InvokeAsync("import", "./js/monaco-editor-interop-extension.js?v=2").AsTask()); + readonly Lazy> moduleTask = new(() => jsRuntime.InvokeAsync("import", "./js/monaco-editor-interop-extension.js?v=3.1").AsTask()); /// /// Adds the provided schema to monaco editor's diagnostics options diff --git a/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs b/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs index 305a09912..3e8947e7d 100644 --- a/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs +++ b/src/dashboard/Synapse.Dashboard/Services/WorkflowGraphBuilder.cs @@ -230,21 +230,20 @@ protected virtual NodeViewModel BuildCallTaskNode(TaskNodeRenderingContext s.uri === schemaUri)) { monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ @@ -18,7 +24,7 @@ export function addValidationSchema(schema, schemaUri, schemaType) { schemas: [ ...monaco.languages.json.jsonDefaults.diagnosticsOptions.schemas, { - schema: JSON.parse(schema), + schema, uri: schemaUri, fileMatch: [schemaType] } @@ -36,7 +42,7 @@ export function addValidationSchema(schema, schemaUri, schemaType) { schemas: [ ...monacoYaml.yamlDefaults.diagnosticsOptions.schemas, { - schema: JSON.parse(schema), + schema, uri: schemaUri, fileMatch: [schemaType] } diff --git a/src/operator/Synapse.Operator/Synapse.Operator.csproj b/src/operator/Synapse.Operator/Synapse.Operator.csproj index b384a7848..b4dfbda43 100644 --- a/src/operator/Synapse.Operator/Synapse.Operator.csproj +++ b/src/operator/Synapse.Operator/Synapse.Operator.csproj @@ -8,7 +8,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/runner/Synapse.Runner/Configuration/RunnerCertificateOptions.cs b/src/runner/Synapse.Runner/Configuration/RunnerCertificateOptions.cs new file mode 100644 index 000000000..edb8d0635 --- /dev/null +++ b/src/runner/Synapse.Runner/Configuration/RunnerCertificateOptions.cs @@ -0,0 +1,39 @@ +// Copyright © 2024-Present The Synapse Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"), +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Runtime.Serialization; + +namespace Synapse.Runner.Configuration; + +/// +/// Represents the options used to configure how a Synapse Runner should handle certificates +/// +[DataContract] +public class RunnerCertificateOptions +{ + + /// + /// Initializes a new + /// + public RunnerCertificateOptions() + { + var env = Environment.GetEnvironmentVariable(SynapseDefaults.EnvironmentVariables.SkipCertificateValidation); + if (!string.IsNullOrWhiteSpace(env) && bool.TryParse(env, out var skipValidation)) this.Validate = !skipValidation; + } + + /// + /// Gets/sets a boolean indicating whether or not the runner should validate certificates + /// + public virtual bool Validate { get; set; } = true; + +} \ No newline at end of file diff --git a/src/runner/Synapse.Runner/Configuration/RunnerCloudEventOptions.cs b/src/runner/Synapse.Runner/Configuration/RunnerCloudEventOptions.cs index 979cf9f60..79432187f 100644 --- a/src/runner/Synapse.Runner/Configuration/RunnerCloudEventOptions.cs +++ b/src/runner/Synapse.Runner/Configuration/RunnerCloudEventOptions.cs @@ -36,4 +36,4 @@ public RunnerCloudEventOptions() /// public virtual bool PublishLifecycleEvents { get; set; } = true; -} \ No newline at end of file +} diff --git a/src/runner/Synapse.Runner/Configuration/RunnerOptions.cs b/src/runner/Synapse.Runner/Configuration/RunnerOptions.cs index 458685e44..94db483b8 100644 --- a/src/runner/Synapse.Runner/Configuration/RunnerOptions.cs +++ b/src/runner/Synapse.Runner/Configuration/RunnerOptions.cs @@ -28,6 +28,11 @@ public class RunnerOptions /// public virtual SynapseHttpApiClientOptions Api { get; set; } = new(); + /// + /// Gets/sets the options used to configure how the Synapse Runner should handle certificates + /// + public virtual RunnerCertificateOptions Certificates { get; set; } = new(); + /// /// Gets/sets the options used to configure the cloud events published by the Synapse Runner /// diff --git a/src/runner/Synapse.Runner/Program.cs b/src/runner/Synapse.Runner/Program.cs index 441b2c5e9..e97eae2cd 100644 --- a/src/runner/Synapse.Runner/Program.cs +++ b/src/runner/Synapse.Runner/Program.cs @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Microsoft.Extensions.Http; using Synapse; using Synapse.Core.Infrastructure.Containers; @@ -84,6 +85,22 @@ services.AddSingleton(); services.AddSingleton(); services.AddHostedService(); + + if (!options.Certificates.Validate) + { + ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true; + services.ConfigureAll(options => + { + options.HttpMessageHandlerBuilderActions.Add(builder => + { + builder.PrimaryHandler = new HttpClientHandler + { + ClientCertificateOptions = ClientCertificateOption.Manual, + ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true + }; + }); + }); + } }); using var app = builder.Build(); diff --git a/src/runner/Synapse.Runner/Services/Executors/FunctionCallExecutor.cs b/src/runner/Synapse.Runner/Services/Executors/FunctionCallExecutor.cs index b53591f98..36aee541d 100644 --- a/src/runner/Synapse.Runner/Services/Executors/FunctionCallExecutor.cs +++ b/src/runner/Synapse.Runner/Services/Executors/FunctionCallExecutor.cs @@ -55,7 +55,13 @@ public override async Task InitializeAsync(CancellationToken cancellationToken = { await base.InitializeAsync(cancellationToken).ConfigureAwait(false); if (this.Task.Workflow.Definition.Use?.Functions?.TryGetValue(this.Task.Definition.Call, out var function) == true && function != null) this.Function = function; - else if (Uri.TryCreate(this.Task.Definition.Call, UriKind.Absolute, out var uri)) this.Function = await this.GetCustomFunctionAsync(uri, cancellationToken).ConfigureAwait(false); + else if (Uri.TryCreate(this.Task.Definition.Call, UriKind.Absolute, out var uri) && (uri.IsFile || !string.IsNullOrWhiteSpace(uri.Host))) this.Function = await this.GetCustomFunctionAsync(uri, cancellationToken).ConfigureAwait(false); + else if (this.Task.Definition.Call.Contains('@')) + { + var components = this.Task.Definition.Call.Split('@', StringSplitOptions.RemoveEmptyEntries); + if (components.Length != 2) throw new NotSupportedException($"Unknown/unsupported function '{this.Task.Definition.Call}'"); + this.Function = await this.GetCustomFunctionAsync(components[0], components[1], cancellationToken).ConfigureAwait(false); + } else throw new NotSupportedException($"Unknown/unsupported function '{this.Task.Definition.Call}'"); } @@ -83,6 +89,33 @@ protected virtual async Task GetCustomFunctionAsync(Uri uri, Can } } + /// + /// Gets the custom function with the specified name from the specified catalog + /// + /// The name of the custom function to get + /// The name of the catalog to get the custom function from + /// A + /// The of the custom function with the specified name + protected virtual async Task GetCustomFunctionAsync(string functionName, string catalogName, CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(functionName); + ArgumentException.ThrowIfNullOrWhiteSpace(catalogName); + if (catalogName == SynapseDefaults.Tasks.CustomFunctions.Catalogs.Default) + { + var components = functionName.Split(':', StringSplitOptions.RemoveEmptyEntries); + if (components.Length != 2) throw new Exception($"The specified value '{functionName}' is not a valid custom function versioned qualified name ({{name}}.{{namespace}}:{{version}})"); + var qualifiedName = components[0]; + var version = components[1]; + components = qualifiedName.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (components.Length != 2) throw new Exception($"The specified value '{functionName}' is not a valid custom function qualified name ({{name}}.{{namespace}})"); + var name = components[0]; + var @namespace = components[1]; + var function = await this.Task.Workflow.CustomFunctions.GetAsync(name, @namespace, cancellationToken).ConfigureAwait(false) ?? throw new NullReferenceException($"Failed to find the specified custom function '{qualifiedName}'"); + return function.Spec.Versions.Get(version) ?? throw new NullReferenceException($"Failed to find the version '{version}' of the custom function '{qualifiedName}'"); + } + else throw new NotImplementedException("Using non-default custom function catalog is not yet implemented"); //todo: implement + } + /// protected override async Task CreateTaskExecutorAsync(TaskInstance task, TaskDefinition definition, IDictionary contextData, IDictionary? arguments = null, CancellationToken cancellationToken = default) { diff --git a/src/runner/Synapse.Runner/Services/Interfaces/IWorkflowExecutionContext.cs b/src/runner/Synapse.Runner/Services/Interfaces/IWorkflowExecutionContext.cs index 5695545a8..20305a22e 100644 --- a/src/runner/Synapse.Runner/Services/Interfaces/IWorkflowExecutionContext.cs +++ b/src/runner/Synapse.Runner/Services/Interfaces/IWorkflowExecutionContext.cs @@ -44,6 +44,11 @@ public interface IWorkflowExecutionContext /// IDocumentApiClient Documents { get; } + /// + /// Gets the Synapse API used to manage s + /// + INamespacedResourceApiClient CustomFunctions { get; } + /// /// Gets/sets the workflow's context data /// diff --git a/src/runner/Synapse.Runner/Services/WorkflowExecutionContext.cs b/src/runner/Synapse.Runner/Services/WorkflowExecutionContext.cs index 105169221..88bcb4e4c 100644 --- a/src/runner/Synapse.Runner/Services/WorkflowExecutionContext.cs +++ b/src/runner/Synapse.Runner/Services/WorkflowExecutionContext.cs @@ -67,6 +67,9 @@ public class WorkflowExecutionContext(IServiceProvider services, IExpressionEval /// public IDocumentApiClient Documents => this.Api.Documents; + /// + public INamespacedResourceApiClient CustomFunctions => this.Api.CustomFunctions; + /// /// Gets the object used to asynchronously lock the /// diff --git a/src/runner/Synapse.Runner/Synapse.Runner.csproj b/src/runner/Synapse.Runner/Synapse.Runner.csproj index 15abab077..b8714984a 100644 --- a/src/runner/Synapse.Runner/Synapse.Runner.csproj +++ b/src/runner/Synapse.Runner/Synapse.Runner.csproj @@ -8,7 +8,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/runtime/Synapse.Runtime.Abstractions/Synapse.Runtime.Abstractions.csproj b/src/runtime/Synapse.Runtime.Abstractions/Synapse.Runtime.Abstractions.csproj index 57935eca2..dda8668c1 100644 --- a/src/runtime/Synapse.Runtime.Abstractions/Synapse.Runtime.Abstractions.csproj +++ b/src/runtime/Synapse.Runtime.Abstractions/Synapse.Runtime.Abstractions.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/runtime/Synapse.Runtime.Docker/Synapse.Runtime.Docker.csproj b/src/runtime/Synapse.Runtime.Docker/Synapse.Runtime.Docker.csproj index 5996fa377..aa2a63eb6 100644 --- a/src/runtime/Synapse.Runtime.Docker/Synapse.Runtime.Docker.csproj +++ b/src/runtime/Synapse.Runtime.Docker/Synapse.Runtime.Docker.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/runtime/Synapse.Runtime.Kubernetes/Synapse.Runtime.Kubernetes.csproj b/src/runtime/Synapse.Runtime.Kubernetes/Synapse.Runtime.Kubernetes.csproj index bda612a68..3db4abe4d 100644 --- a/src/runtime/Synapse.Runtime.Kubernetes/Synapse.Runtime.Kubernetes.csproj +++ b/src/runtime/Synapse.Runtime.Kubernetes/Synapse.Runtime.Kubernetes.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/src/runtime/Synapse.Runtime.Native/Synapse.Runtime.Native.csproj b/src/runtime/Synapse.Runtime.Native/Synapse.Runtime.Native.csproj index 719adedb8..ec08bb5a2 100644 --- a/src/runtime/Synapse.Runtime.Native/Synapse.Runtime.Native.csproj +++ b/src/runtime/Synapse.Runtime.Native/Synapse.Runtime.Native.csproj @@ -7,7 +7,7 @@ en True 1.0.0 - alpha3.3 + alpha4 $(VersionPrefix) $(VersionPrefix) The Synapse Authors diff --git a/tests/Synapse.IntegrationTests/Synapse.IntegrationTests.csproj b/tests/Synapse.IntegrationTests/Synapse.IntegrationTests.csproj index e6b25f100..824e49800 100644 --- a/tests/Synapse.IntegrationTests/Synapse.IntegrationTests.csproj +++ b/tests/Synapse.IntegrationTests/Synapse.IntegrationTests.csproj @@ -17,7 +17,7 @@ - + diff --git a/tests/Synapse.UnitTests/Services/MockCloudFlowsApiClient.cs b/tests/Synapse.UnitTests/Services/MockCloudFlowsApiClient.cs index 2289c8230..c92db3d56 100644 --- a/tests/Synapse.UnitTests/Services/MockCloudFlowsApiClient.cs +++ b/tests/Synapse.UnitTests/Services/MockCloudFlowsApiClient.cs @@ -24,6 +24,8 @@ internal class MockSynapseApiClient(IServiceProvider serviceProvider) public INamespacedResourceApiClient Correlators { get; } = ActivatorUtilities.CreateInstance>(serviceProvider); + public INamespacedResourceApiClient CustomFunctions { get; } = ActivatorUtilities.CreateInstance>(serviceProvider); + public IClusterResourceApiClient Namespaces { get; } = ActivatorUtilities.CreateInstance>(serviceProvider); public INamespacedResourceApiClient Operators { get; } = ActivatorUtilities.CreateInstance>(serviceProvider); diff --git a/tests/Synapse.UnitTests/Synapse.UnitTests.csproj b/tests/Synapse.UnitTests/Synapse.UnitTests.csproj index 539f543c4..07c5d7c14 100644 --- a/tests/Synapse.UnitTests/Synapse.UnitTests.csproj +++ b/tests/Synapse.UnitTests/Synapse.UnitTests.csproj @@ -22,8 +22,8 @@ - - + +