diff --git a/Synapse.sln b/Synapse.sln index 7da6cabb5..46aa37df3 100644 --- a/Synapse.sln +++ b/Synapse.sln @@ -53,10 +53,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Synapse.Runtime.Containeriz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Synapse.Runner", "src\runner\Synapse.Runner\Synapse.Runner.csproj", "{E5FAA9BA-07C3-49CF-AD3B-897AE1D0B018}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Synapse.Dashboard.StateManagement", "src\dashboard\Synapse.Dashboard.StateManagement\Synapse.Dashboard.StateManagement.csproj", "{91EF9F64-4997-407C-B353-C26B1421D0FB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Synapse.Dashboard.StateManagement", "src\dashboard\Synapse.Dashboard.StateManagement\Synapse.Dashboard.StateManagement.csproj", "{91EF9F64-4997-407C-B353-C26B1421D0FB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Synapse.Operator", "src\operator\Synapse.Operator\Synapse.Operator.csproj", "{A9085F4A-5FDF-4F4A-B267-A03BC5E0FDB0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Synapse.Cli", "src\cli\Synapse.Cli\Synapse.Cli.csproj", "{C86F6C8B-5946-433D-9E09-2C0269CE6372}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -131,6 +133,10 @@ Global {A9085F4A-5FDF-4F4A-B267-A03BC5E0FDB0}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9085F4A-5FDF-4F4A-B267-A03BC5E0FDB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {A9085F4A-5FDF-4F4A-B267-A03BC5E0FDB0}.Release|Any CPU.Build.0 = Release|Any CPU + {C86F6C8B-5946-433D-9E09-2C0269CE6372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C86F6C8B-5946-433D-9E09-2C0269CE6372}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C86F6C8B-5946-433D-9E09-2C0269CE6372}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C86F6C8B-5946-433D-9E09-2C0269CE6372}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -160,6 +166,7 @@ Global {E5FAA9BA-07C3-49CF-AD3B-897AE1D0B018} = {1DA47E5F-B23A-4D3C-96AA-4BD2662AB946} {91EF9F64-4997-407C-B353-C26B1421D0FB} = {7DF998B8-0FB1-470E-8ED0-EA1CC7B16901} {A9085F4A-5FDF-4F4A-B267-A03BC5E0FDB0} = {32EAD165-3D99-42CD-B3AF-05136DCC7F35} + {C86F6C8B-5946-433D-9E09-2C0269CE6372} = {D3B3B95D-B598-4B13-B754-4A7E530405A6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2A6C03D6-355A-4B39-9F2B-D0FDE429C0E2} diff --git a/src/api/Synapse.Api.Client.Http/Extensions/IServiceCollectionExtensions.cs b/src/api/Synapse.Api.Client.Http/Extensions/IServiceCollectionExtensions.cs index db8e48655..55d5c1544 100644 --- a/src/api/Synapse.Api.Client.Http/Extensions/IServiceCollectionExtensions.cs +++ b/src/api/Synapse.Api.Client.Http/Extensions/IServiceCollectionExtensions.cs @@ -11,13 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -using Synapse.Api.Client.Http.Configuration; -using Synapse.Api.Client.Services; - namespace Synapse.Api.Client; /// diff --git a/src/api/Synapse.Api.Client.Http/Extensions/ISynapseApiClientExtensions.cs b/src/api/Synapse.Api.Client.Http/Extensions/ISynapseApiClientExtensions.cs index 775011270..ae7633d4c 100644 --- a/src/api/Synapse.Api.Client.Http/Extensions/ISynapseApiClientExtensions.cs +++ b/src/api/Synapse.Api.Client.Http/Extensions/ISynapseApiClientExtensions.cs @@ -11,9 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Neuroglia.Data.Infrastructure.ResourceOriented; -using Synapse.Api.Client.Services; - namespace Synapse.Api.Client; /// diff --git a/src/api/Synapse.Api.Client.Http/Services/DocumentHttpApiClient.cs b/src/api/Synapse.Api.Client.Http/Services/DocumentHttpApiClient.cs index addc86913..e833277b3 100644 --- a/src/api/Synapse.Api.Client.Http/Services/DocumentHttpApiClient.cs +++ b/src/api/Synapse.Api.Client.Http/Services/DocumentHttpApiClient.cs @@ -11,12 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Microsoft.Extensions.Logging; -using Neuroglia; -using Neuroglia.Serialization; -using System.Net.Mime; -using System.Text; - namespace Synapse.Api.Client.Services; /// diff --git a/src/api/Synapse.Api.Client.Http/Services/ResourceHttpApiClient.cs b/src/api/Synapse.Api.Client.Http/Services/ResourceHttpApiClient.cs index c34c11659..6086baddc 100644 --- a/src/api/Synapse.Api.Client.Http/Services/ResourceHttpApiClient.cs +++ b/src/api/Synapse.Api.Client.Http/Services/ResourceHttpApiClient.cs @@ -11,14 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Microsoft.Extensions.Logging; -using Neuroglia; -using Neuroglia.Data; -using Neuroglia.Data.Infrastructure.ResourceOriented; -using Neuroglia.Serialization; -using System.Net.Mime; -using System.Text; - namespace Synapse.Api.Client.Services; /// @@ -229,7 +221,7 @@ public virtual async Task DeleteAsync(string name, string @namespace, Cancellati var resource = new TResource(); var uri = $"/api/{resource.Definition.Version}/{resource.Definition.Plural}/{@namespace}/{name}"; using var request = await this.ProcessRequestAsync(new HttpRequestMessage(HttpMethod.Delete, uri), cancellationToken).ConfigureAwait(false); - await ProcessResponseAsync(await this.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); + await this.ProcessResponseAsync(await this.HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); } /// diff --git a/src/api/Synapse.Api.Client.Http/Services/ResourceWatch.cs b/src/api/Synapse.Api.Client.Http/Services/ResourceWatch.cs index 102a6a909..56a6703bf 100644 --- a/src/api/Synapse.Api.Client.Http/Services/ResourceWatch.cs +++ b/src/api/Synapse.Api.Client.Http/Services/ResourceWatch.cs @@ -11,8 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Neuroglia.Data.Infrastructure.ResourceOriented; - namespace Synapse.Api.Client.Services; /// diff --git a/src/api/Synapse.Api.Client.Http/Services/ResourceWatchEventHubClient.cs b/src/api/Synapse.Api.Client.Http/Services/ResourceWatchEventHubClient.cs index 606836f29..ab80229f6 100644 --- a/src/api/Synapse.Api.Client.Http/Services/ResourceWatchEventHubClient.cs +++ b/src/api/Synapse.Api.Client.Http/Services/ResourceWatchEventHubClient.cs @@ -11,12 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Microsoft.AspNetCore.SignalR.Client; -using Neuroglia.Data.Infrastructure.ResourceOriented; -using Neuroglia.Eventing.CloudEvents; -using System.Reactive.Linq; -using System.Reactive.Subjects; - namespace Synapse.Api.Client.Services; /// diff --git a/src/api/Synapse.Api.Client.Http/Services/SynapseHttpApiClient.cs b/src/api/Synapse.Api.Client.Http/Services/SynapseHttpApiClient.cs index e144d88dd..fd46fcf37 100644 --- a/src/api/Synapse.Api.Client.Http/Services/SynapseHttpApiClient.cs +++ b/src/api/Synapse.Api.Client.Http/Services/SynapseHttpApiClient.cs @@ -11,12 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Neuroglia; -using Neuroglia.Data.Infrastructure.ResourceOriented; -using Neuroglia.Serialization; - namespace Synapse.Api.Client.Services; /// diff --git a/src/api/Synapse.Api.Client.Http/Usings.cs b/src/api/Synapse.Api.Client.Http/Usings.cs index f5f346b39..af6d71643 100644 --- a/src/api/Synapse.Api.Client.Http/Usings.cs +++ b/src/api/Synapse.Api.Client.Http/Usings.cs @@ -11,4 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +global using Microsoft.AspNetCore.SignalR.Client; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using Neuroglia; +global using Neuroglia.Data; +global using Neuroglia.Data.Infrastructure.ResourceOriented; +global using Neuroglia.Eventing.CloudEvents; +global using Neuroglia.Serialization; +global using Synapse.Api.Client.Http.Configuration; +global using Synapse.Api.Client.Services; global using Synapse.Resources; +global using System.Net.Mime; +global using System.Reactive.Linq; +global using System.Reactive.Subjects; +global using System.Text; diff --git a/src/cli/Synapse.Cli/CliConstants.cs b/src/cli/Synapse.Cli/CliConstants.cs new file mode 100644 index 000000000..04fac1d01 --- /dev/null +++ b/src/cli/Synapse.Cli/CliConstants.cs @@ -0,0 +1,14 @@ +namespace Synapse.Cli; + +/// +/// Exposes constants and statics used by the CLI +/// +internal static class CliConstants +{ + + /// + /// Gets the name of the CLI configuration file + /// + public const string ConfigurationFileName = "config.yaml"; + +} diff --git a/src/cli/Synapse.Cli/Commands/Command.cs b/src/cli/Synapse.Cli/Commands/Command.cs new file mode 100644 index 000000000..f78074618 --- /dev/null +++ b/src/cli/Synapse.Cli/Commands/Command.cs @@ -0,0 +1,41 @@ +namespace Synapse.Cli.Commands; + +/// +/// Represents the base class for all implementations +/// +public abstract class Command + : System.CommandLine.Command +{ + + /// + /// Initializes a new + /// + /// The current + /// The service used to create s + /// The service used to interact with the remote Synapse API + /// The 's name + /// The 's description + protected Command(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ISynapseApiClient api, string name, string description) + : base(name, description) + { + this.ServiceProvider = serviceProvider; + this.Logger = loggerFactory.CreateLogger(this.GetType()); + this.Api = api; + } + + /// + /// Gets the current + /// + protected IServiceProvider ServiceProvider { get; } + + /// + /// Gets the service used to perform logging + /// + protected ILogger Logger { get; } + + /// + /// Gets the service used to interact with the remote Synapse API + /// + protected ISynapseApiClient Api { get; } + +} diff --git a/src/cli/Synapse.Cli/Commands/Config/DeleteApiCommand.cs b/src/cli/Synapse.Cli/Commands/Config/DeleteApiCommand.cs new file mode 100644 index 000000000..1ad7b0dce --- /dev/null +++ b/src/cli/Synapse.Cli/Commands/Config/DeleteApiCommand.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Options; +using Synapse.Cli.Configuration; +using Synapse.Cli.Services; + +namespace Synapse.Cli.Commands.Config; + +/// +/// Represents the used to configure the CLI to delete the specified API configuration +/// +internal class DeleteApiCommand + : Command +{ + + /// + /// Gets the 's name + /// + public const string CommandName = "delete-api"; + /// + /// Gets the 's description + /// + public const string CommandDescription = "Deletes the API configuration with the specified name."; + + /// + public DeleteApiCommand(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ISynapseApiClient api, IOptionsManager optionsManager, IOptionsMonitor applicationOptions) + : base(serviceProvider, loggerFactory, api, CommandName, CommandDescription) + { + this.OptionsManager = optionsManager; + this.ApplicationOptions = applicationOptions; + this.Add(new Argument("name") { Description = "The name of the API configuration to delete." }); + this.Handler = CommandHandler.Create(HandleAsync); + } + + /// + /// Gets the service used to manage the application's options + /// + protected IOptionsManager OptionsManager { get; } + + /// + /// Gets the current + /// + protected IOptionsMonitor ApplicationOptions { get; } + + /// + /// Handles the + /// + /// The name of the API configuration to use + /// A new awaitable + public async Task HandleAsync(string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + if (this.ApplicationOptions.CurrentValue.Api.Current == name) throw new NotSupportedException($"Failed to delete the API configuration with name '{name}' because it is the API currently in use."); + if (!this.ApplicationOptions.CurrentValue.Api.Configurations.Remove(name)) throw new NullReferenceException($"Failed to find a configured API with name '{name}'."); + await this.OptionsManager.UpdateOptionsAsync(this.ApplicationOptions.CurrentValue); + } + +} \ No newline at end of file diff --git a/src/cli/Synapse.Cli/Commands/Config/GetApisCommand.cs b/src/cli/Synapse.Cli/Commands/Config/GetApisCommand.cs new file mode 100644 index 000000000..f8c82930c --- /dev/null +++ b/src/cli/Synapse.Cli/Commands/Config/GetApisCommand.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.Options; +using Synapse.Cli.Configuration; +using Synapse.Cli.Services; +using Synapse.Resources; + +namespace Synapse.Cli.Commands.Config; + +/// +/// Represents the used to configure the API used by the Synapse CLI +/// +internal class GetApisCommand + : Command +{ + + /// + /// Gets the 's name + /// + public const string CommandName = "get-apis"; + /// + /// Gets the 's description + /// + public const string CommandDescription = "Retrieves all configured APIs"; + + /// + public GetApisCommand(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ISynapseApiClient api, IOptionsManager optionsManager, IOptionsMonitor applicationOptions) + : base(serviceProvider, loggerFactory, api, CommandName, CommandDescription) + { + this.OptionsManager = optionsManager; + this.ApplicationOptions = applicationOptions; + this.Handler = CommandHandler.Create(HandleAsync); + } + + /// + /// Gets the service used to manage the application's options + /// + protected IOptionsManager OptionsManager { get; } + + /// + /// Gets the current + /// + protected IOptionsMonitor ApplicationOptions { get; } + + /// + /// Handles the + /// + /// A new awaitable + public async Task HandleAsync() + { + var table = new Table(); + table.Border(TableBorder.None); + table.AddColumn("CURRENT"); + table.AddColumn("NAME"); + foreach (var apiConfig in this.ApplicationOptions.CurrentValue.Api.Configurations) + { + table.AddRow + ( + this.ApplicationOptions.CurrentValue.Api.Current == apiConfig.Key || this.ApplicationOptions.CurrentValue.Api.Configurations.Count == 1 ? "*" : string.Empty, + apiConfig.Key + ); + } + AnsiConsole.Write(table); + await Task.CompletedTask; + } + + static class CommandOptions + { + + public static Option Server => new(["-s", "--server"], "The address of the API server to use"); + + } + +} \ No newline at end of file diff --git a/src/cli/Synapse.Cli/Commands/Config/SetApiCommand.cs b/src/cli/Synapse.Cli/Commands/Config/SetApiCommand.cs new file mode 100644 index 000000000..cc25f4a21 --- /dev/null +++ b/src/cli/Synapse.Cli/Commands/Config/SetApiCommand.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Options; +using Synapse.Cli.Configuration; +using Synapse.Cli.Services; + +namespace Synapse.Cli.Commands.Config; + +/// +/// Represents the used to configure the API used by the Synapse CLI +/// +internal class SetApiCommand + : Command +{ + + /// + /// Gets the 's name + /// + public const string CommandName = "set-api"; + /// + /// Gets the 's description + /// + public const string CommandDescription = "Configures the API used by the Synapse CLI"; + + /// + public SetApiCommand(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ISynapseApiClient api, IOptionsManager optionsManager, IOptionsMonitor applicationOptions) + : base(serviceProvider, loggerFactory, api, CommandName, CommandDescription) + { + this.OptionsManager = optionsManager; + this.ApplicationOptions = applicationOptions; + this.Add(new Argument("name") { Description = "The name of the API configuration to update." }); + this.Add(CommandOptions.Server); + this.Handler = CommandHandler.Create(HandleAsync); + } + + /// + /// Gets the service used to manage the application's options + /// + protected IOptionsManager OptionsManager { get; } + + /// + /// Gets the current + /// + protected IOptionsMonitor ApplicationOptions { get; } + + /// + /// Handles the + /// + /// The name of the API configuration to update + /// The uri of the API server to use + /// A new awaitable + public async Task HandleAsync(string name, Uri server) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentNullException.ThrowIfNull(server); + if (!this.ApplicationOptions.CurrentValue.Api.Configurations.TryGetValue(name, out var apiConfig) || apiConfig == null) apiConfig = new ApiConfiguration() + { + Server = server + }; + apiConfig.Server = server; + this.ApplicationOptions.CurrentValue.Api.Configurations[name] = apiConfig; + if (this.ApplicationOptions.CurrentValue.Api.Configurations.Count == 1) this.ApplicationOptions.CurrentValue.Api.Current = name; + await this.OptionsManager.UpdateOptionsAsync(this.ApplicationOptions.CurrentValue); + } + + static class CommandOptions + { + + public static Option Server => new(["-s", "--server"], "The address of the API server to use"); + + } + +} \ No newline at end of file diff --git a/src/cli/Synapse.Cli/Commands/Config/UseApiCommand.cs b/src/cli/Synapse.Cli/Commands/Config/UseApiCommand.cs new file mode 100644 index 000000000..fb0741026 --- /dev/null +++ b/src/cli/Synapse.Cli/Commands/Config/UseApiCommand.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Options; +using Synapse.Cli.Configuration; +using Synapse.Cli.Services; + +namespace Synapse.Cli.Commands.Config; + +/// +/// Represents the used to configure the CLI to use the specified API configuration +/// +internal class UseApiCommand + : Command +{ + + /// + /// Gets the 's name + /// + public const string CommandName = "use-api"; + /// + /// Gets the 's description + /// + public const string CommandDescription = "Configures the API used by the Synapse CLI"; + + /// + public UseApiCommand(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ISynapseApiClient api, IOptionsManager optionsManager, IOptionsMonitor applicationOptions) + : base(serviceProvider, loggerFactory, api, CommandName, CommandDescription) + { + this.OptionsManager = optionsManager; + this.ApplicationOptions = applicationOptions; + this.Add(new Argument("name") { Description = "The name of the API configuration to use." }); + this.Handler = CommandHandler.Create(HandleAsync); + } + + /// + /// Gets the service used to manage the application's options + /// + protected IOptionsManager OptionsManager { get; } + + /// + /// Gets the current + /// + protected IOptionsMonitor ApplicationOptions { get; } + + /// + /// Handles the + /// + /// The name of the API configuration to use + /// A new awaitable + public async Task HandleAsync(string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + if (!this.ApplicationOptions.CurrentValue.Api.Configurations.TryGetValue(name, out var apiConfig) || apiConfig == null) throw new NullReferenceException($"Failed to find a configured API with name '{name}'."); + this.ApplicationOptions.CurrentValue.Api.Current = name; + await this.OptionsManager.UpdateOptionsAsync(this.ApplicationOptions.CurrentValue); + } + +} \ No newline at end of file diff --git a/src/cli/Synapse.Cli/Commands/ConfigCommand.cs b/src/cli/Synapse.Cli/Commands/ConfigCommand.cs new file mode 100644 index 000000000..67472d411 --- /dev/null +++ b/src/cli/Synapse.Cli/Commands/ConfigCommand.cs @@ -0,0 +1,31 @@ +using Synapse.Cli.Commands.Config; + +namespace Synapse.Cli.Commands; + +/// +/// Represents the used to configure the Synapse CLI +/// +public class ConfigCommand + : Command +{ + + /// + /// Gets the 's name + /// + public const string CommandName = "config"; + /// + /// Gets the 's description + /// + public const string CommandDescription = "Configures the Synapse CLI"; + + /// + public ConfigCommand(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ISynapseApiClient api) + : base(serviceProvider, loggerFactory, api, CommandName, CommandDescription) + { + this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + } + +} diff --git a/src/cli/Synapse.Cli/Commands/WorkflowCommand.cs b/src/cli/Synapse.Cli/Commands/WorkflowCommand.cs new file mode 100644 index 000000000..ad9a538fb --- /dev/null +++ b/src/cli/Synapse.Cli/Commands/WorkflowCommand.cs @@ -0,0 +1,34 @@ +using Synapse.Cli.Commands.Workflows; + +namespace Synapse.Cli.Commands; + +/// +/// Represents the used to manage s +/// +public class WorkflowCommand + : Command +{ + + /// + /// Gets the 's name + /// + public const string CommandName = "workflow"; + /// + /// Gets the 's description + /// + public const string CommandDescription = "Manages workflows"; + + /// + public WorkflowCommand(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ISynapseApiClient api) + : base(serviceProvider, loggerFactory, api, CommandName, CommandDescription) + { + this.AddAlias("workflows"); + this.AddAlias("wf"); + this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + //this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + //this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + //this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + } + +} diff --git a/src/cli/Synapse.Cli/Commands/WorkflowInstanceCommand.cs b/src/cli/Synapse.Cli/Commands/WorkflowInstanceCommand.cs new file mode 100644 index 000000000..012714118 --- /dev/null +++ b/src/cli/Synapse.Cli/Commands/WorkflowInstanceCommand.cs @@ -0,0 +1,35 @@ +namespace Synapse.Cli.Commands; + +/// +/// Represents the used to manage s +/// +public class WorkflowInstanceCommand + : Command +{ + + /// + /// Gets the 's name + /// + public const string CommandName = "workflow-instance"; + /// + /// Gets the 's description + /// + public const string CommandDescription = "Manages workflow instances"; + + /// + public WorkflowInstanceCommand(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ISynapseApiClient api) + : base(serviceProvider, loggerFactory, api, CommandName, CommandDescription) + { + this.AddAlias("workflow-instances"); + this.AddAlias("instance"); + this.AddAlias("instances"); + this.AddAlias("wfi"); + //this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + //this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + //this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + //this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + //this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + //this.AddCommand(ActivatorUtilities.CreateInstance(this.ServiceProvider)); + } + +} \ No newline at end of file diff --git a/src/cli/Synapse.Cli/Commands/Workflows/CreateWorkflowCommand.cs b/src/cli/Synapse.Cli/Commands/Workflows/CreateWorkflowCommand.cs new file mode 100644 index 000000000..d28908e51 --- /dev/null +++ b/src/cli/Synapse.Cli/Commands/Workflows/CreateWorkflowCommand.cs @@ -0,0 +1,94 @@ +using Neuroglia.Data; +using Neuroglia.Data.Infrastructure.ResourceOriented; +using ServerlessWorkflow.Sdk.Models; + +namespace Synapse.Cli.Commands.Workflows; + +/// +/// Represents the used to create a new +/// +internal class CreateWorkflowCommand + : Command +{ + + /// + /// Gets the 's name + /// + public const string CommandName = "create"; + /// + /// Gets the 's description + /// + public const string CommandDescription = "Creates a new workflow."; + + /// + public CreateWorkflowCommand(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ISynapseApiClient api, IWorkflowDefinitionReader workflowDefinitionReader) + : base(serviceProvider, loggerFactory, api, CommandName, CommandDescription) + { + this.WorkflowDefinitionReader = workflowDefinitionReader; + this.Add(CommandOptions.File); + this.Handler = CommandHandler.Create(this.HandleAsync); + } + + /// + /// Gets the service used to read s + /// + protected IWorkflowDefinitionReader WorkflowDefinitionReader { get; } + + /// + /// Handles the + /// + /// The path to the file that contains the definition of the workflow to create + /// A new awaitable + public async Task HandleAsync(string file) + { + Stream? stream; + if (!string.IsNullOrWhiteSpace(file)) stream = File.OpenRead(file); + else throw new InvalidOperationException("You must specify exactly one of the following options: --file"); + var workflowDefinition = await this.WorkflowDefinitionReader.ReadAsync(stream); + var workflow = await this.Api.Workflows.GetAsync(workflowDefinition.Document.Name, workflowDefinition.Document.Namespace); + if (workflow == null) + { + workflow = new() + { + Metadata = new() + { + Namespace = "default", + Name = "test" + }, + Spec = new() + { + Versions = [workflowDefinition] + } + }; + workflow = await this.Api.Workflows.CreateAsync(workflow); + } + else + { + var originalWorkflow = workflow.Clone()!; + workflow.Spec.Versions.Add(workflowDefinition); + var patch = JsonPatchUtility.CreateJsonPatchFromDiff(originalWorkflow, workflow); + workflow = await this.Api.Workflows.PatchAsync(workflowDefinition.Document.Name, workflowDefinition.Document.Namespace, new(PatchType.JsonPatch, patch)); + } + Console.WriteLine($"The workflow '{workflow.GetQualifiedName()}:{workflowDefinition.Document.Version}' has been successfully created"); + await stream.DisposeAsync(); + } + + static class CommandOptions + { + + public static Option File + { + get + { + var option = new Option("--file") + { + Description = "The file that contains the definition of the workflow to create." + }; + option.AddAlias("-f"); + return option; + } + } + + } + +} diff --git a/src/cli/Synapse.Cli/Commands/Workflows/DeleteWorkflowCommand.cs b/src/cli/Synapse.Cli/Commands/Workflows/DeleteWorkflowCommand.cs new file mode 100644 index 000000000..abede18f3 --- /dev/null +++ b/src/cli/Synapse.Cli/Commands/Workflows/DeleteWorkflowCommand.cs @@ -0,0 +1,70 @@ +namespace Synapse.Cli.Commands.Workflows; + +/// +/// Represents the used to delete a single +/// +internal class DeleteWorkflowCommand + : Command +{ + + /// + /// Gets the 's name + /// + public const string CommandName = "delete"; + /// + /// Gets the 's description + /// + public const string CommandDescription = "Deletes the specified workflow"; + + /// + public DeleteWorkflowCommand(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, ISynapseApiClient api) + : base(serviceProvider, loggerFactory, api, CommandName, CommandDescription) + { + this.AddAlias("del"); + this.Add(new Argument("name") { Description = "The name of the workflow to delete" }); + this.Add(CommandOptions.Namespace); + this.Add(CommandOptions.Version); + this.Add(CommandOptions.Confirm); + this.Handler = CommandHandler.Create(this.HandleAsync); + } + + /// + /// Handles the + /// + /// The namespace of the workflow to delete + /// The name of the workflow to delete + /// The version of the workflow to delete + /// A boolean indicating whether or not to ask for the user's confirmation + /// A new awaitable + public async Task HandleAsync(string name, string @namespace, string version, bool y) + { + var components = name.Split(':', StringSplitOptions.RemoveEmptyEntries); + if (!y) + { + if (components.Length == 2) Console.Write($"Are you sure you wish to delete the workflow '{name}.{@namespace}'? Press 'y' to confirm, or any other key to cancel: "); + else Console.Write($"Are you sure you wish to delete all version of the workflow '{name}.{@namespace}'? Press 'y' to confirm, or any other key to cancel: "); + var inputKey = Console.ReadKey(); + Console.WriteLine(); + if (inputKey.Key != ConsoleKey.Y) + { + Console.WriteLine("Deletion cancelled"); + return; + } + } + await this.Api.Workflows.DeleteAsync(name, @namespace); + if (components.Length == 2) Console.WriteLine($"The workflow '{name}.{@namespace}' has been successfully deleted"); + else Console.WriteLine($"All version of the workflow '{name}.{@namespace}' have been successfully deleted"); + } + + static class CommandOptions + { + + public static Option Namespace => new([ "-n", "--namespace" ], () => "default", "The namespace the workflow to delete belongs to"); + + public static Option Version => new(["-v", "--version"], () => string.Empty, "The version of the workflow to delete. Note that failing to specify the version will delete all version of the specified workflow"); + + public static Option Confirm => new(["-y", "--yes"], () => false, "Delete the workflow(s) without prompting confirmation"); + + } + +} diff --git a/src/cli/Synapse.Cli/Configuration/ApiConfiguration.cs b/src/cli/Synapse.Cli/Configuration/ApiConfiguration.cs new file mode 100644 index 000000000..9db500a46 --- /dev/null +++ b/src/cli/Synapse.Cli/Configuration/ApiConfiguration.cs @@ -0,0 +1,16 @@ +namespace Synapse.Cli.Configuration; + +/// +/// Represents the named options used to configure a Synapse API to connect to using the Synapse CLI +/// +[DataContract] +public class ApiConfiguration +{ + + /// + /// Gets/sets the uri that references the API server to connect to + /// + [DataMember(Name = "server", Order = 1), JsonPropertyOrder(1), JsonPropertyName("server"), YamlMember(Alias = "server", Order = 1)] + public required virtual Uri Server { get; set; } + +} \ No newline at end of file diff --git a/src/cli/Synapse.Cli/Configuration/ApplicationApiOptions.cs b/src/cli/Synapse.Cli/Configuration/ApplicationApiOptions.cs new file mode 100644 index 000000000..c310e557a --- /dev/null +++ b/src/cli/Synapse.Cli/Configuration/ApplicationApiOptions.cs @@ -0,0 +1,23 @@ +namespace Synapse.Cli.Configuration; + +/// +/// Represents the object used to configure the Synapse API used by the CLI +/// +[DataContract] +public record ApplicationApiOptions +{ + + /// + /// Gets/sets a name/value mapping of all configured APIs + /// + [Required, MinLength(1)] + [DataMember(Name = "configurations", Order = 1), JsonPropertyOrder(1), JsonPropertyName("configurations"), YamlMember(Alias = "configurations", Order = 1)] + public virtual IDictionary Configurations { get; set; } = new Dictionary(); + + /// + /// Gets/sets the name of the API currently used by the Synapse CLI + /// + [DataMember(Name = "current", Order = 2), JsonPropertyOrder(2), JsonPropertyName("current"), YamlMember(Alias = "current", Order = 2)] + public string? Current { get; set; } + +} diff --git a/src/cli/Synapse.Cli/Configuration/ApplicationOptions.cs b/src/cli/Synapse.Cli/Configuration/ApplicationOptions.cs new file mode 100644 index 000000000..3a83b5a32 --- /dev/null +++ b/src/cli/Synapse.Cli/Configuration/ApplicationOptions.cs @@ -0,0 +1,16 @@ +namespace Synapse.Cli.Configuration; + +/// +/// Represents the options used to configure the Synapse CLI application +/// +[DataContract] +public record ApplicationOptions +{ + + /// + /// Gets/sets the name of the API currently used by the Synapse CLI + /// + [DataMember(Name = "api", Order = 1), JsonPropertyOrder(1), JsonPropertyName("api"), YamlMember(Alias = "api", Order = 1)] + public ApplicationApiOptions Api { get; set; } = new(); + +} diff --git a/src/cli/Synapse.Cli/Extensions/IServiceCollectionExtensions.cs b/src/cli/Synapse.Cli/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 000000000..8358055e4 --- /dev/null +++ b/src/cli/Synapse.Cli/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using Neuroglia; + +namespace Synapse.Cli; + +/// +/// Defines extensions for s +/// +public static class IServiceCollectionExtensions +{ + + /// + /// Adds and configures all s loaded from the specified assemblies + /// + /// The to configure + /// The configured + public static IServiceCollection AddCliCommands(this IServiceCollection services) + { + foreach (var commandType in TypeCacheUtil.FindFilteredTypes + ( + "synctl-cmd", + t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType && typeof(Command).IsAssignableFrom(t) && t.IsPublic, + typeof(Commands.Command).Assembly) + ) + { + services.AddSingleton(typeof(Command), commandType); + } + return services; + } + +} + diff --git a/src/cli/Synapse.Cli/Program.cs b/src/cli/Synapse.Cli/Program.cs new file mode 100644 index 000000000..60e48a66d --- /dev/null +++ b/src/cli/Synapse.Cli/Program.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Configuration; +using Synapse.Cli.Configuration; +using Synapse.Cli.Services; + +var parser = BuildCommandLineParser(); +await parser.InvokeAsync(args); + +static Parser BuildCommandLineParser() +{ + var configuration = new ConfigurationBuilder() + .AddYamlFile(CliConstants.ConfigurationFileName, true, true) + .Build(); + var services = new ServiceCollection(); + ConfigureServices(services, configuration); + using var serviceProvider = services.BuildServiceProvider(); + var rootCommand = new RootCommand(); + foreach (var command in serviceProvider.GetServices()) rootCommand.AddCommand(command); + return new CommandLineBuilder(rootCommand) + .UseDefaults() + .UseExceptionHandler((ex, context) => + { + AnsiConsole.MarkupLine($"[red]{ex.Message}[/]"); + var inner = ex.InnerException; + while (inner != null) + { + AnsiConsole.MarkupLine($"[red]{inner.Message}[/]"); + inner = inner.InnerException; + } + }) + .Build(); +} + +static void ConfigureServices(IServiceCollection services, IConfiguration configuration) +{ + services.AddSingleton(configuration); + services.Configure(configuration); + services.AddLogging(); + services.AddServerlessWorkflowIO(); + services.AddSynapseHttpApiClient(http => http.BaseAddress = new Uri("http://localhost:42286")); //todo: config based + services.AddCliCommands(); + services.AddSingleton(); +} \ No newline at end of file diff --git a/src/cli/Synapse.Cli/Properties/launchSettings.json b/src/cli/Synapse.Cli/Properties/launchSettings.json new file mode 100644 index 000000000..53cc31cb9 --- /dev/null +++ b/src/cli/Synapse.Cli/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Synapse.Cli": { + "commandName": "Project", + "commandLineArgs": "workflow create --file test.yaml" + } + } +} \ No newline at end of file diff --git a/src/cli/Synapse.Cli/Services/IOptionsManager.cs b/src/cli/Synapse.Cli/Services/IOptionsManager.cs new file mode 100644 index 000000000..3e1289e97 --- /dev/null +++ b/src/cli/Synapse.Cli/Services/IOptionsManager.cs @@ -0,0 +1,41 @@ +using Synapse.Cli.Configuration; + +namespace Synapse.Cli.Services; + +/// +/// Defines the service used to manage the application's options +/// +public interface IOptionsManager +{ + + /// + /// Updates the application's options + /// + /// The updated to persist + /// A + /// A new awaitable + Task UpdateOptionsAsync(ApplicationOptions options, CancellationToken cancellationToken = default); + +} + +/// +/// Represents the default implementation of the interface +/// +/// The service used to serialize/deserialize data to/from YAML +public class OptionsManager(IYamlSerializer serializer) + : IOptionsManager +{ + + /// + /// Gets the service used to serialize/deserialize data to/from YAML + /// + protected IYamlSerializer Serializer { get; } = serializer; + + /// + public virtual async Task UpdateOptionsAsync(ApplicationOptions options, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(options); + var yaml = this.Serializer.SerializeToText(options); + await File.WriteAllTextAsync(CliConstants.ConfigurationFileName, yaml, cancellationToken); + } +} diff --git a/src/cli/Synapse.Cli/Synapse.Cli.csproj b/src/cli/Synapse.Cli/Synapse.Cli.csproj new file mode 100644 index 000000000..96acb45d2 --- /dev/null +++ b/src/cli/Synapse.Cli/Synapse.Cli.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + Exe + en + True + synctl + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/cli/Synapse.Cli/Usings.cs b/src/cli/Synapse.Cli/Usings.cs new file mode 100644 index 000000000..91ee5c7d4 --- /dev/null +++ b/src/cli/Synapse.Cli/Usings.cs @@ -0,0 +1,17 @@ +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using Neuroglia.Serialization; +global using ServerlessWorkflow.Sdk.IO; +global using Spectre.Console; +global using Synapse.Api.Client; +global using Synapse.Api.Client.Services; +global using Synapse.Cli; +global using Synapse.Resources; +global using System.CommandLine; +global using System.CommandLine.Builder; +global using System.CommandLine.NamingConventionBinder; +global using System.CommandLine.Parsing; +global using System.ComponentModel.DataAnnotations; +global using System.Runtime.Serialization; +global using System.Text.Json.Serialization; +global using YamlDotNet.Serialization; \ No newline at end of file diff --git a/src/cli/Synapse.Cli/config.yaml b/src/cli/Synapse.Cli/config.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/src/runner/Synapse.Runner/Services/RunnerApplication.cs b/src/runner/Synapse.Runner/Services/RunnerApplication.cs index fcad8c33d..a6ed0ca2a 100644 --- a/src/runner/Synapse.Runner/Services/RunnerApplication.cs +++ b/src/runner/Synapse.Runner/Services/RunnerApplication.cs @@ -21,8 +21,9 @@ namespace Synapse.Runner.Services; /// Represents a service used to initialize the current /// /// The current +/// The current /// The service used to access the current -internal class RunnerApplication(IServiceProvider serviceProvider, IOptions options) +internal class RunnerApplication(IServiceProvider serviceProvider, IHostApplicationLifetime applicationLifetime, IOptions options) : IHostedService, IDisposable { @@ -38,6 +39,11 @@ internal class RunnerApplication(IServiceProvider serviceProvider, IOptions protected IServiceProvider ServiceProvider => this.ServiceScope.ServiceProvider; + /// + /// Gets the current + /// + protected IHostApplicationLifetime ApplicationLifetime { get; } = applicationLifetime; + /// /// Gets the service used to interact with the Synapse API /// @@ -66,7 +72,7 @@ public virtual async Task StartAsync(CancellationToken cancellationToken) var context = ActivatorUtilities.CreateInstance(this.ServiceProvider, expressionEvaluator, definition, instance); this.Executor = ActivatorUtilities.CreateInstance(this.ServiceProvider, context); await this.Executor.ExecuteAsync(cancellationToken).ConfigureAwait(false); - + this.ApplicationLifetime.StopApplication(); } ///