diff --git a/.gitignore b/.gitignore
index 1fc441d74..6a4d7fa53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,9 @@
# Created by https://www.toptal.com/developers/gitignore/api/aspnetcore,dotnetcore,visualstudio,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=aspnetcore,dotnetcore,visualstudio,visualstudiocode
+# MIG
+/cli
+
# Database
*.db
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 42ddaf7d6..c5dcb8039 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -7,7 +7,7 @@
"request": "launch",
"preLaunchTask": "build-cli",
"program": "${workspaceFolder}/src/CLI/bin/Debug/net5.0/linux-x64/mig-cli",
- "args": ["aet", "add", "-a", "TEST", "--apps", "1,2,3"],
+ "args": ["config", "endpoint", "http://localhost:4500"],
"cwd": "${workspaceFolder}/src/CLI/bin/Debug/net5.0/linux-x64",
"stopAtEntry": true,
"console": "internalConsole"
diff --git a/Dockerfile b/Dockerfile
index c10f1d02d..b83099d20 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -26,7 +26,7 @@ RUN echo "Building MONAI Deploy Informatics Gateway $Version ($FileVersion)..."
RUN dotnet publish -c Release -o out --nologo /p:Version=$Version /p:FileVersion=$FileVersion src/InformaticsGateway/Monai.Deploy.InformaticsGateway.csproj
# Build runtime image
-FROM mcr.microsoft.com/dotnet/runtime:5.0-focal
+FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal
ENV DEBIAN_FRONTEND=noninteractive
diff --git a/src/Api/FileStorageInfo.cs b/src/Api/FileStorageInfo.cs
index 646a174c6..148d77c69 100644
--- a/src/Api/FileStorageInfo.cs
+++ b/src/Api/FileStorageInfo.cs
@@ -10,6 +10,7 @@
// limitations under the License.
using Ardalis.GuardClauses;
+using System;
using System.IO.Abstractions;
namespace Monai.Deploy.InformaticsGateway.Api
@@ -21,6 +22,11 @@ public record FileStorageInfo
{
private readonly IFileSystem _fileSystem;
+ ///
+ /// Gets the unique ID of the file.
+ ///
+ public Guid Id { get; init; }
+
///
/// Gets the correlation ID of the file.
/// For SCP received DICOM instances: use internally generated unique association ID.
@@ -44,7 +50,19 @@ public record FileStorageInfo
public string[] Applications { get; private set; }
///
- /// Gets or set the number of attempts to upload.
+ /// Gets or sets the DateTime that the file was received.
+ ///
+ ///
+ public DateTime Received { get; set; }
+
+ ///
+ /// Gets or set database row versioning info.
+ ///
+ ///
+ public byte[] Timestamp { get; set; }
+
+ ///
+ /// Gets or sets the number of attempts to upload.
///
public int TryCount { get; set; } = 0;
@@ -67,8 +85,10 @@ public FileStorageInfo(string correlationId, string storageRootPath, string mess
}
_fileSystem = fileSystem;
+ Id = Guid.NewGuid();
CorrelationId = correlationId;
StorageRootPath = storageRootPath;
+ Received = DateTime.UtcNow;
FilePath = GenerateStoragePath(storageRootPath, correlationId, messageId, fileExtension);
}
diff --git a/src/CLI/AssemblyInfo.cs b/src/CLI/AssemblyInfo.cs
new file mode 100644
index 000000000..a33bb495b
--- /dev/null
+++ b/src/CLI/AssemblyInfo.cs
@@ -0,0 +1,14 @@
+// Copyright 2021 MONAI Consortium
+// 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.CompilerServices;
+
+[assembly: InternalsVisibleTo("Monai.Deploy.InformaticsGateway.CLI.Test")]
diff --git a/src/CLI/Commands/AetCommand.cs b/src/CLI/Commands/AetCommand.cs
index 63466b2db..97592bffe 100644
--- a/src/CLI/Commands/AetCommand.cs
+++ b/src/CLI/Commands/AetCommand.cs
@@ -83,6 +83,8 @@ private void SetupAddAetCommand()
private async Task ListAeTitlehandlerAsync(IHost host, bool verbose, CancellationToken cancellationToken)
{
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var console = host.Services.GetRequiredService();
@@ -100,8 +102,9 @@ private async Task ListAeTitlehandlerAsync(IHost host, bool verbose, Cancel
IReadOnlyList items = null;
try
{
- ConfigurationOptions config = LoadConfiguration(verbose, configService, client);
- this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {config.Endpoint}...");
+ CheckConfiguration(configService);
+ client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
+ this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
this.LogVerbose(verbose, host, $"Retrieving MONAI SCP AE Titles...");
items = await client.MonaiScpAeTitle.List(cancellationToken);
}
@@ -142,6 +145,9 @@ private async Task ListAeTitlehandlerAsync(IHost host, bool verbose, Cancel
private async Task RemoveAeTitlehandlerAsync(string name, IHost host, bool verbose, CancellationToken cancellationToken)
{
+ Guard.Against.NullOrWhiteSpace(name, nameof(name));
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var configService = host.Services.GetRequiredService();
var client = host.Services.GetRequiredService();
@@ -153,8 +159,9 @@ private async Task RemoveAeTitlehandlerAsync(string name, IHost host, bool
try
{
- ConfigurationOptions config = LoadConfiguration(verbose, configService, client);
- this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {config.Endpoint}...");
+ CheckConfiguration(configService);
+ client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
+ this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
this.LogVerbose(verbose, host, $"Deleting MONAI SCP AE Title {name}...");
_ = await client.MonaiScpAeTitle.Delete(name, cancellationToken);
logger.Log(LogLevel.Information, $"MONAI SCP AE Title '{name}' deleted.");
@@ -174,6 +181,9 @@ private async Task RemoveAeTitlehandlerAsync(string name, IHost host, bool
private async Task AddAeTitlehandlerAsync(MonaiApplicationEntity entity, IHost host, bool verbose, CancellationToken cancellationToken)
{
+ Guard.Against.Null(entity, nameof(entity));
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var configService = host.Services.GetRequiredService();
var client = host.Services.GetRequiredService();
@@ -185,9 +195,10 @@ private async Task AddAeTitlehandlerAsync(MonaiApplicationEntity entity, IH
try
{
- ConfigurationOptions config = LoadConfiguration(verbose, configService, client);
+ CheckConfiguration(configService);
+ client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
- this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {config.Endpoint}...");
+ this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
var result = await client.MonaiScpAeTitle.Create(entity, cancellationToken);
logger.Log(LogLevel.Information, "New MONAI Deploy SCP Application Entity created:");
diff --git a/src/CLI/Commands/CommandBase.cs b/src/CLI/Commands/CommandBase.cs
index 2b45616c7..68955d844 100644
--- a/src/CLI/Commands/CommandBase.cs
+++ b/src/CLI/Commands/CommandBase.cs
@@ -14,7 +14,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using Monai.Deploy.InformaticsGateway.Client;
using System;
using System.CommandLine;
@@ -28,12 +27,17 @@ public CommandBase(string name, string description) : base(name, description)
protected ILogger CreateLogger(IHost host)
{
+ Guard.Against.Null(host, nameof(host));
+
var loggerFactory = host.Services.GetService();
return loggerFactory?.CreateLogger();
}
protected void LogVerbose(bool verbose, IHost host, string message)
{
+ Guard.Against.Null(host, nameof(host));
+ Guard.Against.NullOrWhiteSpace(message, nameof(message));
+
if (verbose)
{
var logger = CreateLogger(host);
@@ -48,33 +52,24 @@ protected void LogVerbose(bool verbose, IHost host, string message)
}
}
- protected ConfigurationOptions LoadConfiguration(bool verbose, IConfigurationService configurationService, IInformaticsGatewayClient client)
+ protected void AddConfirmationOption() => AddConfirmationOption(this);
+
+ protected void AddConfirmationOption(Command command)
{
- Guard.Against.Null(configurationService, nameof(configurationService));
- Guard.Against.Null(client, nameof(client));
+ Guard.Against.Null(command, nameof(command));
- var configuration = LoadConfiguration(verbose, configurationService);
- client.ConfigureServiceUris(new Uri(configuration.Endpoint));
- return configuration;
+ var confirmationOption = new Option(new[] { "-y", "--yes" }, "Automatic yes to prompts");
+ command.AddOption(confirmationOption);
}
- protected ConfigurationOptions LoadConfiguration(bool verbose, IConfigurationService configurationService)
+ protected void CheckConfiguration(IConfigurationService configService)
{
- Guard.Against.Null(configurationService, nameof(configurationService));
-
- if (configurationService.ConfigurationExists())
+ Guard.Against.Null(configService, nameof(configService));
+
+ if (!configService.IsInitialized)
{
- var config = configurationService.Load(verbose);
- return config;
+ throw new ConfigurationException($"Please execute `{AppDomain.CurrentDomain.FriendlyName} config init` to intialize Informatics Gateway.");
}
-
- throw new ConfigurationException($"{Strings.ApplicationName} endpoint not configured. Please run 'config` first.");
- }
-
- protected void AddConfirmationOption()
- {
- var confirmationOption = new Option(new[] { "-y", "--yes" }, "Automatic yes to prompts");
- this.AddOption(confirmationOption);
}
}
}
diff --git a/src/CLI/Commands/ConfigCommand.cs b/src/CLI/Commands/ConfigCommand.cs
index 2014f0563..f2f8a4f8a 100644
--- a/src/CLI/Commands/ConfigCommand.cs
+++ b/src/CLI/Commands/ConfigCommand.cs
@@ -9,13 +9,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using Ardalis.GuardClauses;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Monai.Deploy.InformaticsGateway.CLI.Services;
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Threading;
+using System.Threading.Tasks;
namespace Monai.Deploy.InformaticsGateway.CLI
{
@@ -23,58 +26,140 @@ public class ConfigCommand : CommandBase
{
public ConfigCommand() : base("config", "Configure the CLI endpoint")
{
- var endpointOption = new Option(new[] { "-e", "--endpoint" }, $"URL to the {Strings.ApplicationName} API. E.g. http://localhost:5000") { IsRequired = true };
- this.AddOption(endpointOption);
+ AddCommandEndpoint();
+ AddCommandRunner();
+ AddCommandWorkloadManagerRest();
+ AddCommandWorkloadManagerGrpc();
- this.Handler = CommandHandler.Create(ConfigCommandHandler);
+ SetupInitCommand();
+ SetupShowConfigCommand();
+ }
+
+ private void AddCommandWorkloadManagerGrpc()
+ {
+ var wmgrpcCommand = new Command("wmgrpc", $"RESTful endpoint for the {Strings.WorkloadManagerName}.");
+ this.Add(wmgrpcCommand);
+
+ wmgrpcCommand.AddArgument(new Argument("uri"));
+ wmgrpcCommand.Handler = CommandHandler.Create((string uri, IHost host, bool verbose) =>
+ ConfigUpdateHandler(uri, host, verbose, (IConfigurationService options) =>
+ {
+ options.Configurations.WorkloadManagerGrpcEndpoint = uri;
+ })
+ );
+ }
+
+ private void AddCommandWorkloadManagerRest()
+ {
+ var wmRestCommand = new Command("wmrest", $"RESTful endpoint for the {Strings.WorkloadManagerName}.");
+ this.Add(wmRestCommand);
+
+ wmRestCommand.AddArgument(new Argument("uri"));
+ wmRestCommand.Handler = CommandHandler.Create((string uri, IHost host, bool verbose) =>
+ ConfigUpdateHandler(uri, host, verbose, (IConfigurationService options) =>
+ {
+ options.Configurations.WorkloadManagerRestEndpoint = uri;
+ })
+ );
+ }
+
+ private void AddCommandRunner()
+ {
+ var endpointCommand = new Command("runner", $"Default container runner/orchestration engine to run {Strings.ApplicationName}.");
+ this.Add(endpointCommand);
- SetupShowConfigCmmand();
+ endpointCommand.AddArgument(new Argument("runner"));
+ endpointCommand.Handler = CommandHandler.Create((Runner runner, IHost host, bool verbose) =>
+ ConfigUpdateHandler(runner, host, verbose, (IConfigurationService options) =>
+ {
+ options.Configurations.Runner = runner;
+ })
+ );
}
- private void SetupShowConfigCmmand()
+ private void AddCommandEndpoint()
+ {
+ var endpointCommand = new Command("endpoint", $"URL to the {Strings.ApplicationName} API. E.g. http://localhost:5000");
+ this.Add(endpointCommand);
+
+ endpointCommand.AddArgument(new Argument("uri"));
+ endpointCommand.Handler = CommandHandler.Create((string uri, IHost host, bool verbose) =>
+ ConfigUpdateHandler(uri, host, verbose, (IConfigurationService options) =>
+ {
+ options.Configurations.InformaticsGatewayServerEndpoint = uri;
+ })
+ );
+ }
+
+ private void SetupInitCommand()
+ {
+ var listCommand = new Command("init", $"Initialize with default configuration options");
+ this.AddCommand(listCommand);
+
+ listCommand.Handler = CommandHandler.Create(InitHandlerAsync);
+ this.AddConfirmationOption(listCommand);
+ }
+
+ private void SetupShowConfigCommand()
{
var showCommand = new Command("show", "Show configurations");
this.AddCommand(showCommand);
- showCommand.Handler = CommandHandler.Create(ShowConfiguratonHandler);
+ showCommand.Handler = CommandHandler.Create(ShowConfigurationHandler);
}
- private int ShowConfiguratonHandler(IHost host, bool verbose, CancellationToken cancellationToken)
+ private int ShowConfigurationHandler(IHost host, bool verbose, CancellationToken cancellationToken)
{
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var logger = CreateLogger(host);
var configService = host.Services.GetRequiredService();
+ Guard.Against.Null(configService, nameof(configService), "Configuration service is unavailable.");
try
{
- var service = host.Services.GetRequiredService();
- ConfigurationOptions config = LoadConfiguration(verbose, configService);
-
- logger.Log(LogLevel.Information, $"Endpoint: {config.Endpoint}");
+ CheckConfiguration(configService);
+ logger.Log(LogLevel.Information, $"Informatics Gateway API: {configService.Configurations.InformaticsGatewayServerEndpoint}");
+ logger.Log(LogLevel.Information, $"DICOM SCP Listening Port: {configService.Configurations.DicomListeningPort}");
+ logger.Log(LogLevel.Information, $"Container Runner: {configService.Configurations.Runner}");
+ logger.Log(LogLevel.Information, $"Host:");
+ logger.Log(LogLevel.Information, $" Database storage mount: {configService.Configurations.HostDatabaseStorageMount}");
+ logger.Log(LogLevel.Information, $" Data storage mount: {configService.Configurations.HostDataStorageMount}");
+ logger.Log(LogLevel.Information, $" Logs storage mount: {configService.Configurations.HostLogsStorageMount}");
+ logger.Log(LogLevel.Information, $"Workload Manager:");
+ logger.Log(LogLevel.Information, $" REST API: {configService.Configurations.WorkloadManagerRestEndpoint}");
+ logger.Log(LogLevel.Information, $" gRPC API: {configService.Configurations.WorkloadManagerGrpcEndpoint}");
+ }
+ catch (ConfigurationException)
+ {
+ return ExitCodes.Config_NotConfigured;
}
catch (Exception ex)
{
logger.Log(LogLevel.Error, ex.Message);
- return ExitCodes.Config_NotConfigured;
+ return ExitCodes.Config_ErrorShowing;
}
return ExitCodes.Success;
}
- private int ConfigCommandHandler(ConfigurationOptions options, IHost host, bool verbose)
+ private int ConfigUpdateHandler(T argument, IHost host, bool verbose, Action updater)
{
+ Guard.Against.Null(host, nameof(host));
+ Guard.Against.Null(updater, nameof(updater));
+
var logger = CreateLogger(host);
+ var config = host.Services.GetRequiredService();
+
+ Guard.Against.Null(config, nameof(config), "Configuration service is unavailable.");
try
{
- options.Validate();
- var service = host.Services.GetRequiredService();
- service.CreateConfigDirectoryIfNotExist();
-
- var configuration = service.Load(verbose);
- configuration.Endpoint = options.Endpoint;
- service.Save(configuration);
+ CheckConfiguration(config);
+ updater(config);
+ logger.Log(LogLevel.Information, "Configuration updated successfully.");
}
- catch (ArgumentNullException)
+ catch (ConfigurationException)
{
return ExitCodes.Config_NotConfigured;
}
@@ -85,5 +170,36 @@ private int ConfigCommandHandler(ConfigurationOptions options, IHost host, bool
}
return ExitCodes.Success;
}
+
+ private async Task InitHandlerAsync(IHost host, bool verbose, bool yes, CancellationToken cancellationToken)
+ {
+ Guard.Against.Null(host, nameof(host));
+
+ var logger = CreateLogger(host);
+ var configService = host.Services.GetRequiredService();
+ var confirmation = host.Services.GetRequiredService();
+ Guard.Against.Null(configService, nameof(configService), "Configuration service is unavailable.");
+ Guard.Against.Null(confirmation, nameof(confirmation), "Confirmation prompt is unavailable.");
+
+ if (!yes)
+ {
+ if (configService.IsConfigExists && !confirmation.ShowConfirmationPrompt($"Existing application configuration file already exists. Do you want to overwrite it?"))
+ {
+ logger.Log(LogLevel.Warning, "Action cancelled.");
+ return ExitCodes.Stop_Cancelled;
+ }
+ }
+
+ try
+ {
+ await configService.Initialize(cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ logger.Log(LogLevel.Error, ex.Message);
+ return ExitCodes.Config_ErrorInitializing;
+ }
+ return ExitCodes.Success;
+ }
}
}
diff --git a/src/CLI/Commands/ConfigurationException.cs b/src/CLI/Commands/ConfigurationException.cs
index 0a41d1ae5..557f9e490 100644
--- a/src/CLI/Commands/ConfigurationException.cs
+++ b/src/CLI/Commands/ConfigurationException.cs
@@ -14,7 +14,7 @@
namespace Monai.Deploy.InformaticsGateway.CLI
{
[Serializable]
- internal class ConfigurationException : Exception
+ public class ConfigurationException : Exception
{
public ConfigurationException(string message) : base(message)
{
diff --git a/src/CLI/Commands/DestinationCommand.cs b/src/CLI/Commands/DestinationCommand.cs
index dc9811393..476f9bc5c 100644
--- a/src/CLI/Commands/DestinationCommand.cs
+++ b/src/CLI/Commands/DestinationCommand.cs
@@ -81,6 +81,9 @@ private void SetupAddDestinationCommand()
private async Task ListDestinationHandlerAsync(DestinationApplicationEntity entity, IHost host, bool verbose, CancellationToken cancellationToken)
{
+ Guard.Against.Null(entity, nameof(entity));
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var console = host.Services.GetRequiredService();
@@ -98,11 +101,17 @@ private async Task ListDestinationHandlerAsync(DestinationApplicationEntity
IReadOnlyList items = null;
try
{
- ConfigurationOptions config = LoadConfiguration(verbose, configService, client);
- this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {config.Endpoint}...");
+ CheckConfiguration(configService);
+ client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
+ this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
this.LogVerbose(verbose, host, $"Retrieving DICOM destinations...");
items = await client.DicomDestinations.List(cancellationToken);
}
+ catch (ConfigurationException ex)
+ {
+ logger.Log(LogLevel.Critical, ex.Message);
+ return ExitCodes.Config_NotConfigured;
+ }
catch (Exception ex)
{
logger.Log(LogLevel.Critical, $"Error retrieving DICOM destinations: {ex.Message}");
@@ -136,6 +145,9 @@ private async Task ListDestinationHandlerAsync(DestinationApplicationEntity
private async Task RemoveDestinationHandlerAsync(string name, IHost host, bool verbose, CancellationToken cancellationToken)
{
+ Guard.Against.NullOrWhiteSpace(name, nameof(name));
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var configService = host.Services.GetRequiredService();
var client = host.Services.GetRequiredService();
@@ -147,12 +159,18 @@ private async Task RemoveDestinationHandlerAsync(string name, IHost host, b
try
{
- ConfigurationOptions config = LoadConfiguration(verbose, configService, client);
- this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {config.Endpoint}...");
+ CheckConfiguration(configService);
+ client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
+ this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
this.LogVerbose(verbose, host, $"Deleting DICOM destination {name}...");
_ = await client.DicomDestinations.Delete(name, cancellationToken);
logger.Log(LogLevel.Information, $"DICOM destination '{name}' deleted.");
}
+ catch (ConfigurationException ex)
+ {
+ logger.Log(LogLevel.Critical, ex.Message);
+ return ExitCodes.Config_NotConfigured;
+ }
catch (Exception ex)
{
logger.Log(LogLevel.Critical, $"Error deleting DICOM destination {name}: {ex.Message}");
@@ -163,6 +181,9 @@ private async Task RemoveDestinationHandlerAsync(string name, IHost host, b
private async Task AddDestinationHandlerAsync(DestinationApplicationEntity entity, IHost host, bool verbose, CancellationToken cancellationToken)
{
+ Guard.Against.Null(entity, nameof(entity));
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var configService = host.Services.GetRequiredService();
var client = host.Services.GetRequiredService();
@@ -174,9 +195,10 @@ private async Task AddDestinationHandlerAsync(DestinationApplicationEntity
try
{
- ConfigurationOptions config = LoadConfiguration(verbose, configService, client);
+ CheckConfiguration(configService);
+ client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
- this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {config.Endpoint}...");
+ this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
var result = await client.DicomDestinations.Create(entity, cancellationToken);
logger.Log(LogLevel.Information, "New DICOM destination created:");
diff --git a/src/CLI/Commands/RestartCommand.cs b/src/CLI/Commands/RestartCommand.cs
index 0932078e2..211291854 100644
--- a/src/CLI/Commands/RestartCommand.cs
+++ b/src/CLI/Commands/RestartCommand.cs
@@ -16,6 +16,7 @@
using Monai.Deploy.InformaticsGateway.CLI.Services;
using System;
using System.CommandLine.Invocation;
+using System.Threading;
using System.Threading.Tasks;
namespace Monai.Deploy.InformaticsGateway.CLI
@@ -25,11 +26,13 @@ public class RestartCommand : CommandBase
public RestartCommand() : base("restart", $"Restart the {Strings.ApplicationName} service")
{
this.AddConfirmationOption();
- this.Handler = CommandHandler.Create(RestartCommandHandler);
+ this.Handler = CommandHandler.Create(RestartCommandHandler);
}
- private async Task RestartCommandHandler(IHost host, bool yes, bool verbose)
+ private async Task RestartCommandHandler(IHost host, bool yes, bool verbose, CancellationToken cancellationToken)
{
+ Guard.Against.Null(host, nameof(host));
+
var service = host.Services.GetRequiredService();
var confirmation = host.Services.GetRequiredService();
var logger = CreateLogger(host);
@@ -49,7 +52,7 @@ private async Task RestartCommandHandler(IHost host, bool yes, bool verbose
try
{
- await service.Restart();
+ await service.Restart(cancellationToken);
}
catch (Exception ex)
{
diff --git a/src/CLI/Commands/SourceCommand.cs b/src/CLI/Commands/SourceCommand.cs
index 8c35414b4..e687966c7 100644
--- a/src/CLI/Commands/SourceCommand.cs
+++ b/src/CLI/Commands/SourceCommand.cs
@@ -78,6 +78,9 @@ private void SetupAddSourceCommand()
private async Task ListSourceHandlerAsync(SourceApplicationEntity entity, IHost host, bool verbose, CancellationToken cancellationTokena)
{
+ Guard.Against.Null(entity, nameof(entity));
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var console = host.Services.GetRequiredService();
@@ -95,8 +98,9 @@ private async Task ListSourceHandlerAsync(SourceApplicationEntity entity, I
IReadOnlyList items = null;
try
{
- ConfigurationOptions config = LoadConfiguration(verbose, configService, client);
- this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {config.Endpoint}...");
+ CheckConfiguration(configService);
+ client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
+ this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
this.LogVerbose(verbose, host, $"Retrieving DICOM sources...");
items = await client.DicomSources.List(cancellationTokena);
}
@@ -137,6 +141,9 @@ private async Task ListSourceHandlerAsync(SourceApplicationEntity entity, I
private async Task RemoveSourceHandlerAsync(string name, IHost host, bool verbose, CancellationToken cancellationTokena)
{
+ Guard.Against.NullOrWhiteSpace(name, nameof(name));
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var configService = host.Services.GetRequiredService();
var client = host.Services.GetRequiredService();
@@ -148,8 +155,9 @@ private async Task RemoveSourceHandlerAsync(string name, IHost host, bool v
try
{
- ConfigurationOptions config = LoadConfiguration(verbose, configService, client);
- this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {config.Endpoint}...");
+ CheckConfiguration(configService);
+ client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
+ this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
this.LogVerbose(verbose, host, $"Deleting DICOM source {name}...");
_ = await client.DicomSources.Delete(name, cancellationTokena);
logger.Log(LogLevel.Information, $"DICOM source '{name}' deleted.");
@@ -169,6 +177,9 @@ private async Task RemoveSourceHandlerAsync(string name, IHost host, bool v
private async Task AddSourceHandlerAsync(SourceApplicationEntity entity, IHost host, bool verbose, CancellationToken cancellationTokena)
{
+ Guard.Against.Null(entity, nameof(entity));
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var configService = host.Services.GetRequiredService();
var client = host.Services.GetRequiredService();
@@ -180,8 +191,9 @@ private async Task AddSourceHandlerAsync(SourceApplicationEntity entity, IH
try
{
- ConfigurationOptions config = LoadConfiguration(verbose, configService, client);
- this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {config.Endpoint}...");
+ CheckConfiguration(configService);
+ client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
+ this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
this.LogVerbose(verbose, host, $"Creating new DICOM source {entity.AeTitle}...");
var result = await client.DicomSources.Create(entity, cancellationTokena);
diff --git a/src/CLI/Commands/StartCommand.cs b/src/CLI/Commands/StartCommand.cs
index ce0452606..d57539aad 100644
--- a/src/CLI/Commands/StartCommand.cs
+++ b/src/CLI/Commands/StartCommand.cs
@@ -16,6 +16,7 @@
using Monai.Deploy.InformaticsGateway.CLI.Services;
using System;
using System.CommandLine.Invocation;
+using System.Threading;
using System.Threading.Tasks;
namespace Monai.Deploy.InformaticsGateway.CLI
@@ -25,11 +26,13 @@ public class StartCommand : CommandBase
public StartCommand() : base("start", $"Start the {Strings.ApplicationName} service")
{
this.AddConfirmationOption();
- this.Handler = CommandHandler.Create(StartCommandHandler);
+ this.Handler = CommandHandler.Create(StartCommandHandler);
}
- private async Task StartCommandHandler(IHost host, bool yes, bool verbose)
- {
+ private async Task StartCommandHandler(IHost host, bool verbose, CancellationToken cancellationToken)
+ {
+ Guard.Against.Null(host, nameof(host));
+
var service = host.Services.GetRequiredService();
var confirmation = host.Services.GetRequiredService();
var logger = CreateLogger(host);
@@ -38,22 +41,18 @@ private async Task StartCommandHandler(IHost host, bool yes, bool verbose)
Guard.Against.Null(service, nameof(service), "Control service is unavailable.");
Guard.Against.Null(logger, nameof(logger), "Logger is unavailable.");
- if (!yes)
+ try
{
- if (!confirmation.ShowConfirmationPrompt($"Do you want to restart {Strings.ApplicationName}?"))
- {
- logger.Log(LogLevel.Warning, "Action cancelled.");
- return ExitCodes.Start_Cancelled;
- }
+ await service.Start(cancellationToken);
}
-
- try
+ catch (ControlException ex) when (ex.ErrorCode == ExitCodes.Start_Error_ApplicationAlreadyRunning)
{
- await service.Start();
+ logger.Log(LogLevel.Warning, ex.Message);
+ return ex.ErrorCode;
}
catch (Exception ex)
{
- logger.Log(LogLevel.Critical, $"Error starting {Strings.ApplicationName}: {ex.Message}");
+ logger.Log(LogLevel.Critical, ex.Message);
return ExitCodes.Start_Error;
}
return ExitCodes.Success;
diff --git a/src/CLI/Commands/StatusCommand.cs b/src/CLI/Commands/StatusCommand.cs
index 2130dcc27..141bc0e96 100644
--- a/src/CLI/Commands/StatusCommand.cs
+++ b/src/CLI/Commands/StatusCommand.cs
@@ -31,6 +31,8 @@ public StatusCommand() : base("status", $"{Strings.ApplicationName} service stat
private async Task StatusCommandHandlerAsync(IHost host, bool verbose, CancellationToken cancellationToken)
{
+ Guard.Against.Null(host, nameof(host));
+
this.LogVerbose(verbose, host, "Configuring services...");
var configService = host.Services.GetRequiredService();
@@ -44,9 +46,10 @@ private async Task StatusCommandHandlerAsync(IHost host, bool verbose, Canc
HealthStatusResponse response = null;
try
{
- ConfigurationOptions config = LoadConfiguration(verbose, configService, client);
+ CheckConfiguration(configService);
+ client.ConfigureServiceUris(configService.Configurations.InformaticsGatewayServerUri);
- this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {config.Endpoint}...");
+ this.LogVerbose(verbose, host, $"Connecting to {Strings.ApplicationName} at {configService.Configurations.InformaticsGatewayServerEndpoint}...");
this.LogVerbose(verbose, host, $"Retrieving service status...");
response = await client.Health.Status(cancellationToken);
}
diff --git a/src/CLI/Commands/StopCommand.cs b/src/CLI/Commands/StopCommand.cs
index 018b24d4a..ef6762cde 100644
--- a/src/CLI/Commands/StopCommand.cs
+++ b/src/CLI/Commands/StopCommand.cs
@@ -16,6 +16,7 @@
using Monai.Deploy.InformaticsGateway.CLI.Services;
using System;
using System.CommandLine.Invocation;
+using System.Threading;
using System.Threading.Tasks;
namespace Monai.Deploy.InformaticsGateway.CLI
@@ -25,11 +26,13 @@ public class StopCommand : CommandBase
public StopCommand() : base("stop", $"Stop the {Strings.ApplicationName} service")
{
this.AddConfirmationOption();
- this.Handler = CommandHandler.Create(StopCommandHandler);
+ this.Handler = CommandHandler.Create(StopCommandHandler);
}
- private async Task StopCommandHandler(IHost host, bool yes, bool verbose)
+ private async Task StopCommandHandler(IHost host, bool yes, bool verbose, CancellationToken cancellationToken)
{
+ Guard.Against.Null(host, nameof(host));
+
var service = host.Services.GetRequiredService();
var confirmation = host.Services.GetRequiredService();
var logger = CreateLogger(host);
@@ -40,7 +43,7 @@ private async Task StopCommandHandler(IHost host, bool yes, bool verbose)
if (!yes)
{
- if (!confirmation.ShowConfirmationPrompt($"Do you want to restart {Strings.ApplicationName}?"))
+ if (!confirmation.ShowConfirmationPrompt($"Do you want to stop {Strings.ApplicationName}?"))
{
logger.Log(LogLevel.Warning, "Action cancelled.");
return ExitCodes.Stop_Cancelled;
@@ -49,11 +52,11 @@ private async Task StopCommandHandler(IHost host, bool yes, bool verbose)
try
{
- await service.Stop();
+ await service.Stop(cancellationToken);
}
catch (Exception ex)
{
- logger.Log(LogLevel.Critical, $"Error stopping {Strings.ApplicationName}: {ex.Message}");
+ logger.Log(LogLevel.Critical, ex, ex.Message);
return ExitCodes.Stop_Error;
}
return ExitCodes.Success;
diff --git a/src/CLI/ControlException.cs b/src/CLI/ControlException.cs
new file mode 100644
index 000000000..06cb42247
--- /dev/null
+++ b/src/CLI/ControlException.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Monai.Deploy.InformaticsGateway.CLI
+{
+ public class ControlException : Exception
+ {
+ public int ErrorCode { get; }
+
+ public ControlException(int errorCode, string message) : base(message)
+ {
+ ErrorCode = errorCode;
+ }
+ }
+}
diff --git a/src/CLI/ExitCodes.cs b/src/CLI/ExitCodes.cs
index c151c34ad..63788b5f8 100644
--- a/src/CLI/ExitCodes.cs
+++ b/src/CLI/ExitCodes.cs
@@ -17,6 +17,8 @@ public static class ExitCodes
public const int Config_NotConfigured = 100;
public const int Config_ErrorSaving = 101;
+ public const int Config_ErrorInitializing = 102;
+ public const int Config_ErrorShowing = 103;
public const int MonaiScp_ErrorList = 200;
public const int MonaiScp_ErrorDelete = 201;
@@ -35,6 +37,8 @@ public static class ExitCodes
public static int Start_Cancelled = 600;
public static int Start_Error = 601;
+ public static int Start_Error_ApplicationNotFound = 602;
+ public static int Start_Error_ApplicationAlreadyRunning = 603;
public static int Stop_Cancelled = 700;
public static int Stop_Error = 701;
diff --git a/src/CLI/Logging/ConsoleLoggerFactoryExtensions.cs b/src/CLI/Logging/ConsoleLoggerFactoryExtensions.cs
index 6df00a1e6..e6b2ec5e5 100644
--- a/src/CLI/Logging/ConsoleLoggerFactoryExtensions.cs
+++ b/src/CLI/Logging/ConsoleLoggerFactoryExtensions.cs
@@ -9,6 +9,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using Ardalis.GuardClauses;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
@@ -26,11 +27,8 @@ public static class ConsoleLoggerFactoryExtensions
{
public static ILoggingBuilder AddInformaticsGatewayConsole(this ILoggingBuilder builder, Action configure)
{
- if (configure is null)
- {
- throw new ArgumentNullException(nameof(configure));
- }
-
+ Guard.Against.Null(configure, nameof(configure));
+
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
LoggerProviderOptions.RegisterProviderOptions(builder.Services);
diff --git a/src/CLI/Monai.Deploy.InformaticsGateway.CLI.csproj b/src/CLI/Monai.Deploy.InformaticsGateway.CLI.csproj
index e9e324b3a..8a5e1d165 100644
--- a/src/CLI/Monai.Deploy.InformaticsGateway.CLI.csproj
+++ b/src/CLI/Monai.Deploy.InformaticsGateway.CLI.csproj
@@ -34,8 +34,13 @@
+
+
+
+
+
@@ -44,4 +49,4 @@
-
\ No newline at end of file
+
diff --git a/src/CLI/Options/Common.cs b/src/CLI/Options/Common.cs
index 084a5b17d..a2c7b8c54 100644
--- a/src/CLI/Options/Common.cs
+++ b/src/CLI/Options/Common.cs
@@ -11,12 +11,19 @@
using System;
using System.IO;
+using System.Linq;
+using System.Reflection;
namespace Monai.Deploy.InformaticsGateway.CLI
{
public class Common
{
- public static readonly string MigDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".mig");
- public static readonly string CliConfigFilePath = Path.Combine(MigDirectory, "cli.config");
+ public static readonly string HomeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ public static readonly string MigDirectory = Path.Combine(HomeDir, ".mig");
+ public static readonly string ContainerApplicationRootPath = "/opt/monai/ig";
+ public static readonly string MountedConfigFilePath = Path.Combine(ContainerApplicationRootPath, "appsettings.json");
+ public static readonly string MountedDatabasePath = "/database";
+ public static readonly string ConfigFilePath = Path.Combine(MigDirectory, "appsettings.json");
+ public static readonly string AppSettingsResourceName = $"{Assembly.GetEntryAssembly().GetTypes().First().Namespace}.Resources.appsettings.json";
}
}
diff --git a/src/CLI/Program.cs b/src/CLI/Program.cs
index bde8ee330..1f12ce085 100644
--- a/src/CLI/Program.cs
+++ b/src/CLI/Program.cs
@@ -9,6 +9,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using Docker.DotNet;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -55,6 +56,10 @@ private static async Task Main(string[] args)
services.AddSingleton(p => p.GetRequiredService());
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddTransient();
+ services.AddTransient(p => new DockerClientConfiguration().CreateClient());
});
})
.AddGlobalOption(verboseOption)
diff --git a/src/CLI/Services/ConfigurationOptionAccessor.cs b/src/CLI/Services/ConfigurationOptionAccessor.cs
new file mode 100644
index 000000000..cf98b1481
--- /dev/null
+++ b/src/CLI/Services/ConfigurationOptionAccessor.cs
@@ -0,0 +1,285 @@
+// Copyright 2021 MONAI Consortium
+// 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 Ardalis.GuardClauses;
+using Monai.Deploy.InformaticsGateway.Client.Common;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.IO.Abstractions;
+
+namespace Monai.Deploy.InformaticsGateway.CLI
+{
+ public interface IConfigurationOptionAccessor
+ {
+ ///
+ /// Gets or sets the DICOM SCP listening port from appsettings.json.
+ ///
+ int DicomListeningPort { get; set; }
+
+ ///
+ /// Gets or sets the Docker image prefix from appsettings.json.
+ /// This is used to query the Informatics Gateway Docker containers that are installed.
+ ///
+ string DockerImagePrefix { get; }
+
+ ///
+ /// Gets the database storage location on the host system from appsettings.json.
+ ///
+ string HostDatabaseStorageMount { get; }
+
+ ///
+ /// Gets the temprary data storage location on the host system from appsettings.json.
+ ///
+ string HostDataStorageMount { get; }
+
+ ///
+ /// Gets the logs storages location on the host system from appsettings.json.
+ ///
+ string HostLogsStorageMount { get; }
+
+ ///
+ /// Gets or sets the endpoint of the Informatics Gateway.
+ ///
+ string InformaticsGatewayServerEndpoint { get; set; }
+
+ ///
+ /// Gets the port number of the Informatics Gateway server.
+ ///
+ int InformaticsGatewayServerPort { get; }
+
+ ///
+ /// Gets the endpoint of the Informatics Gateway as Uri object.
+ ///
+ Uri InformaticsGatewayServerUri { get; }
+
+ ///
+ /// Gets the log storage path from appsettings.json.
+ ///
+ string LogStoragePath { get; }
+
+ ///
+ /// Gets or set the type of container runner from appsettings.json.
+ ///
+ Runner Runner { get; set; }
+
+ ///
+ /// Gets the temporary storage path from appsettings.json.
+ ///
+ string TempStoragePath { get; }
+
+ ///
+ /// Gets or sets the endpoint to the gRPC API of the Workload Manager from appsettings.json.
+ ///
+ string WorkloadManagerGrpcEndpoint { get; set; }
+
+ ///
+ /// Gets or sets the endpoint to the RESTful API of the Workload Manager from appsettings.json.
+ ///
+ string WorkloadManagerRestEndpoint { get; set; }
+ }
+
+ public class ConfigurationOptionAccessor : IConfigurationOptionAccessor
+ {
+ private static readonly Object SyncLock = new object();
+ private readonly IFileSystem _fileSystem;
+
+ public ConfigurationOptionAccessor(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ public int DicomListeningPort
+ {
+ get
+ {
+ return GetValueFromJsonPath("InformaticsGateway.dicom.scp.port");
+ }
+ set
+ {
+ Guard.Against.OutOfRangePort(value, nameof(InformaticsGatewayServerEndpoint));
+ var jObject = ReadConfigurationFile();
+ jObject["InformaticsGateway"]["dicom"]["scp"]["port"] = value;
+ SaveConfigurationFile(jObject);
+ }
+ }
+
+ public string DockerImagePrefix
+ {
+ get
+ {
+ return GetValueFromJsonPath("Cli.DockerImagePrefix");
+ }
+ }
+
+ public string HostDatabaseStorageMount
+ {
+ get
+ {
+ var path = GetValueFromJsonPath("Cli.HostDatabaseStorageMount");
+ if (path.StartsWith("~/"))
+ {
+ path = path.Replace("~/", $"{Common.HomeDir}/");
+ }
+ return path;
+ }
+ }
+
+ public string HostDataStorageMount
+ {
+ get
+ {
+ var path = GetValueFromJsonPath("Cli.HostDataStorageMount");
+ if (path.StartsWith("~/"))
+ {
+ path = path.Replace("~/", $"{Common.HomeDir}/");
+ }
+ return path;
+ }
+ }
+
+ public string HostLogsStorageMount
+ {
+ get
+ {
+ var path = GetValueFromJsonPath("Cli.HostLogsStorageMount");
+ if (path.StartsWith("~/"))
+ {
+ path = path.Replace("~/", $"{Common.HomeDir}/");
+ }
+ return path;
+ }
+ }
+
+ public string InformaticsGatewayServerEndpoint
+ {
+ get
+ {
+ return GetValueFromJsonPath("Cli.InformaticsGatewayServerEndpoint");
+ }
+ set
+ {
+ Guard.Against.MalformUri(value, nameof(InformaticsGatewayServerEndpoint));
+ var jObject = ReadConfigurationFile();
+ jObject["Cli"]["InformaticsGatewayServerEndpoint"] = value;
+ SaveConfigurationFile(jObject);
+ }
+ }
+
+ public int InformaticsGatewayServerPort
+ {
+ get
+ {
+ return InformaticsGatewayServerUri.Port;
+ }
+ }
+
+ public Uri InformaticsGatewayServerUri
+ {
+ get
+ {
+ return new Uri(InformaticsGatewayServerEndpoint);
+ }
+ }
+
+ public string LogStoragePath
+ {
+ get
+ {
+ var logPath = GetValueFromJsonPath("Logging.File.BasePath");
+ if (logPath.StartsWith("/"))
+ {
+ return logPath;
+ }
+ return _fileSystem.Path.Combine(Common.ContainerApplicationRootPath, logPath);
+ }
+ }
+
+ public Runner Runner
+ {
+ get
+ {
+ var runner = GetValueFromJsonPath("Cli.Runner");
+ return (Runner)Enum.Parse(typeof(Runner), runner);
+ }
+ set
+ {
+ var jObject = ReadConfigurationFile();
+ jObject["Cli"]["Runner"] = value.ToString();
+ SaveConfigurationFile(jObject);
+ }
+ }
+
+ public string TempStoragePath
+ {
+ get
+ {
+ return GetValueFromJsonPath("InformaticsGateway.storage.temporary");
+ }
+ }
+
+ public string WorkloadManagerGrpcEndpoint
+ {
+ get
+ {
+ return GetValueFromJsonPath("InformaticsGateway.workloadManager.grpcEndpoint");
+ }
+ set
+ {
+ Guard.Against.MalformUri(value, nameof(InformaticsGatewayServerEndpoint));
+ var jObject = ReadConfigurationFile();
+ jObject["InformaticsGateway"]["workloadManager"]["grpcEndpoint"] = value;
+ SaveConfigurationFile(jObject);
+ }
+ }
+
+ public string WorkloadManagerRestEndpoint
+ {
+ get
+ {
+ return GetValueFromJsonPath("InformaticsGateway.workloadManager.restEndpoint");
+ }
+ set
+ {
+ Guard.Against.MalformUri(value, nameof(InformaticsGatewayServerEndpoint));
+ var jObject = ReadConfigurationFile();
+ jObject["InformaticsGateway"]["workloadManager"]["restEndpoint"] = value;
+ SaveConfigurationFile(jObject);
+ }
+ }
+
+ private T GetValueFromJsonPath(string jsonPath)
+ {
+ return ReadConfigurationFile().SelectToken(jsonPath).Value();
+ }
+
+ private JObject ReadConfigurationFile()
+ {
+ lock (SyncLock)
+ {
+ return JObject.Parse(_fileSystem.File.ReadAllText(Common.ConfigFilePath));
+ }
+ }
+
+ private void SaveConfigurationFile(JObject jObject)
+ {
+ lock (SyncLock)
+ {
+ using (var file = _fileSystem.File.CreateText(Common.ConfigFilePath))
+ using (var writer = new JsonTextWriter(file))
+ {
+ writer.Formatting = Formatting.Indented;
+ jObject.WriteTo(writer, new Newtonsoft.Json.Converters.StringEnumConverter());
+ }
+ }
+ }
+ }
+}
diff --git a/src/CLI/Services/ConfigurationService.cs b/src/CLI/Services/ConfigurationService.cs
index b3dc12388..bb1d7a5ec 100644
--- a/src/CLI/Services/ConfigurationService.cs
+++ b/src/CLI/Services/ConfigurationService.cs
@@ -10,34 +10,33 @@
// limitations under the License.
using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
using System;
+using System.IO;
using System.IO.Abstractions;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
namespace Monai.Deploy.InformaticsGateway.CLI
{
- public interface IConfigurationService
- {
- void CreateConfigDirectoryIfNotExist();
-
- bool ConfigurationExists();
-
- ConfigurationOptions Load();
-
- ConfigurationOptions Load(bool verbose);
-
- void Save(ConfigurationOptions options);
- }
-
public class ConfigurationService : IConfigurationService
{
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
+ private readonly IEmbeddedResource _embeddedResource;
+
+ public bool IsInitialized => _fileSystem.Directory.Exists(Common.MigDirectory) && IsConfigExists;
+
+ public bool IsConfigExists => _fileSystem.File.Exists(Common.ConfigFilePath);
+
+ public IConfigurationOptionAccessor Configurations { get; }
- public ConfigurationService(ILogger logger, IFileSystem fileSystem)
+ public ConfigurationService(ILogger logger, IFileSystem fileSystem, IEmbeddedResource embeddedResource)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+ _embeddedResource = embeddedResource ?? throw new ArgumentNullException(nameof(embeddedResource));
+ Configurations = new ConfigurationOptionAccessor(fileSystem);
}
public void CreateConfigDirectoryIfNotExist()
@@ -48,45 +47,25 @@ public void CreateConfigDirectoryIfNotExist()
}
}
- public bool ConfigurationExists()
+ public async Task Initialize(CancellationToken cancellationToken)
{
- return _fileSystem.File.Exists(Common.CliConfigFilePath);
- }
-
- public ConfigurationOptions Load() => Load(false);
+ _logger.Log(LogLevel.Debug, $"Reading default application configurations...");
+ using var stream = _embeddedResource.GetManifestResourceStream(Common.AppSettingsResourceName);
- public ConfigurationOptions Load(bool verbose)
- {
- try
+ if (stream is null)
{
- if (verbose)
- {
- this._logger.Log(LogLevel.Debug, "Loading configuration file from {0}", Common.CliConfigFilePath);
- }
-
- using (var file = _fileSystem.File.OpenText(Common.CliConfigFilePath))
- {
- var serializer = new JsonSerializer();
- return serializer.Deserialize(file, typeof(ConfigurationOptions)) as ConfigurationOptions;
- }
+ _logger.Log(LogLevel.Debug, $"Available manifest names {string.Join(",", Assembly.GetExecutingAssembly().GetManifestResourceNames())}");
+ throw new ConfigurationException($"Default configuration file could not be loaded, please reinstall the CLI.");
}
- catch (Exception)
- {
- this._logger.Log(LogLevel.Warning, "Existing configuration file may be corrupted, createing a new one.");
- return new ConfigurationOptions();
- }
- }
+ CreateConfigDirectoryIfNotExist();
- public void Save(ConfigurationOptions options)
- {
- using (var file = _fileSystem.File.CreateText(Common.CliConfigFilePath))
+ _logger.Log(LogLevel.Information, $"Saving appsettings.json to {Common.ConfigFilePath}...");
+ using (var fileStream = _fileSystem.FileStream.Create(Common.ConfigFilePath, FileMode.Create))
{
- var serializer = new JsonSerializer();
- serializer.Formatting = Formatting.Indented;
- serializer.Serialize(file, options);
+ await stream.CopyToAsync(fileStream, cancellationToken);
+ await fileStream.FlushAsync(cancellationToken);
}
-
- this._logger.Log(LogLevel.Information, $"Configuration file {Common.CliConfigFilePath} updated successfully.");
+ this._logger.Log(LogLevel.Information, $"{Common.ConfigFilePath} updated successfully.");
}
}
}
diff --git a/src/CLI/Services/ConfirmationPrompt.cs b/src/CLI/Services/ConfirmationPrompt.cs
index 8c223bb1e..b5b2701e1 100644
--- a/src/CLI/Services/ConfirmationPrompt.cs
+++ b/src/CLI/Services/ConfirmationPrompt.cs
@@ -9,6 +9,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using Ardalis.GuardClauses;
using System;
namespace Monai.Deploy.InformaticsGateway.CLI.Services
@@ -22,7 +23,9 @@ internal class ConfirmationPrompt : IConfirmationPrompt
{
public bool ShowConfirmationPrompt(string message)
{
- Console.Write($"{message} [y/N]");
+ Guard.Against.NullOrWhiteSpace(message, nameof(message));
+
+ Console.Write($"{message} [y/N]: ");
var key = Console.ReadKey();
Console.WriteLine();
if (key.Key == ConsoleKey.Y)
diff --git a/src/CLI/Services/ContainerRunnerFactory.cs b/src/CLI/Services/ContainerRunnerFactory.cs
new file mode 100644
index 000000000..fcc3b5449
--- /dev/null
+++ b/src/CLI/Services/ContainerRunnerFactory.cs
@@ -0,0 +1,41 @@
+// Copyright 2021 MONAI Consortium
+// 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 Microsoft.Extensions.DependencyInjection;
+using System;
+
+namespace Monai.Deploy.InformaticsGateway.CLI
+{
+ public class ContainerRunnerFactory : IContainerRunnerFactory
+ {
+ private readonly IServiceScopeFactory _serviceScopeFactory;
+ private readonly IConfigurationService _configurationService;
+
+ public ContainerRunnerFactory(IServiceScopeFactory serviceScopeFactory, IConfigurationService configurationService)
+ {
+ _serviceScopeFactory = serviceScopeFactory ?? throw new System.ArgumentNullException(nameof(serviceScopeFactory));
+ _configurationService = configurationService ?? throw new System.ArgumentNullException(nameof(configurationService));
+ }
+
+ public IContainerRunner GetContainerRunner()
+ {
+ var scope = _serviceScopeFactory.CreateScope();
+ switch (_configurationService.Configurations.Runner)
+ {
+ case Runner.Docker:
+ return scope.ServiceProvider.GetRequiredService();
+
+ default:
+ throw new NotImplementedException($"The configured runner isn't yet supported '{_configurationService.Configurations.Runner}'");
+ }
+ }
+ }
+}
diff --git a/src/CLI/Services/ControlService.cs b/src/CLI/Services/ControlService.cs
index 785c78259..14bd2e9d2 100644
--- a/src/CLI/Services/ControlService.cs
+++ b/src/CLI/Services/ControlService.cs
@@ -10,43 +10,89 @@
// limitations under the License.
using Microsoft.Extensions.Logging;
+using Monai.Deploy.InformaticsGateway.Common;
using System;
+using System.Threading;
using System.Threading.Tasks;
namespace Monai.Deploy.InformaticsGateway.CLI
{
public interface IControlService
{
- Task Start();
+ Task Restart(CancellationToken cancellationToken = default);
- Task Stop();
+ Task Start(CancellationToken cancellationToken = default);
- Task Restart();
+ Task Stop(CancellationToken cancellationToken = default);
}
public class ControlService : IControlService
{
+ private readonly IConfigurationService _configurationService;
+ private readonly IContainerRunnerFactory _containerRunnerFactory;
private readonly ILogger _logger;
- public ControlService(ILogger logger)
+ public ControlService(IContainerRunnerFactory containerRunnerFactory, ILogger logger, IConfigurationService configService)
{
+ _containerRunnerFactory = containerRunnerFactory ?? throw new ArgumentNullException(nameof(containerRunnerFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _configurationService = configService ?? throw new ArgumentNullException(nameof(configService));
}
- public async Task Restart()
+ public async Task Restart(CancellationToken cancellationToken = default)
{
await Stop();
- await Start();
+ await Start(cancellationToken);
}
- public Task Start()
+ public async Task Start(CancellationToken cancellationToken = default)
{
- throw new NotImplementedException();
+ var runner = _containerRunnerFactory.GetContainerRunner();
+
+ var applicationVersion = await runner.GetLatestApplicationVersion(cancellationToken);
+ if (applicationVersion is null)
+ {
+ throw new ControlException(ExitCodes.Start_Error_ApplicationNotFound, $"No {Strings.ApplicationName} Docker images with prefix `{_configurationService.Configurations.DockerImagePrefix}` found.");
+ }
+ var runnerState = await runner.IsApplicationRunning(applicationVersion, cancellationToken);
+
+ if (runnerState.IsRunning)
+ {
+ throw new ControlException(ExitCodes.Start_Error_ApplicationAlreadyRunning, $"{Strings.ApplicationName} is already running in container ID {runnerState.IdShort}.");
+ }
+
+ await runner.StartApplication(applicationVersion, cancellationToken);
}
- public Task Stop()
+ ///
+ /// Stops any running applications, including, previous releases/versions.
+ ///
+ ///
+ public async Task Stop(CancellationToken cancellationToken = default)
{
- throw new NotImplementedException();
+ var runner = _containerRunnerFactory.GetContainerRunner();
+ var applicationVersions = await runner.GetApplicationVersions(cancellationToken);
+
+ if (!applicationVersions.IsNullOrEmpty())
+ {
+ foreach (var applicationVersion in applicationVersions)
+ {
+ var runnerState = await runner.IsApplicationRunning(applicationVersion, cancellationToken);
+
+ _logger.Log(LogLevel.Debug, $"{Strings.ApplicationName} with container ID {runnerState.Id} running={runnerState.IsRunning}.");
+ if (runnerState.IsRunning)
+ {
+ if (await runner.StopApplication(runnerState, cancellationToken))
+ {
+ _logger.Log(LogLevel.Information, $"{Strings.ApplicationName} with container ID {runnerState.Id} stopped.");
+ }
+ else
+ {
+ _logger.Log(LogLevel.Warning, $"Error may have occurred stopping {Strings.ApplicationName} with container ID {runnerState.Id}. Please verify with the applicatio state with {_configurationService.Configurations.Runner}.");
+ }
+ }
+ }
+ }
}
}
}
diff --git a/src/CLI/Services/DockerRunner.cs b/src/CLI/Services/DockerRunner.cs
new file mode 100644
index 000000000..5a5398b10
--- /dev/null
+++ b/src/CLI/Services/DockerRunner.cs
@@ -0,0 +1,160 @@
+// Copyright 2021 MONAI Consortium
+// 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 Ardalis.GuardClauses;
+using Docker.DotNet;
+using Docker.DotNet.Models;
+using Microsoft.Extensions.Logging;
+using Monai.Deploy.InformaticsGateway.Common;
+using System;
+using System.Collections.Generic;
+using System.IO.Abstractions;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Monai.Deploy.InformaticsGateway.CLI
+{
+ public class DockerRunner : IContainerRunner
+ {
+ private readonly ILogger _logger;
+ private readonly IConfigurationService _configurationService;
+ private readonly IFileSystem _fileSystem;
+ public readonly IDockerClient _dockerClient;
+
+ public DockerRunner(ILogger logger, IConfigurationService configurationService, IFileSystem fileSystem, IDockerClient dockerClient)
+ {
+ _logger = logger ?? throw new System.ArgumentNullException(nameof(logger));
+ _configurationService = configurationService ?? throw new ArgumentNullException(nameof(configurationService));
+ _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+ _dockerClient = dockerClient ?? throw new ArgumentNullException(nameof(dockerClient));
+ }
+
+ public async Task IsApplicationRunning(ImageVersion imageVersion, CancellationToken cancellationToken = default)
+ {
+ Guard.Against.Null(imageVersion, nameof(imageVersion));
+
+ _logger.Log(LogLevel.Debug, $"Checking for existing {Strings.ApplicationName} ({imageVersion.Version}) containers...");
+ var parameters = new ContainersListParameters();
+ parameters.Filters = new Dictionary>
+ {
+ ["ancestor"] = new Dictionary
+ {
+ [imageVersion.Id] = true
+ }
+ };
+ var matches = await _dockerClient.Containers.ListContainersAsync(parameters, cancellationToken);
+ if (matches is null || matches.Count() == 0)
+ {
+ return new RunnerState { IsRunning = false };
+ }
+
+ return new RunnerState { IsRunning = true, Id = matches.First().ID };
+ }
+
+ public async Task GetLatestApplicationVersion(CancellationToken cancellationToken = default)
+ => await GetLatestApplicationVersion(_configurationService.Configurations.DockerImagePrefix, cancellationToken);
+
+ public async Task GetLatestApplicationVersion(string version, CancellationToken cancellationToken = default)
+ {
+ Guard.Against.NullOrWhiteSpace(version, nameof(version));
+
+ var results = await GetApplicationVersions(version, cancellationToken);
+ return results?.OrderByDescending(p => p.Created).FirstOrDefault();
+ }
+
+ public async Task> GetApplicationVersions(CancellationToken cancellationToken = default)
+ => await GetApplicationVersions(_configurationService.Configurations.DockerImagePrefix, cancellationToken);
+
+ public async Task> GetApplicationVersions(string label, CancellationToken cancellationToken = default)
+ {
+ Guard.Against.NullOrWhiteSpace(label, nameof(label));
+
+ _logger.Log(LogLevel.Debug, "Connecting to Docker...");
+ var parameters = new ImagesListParameters();
+ parameters.Filters = new Dictionary>
+ {
+ ["reference"] = new Dictionary
+ {
+ [label] = true
+ }
+ };
+ _logger.Log(LogLevel.Debug, "Retrieving images from Docker...");
+ var images = await _dockerClient.Images.ListImagesAsync(parameters, cancellationToken);
+ return images?.Select(p => new ImageVersion { Version = p.RepoTags.First(), Id = p.ID, Created = p.Created }).ToList();
+ }
+
+ public async Task StartApplication(ImageVersion imageVersion, CancellationToken cancellationToken = default)
+ {
+ Guard.Against.Null(imageVersion, nameof(imageVersion));
+
+ _logger.Log(LogLevel.Information, $"Creating container {Strings.ApplicationName} - {imageVersion.Version} ({imageVersion.IdShort})...");
+ var createContainerParams = new CreateContainerParameters() { Image = imageVersion.Id, HostConfig = new HostConfig() };
+
+ createContainerParams.ExposedPorts = new Dictionary();
+ createContainerParams.HostConfig.PortBindings = new Dictionary>();
+
+ _logger.Log(LogLevel.Information, $"\tPort binding: {_configurationService.Configurations.DicomListeningPort}/tcp");
+ createContainerParams.ExposedPorts.Add($"{_configurationService.Configurations.DicomListeningPort}/tcp", new EmptyStruct());
+ createContainerParams.HostConfig.PortBindings.Add($"{_configurationService.Configurations.DicomListeningPort}/tcp", new List { new PortBinding { HostPort = $"{_configurationService.Configurations.DicomListeningPort}" } });
+
+ _logger.Log(LogLevel.Information, $"\tPort binding: {_configurationService.Configurations.InformaticsGatewayServerPort}/tcp");
+ createContainerParams.ExposedPorts.Add($"{_configurationService.Configurations.InformaticsGatewayServerPort}/tcp", new EmptyStruct());
+ createContainerParams.HostConfig.PortBindings.Add($"{_configurationService.Configurations.InformaticsGatewayServerPort}/tcp", new List { new PortBinding { HostPort = $"{_configurationService.Configurations.InformaticsGatewayServerPort}" } });
+
+ createContainerParams.HostConfig.Mounts = new List();
+ _logger.Log(LogLevel.Information, $"\tMount (configuration file): {Common.ConfigFilePath} => {Common.MountedConfigFilePath}");
+ createContainerParams.HostConfig.Mounts.Add(new Mount { Type = "bind", ReadOnly = true, Source = Common.ConfigFilePath, Target = Common.MountedConfigFilePath });
+
+ _logger.Log(LogLevel.Information, $"\tMount (database file): {_configurationService.Configurations.HostDatabaseStorageMount} => {Common.MountedDatabasePath}");
+ _fileSystem.Directory.CreateDirectoryIfNotExists(_configurationService.Configurations.HostDatabaseStorageMount);
+ createContainerParams.HostConfig.Mounts.Add(new Mount { Type = "bind", ReadOnly = false, Source = _configurationService.Configurations.HostDatabaseStorageMount, Target = Common.MountedDatabasePath });
+
+ _logger.Log(LogLevel.Information, $"\tMount (temporary storage): {_configurationService.Configurations.HostDataStorageMount} => {_configurationService.Configurations.TempStoragePath}");
+ _fileSystem.Directory.CreateDirectoryIfNotExists(_configurationService.Configurations.HostDataStorageMount);
+ createContainerParams.HostConfig.Mounts.Add(new Mount { Type = "bind", ReadOnly = false, Source = _configurationService.Configurations.HostDataStorageMount, Target = _configurationService.Configurations.TempStoragePath });
+
+ _logger.Log(LogLevel.Information, $"\tMount (application logs): {_configurationService.Configurations.HostLogsStorageMount} => {_configurationService.Configurations.LogStoragePath}");
+ _fileSystem.Directory.CreateDirectoryIfNotExists(_configurationService.Configurations.HostLogsStorageMount);
+ createContainerParams.HostConfig.Mounts.Add(new Mount { Type = "bind", ReadOnly = false, Source = _configurationService.Configurations.HostLogsStorageMount, Target = _configurationService.Configurations.LogStoragePath });
+
+ var response = await _dockerClient.Containers.CreateContainerAsync(createContainerParams, cancellationToken);
+ _logger.Log(LogLevel.Debug, $"{Strings.ApplicationName} created with container ID {response.ID.Substring(0, 12)}");
+ if (response.Warnings.Any())
+ {
+ _logger.Log(LogLevel.Warning, $"Warnings: {string.Join(",", response.Warnings)}");
+ }
+
+ _logger.Log(LogLevel.Debug, $"Starting container {response.ID.Substring(0, 12)}...");
+ var containerStartParams = new ContainerStartParameters();
+ if (!await _dockerClient.Containers.StartContainerAsync(response.ID, containerStartParams, cancellationToken))
+ {
+ _logger.Log(LogLevel.Error, $"Error starting container {response.ID.Substring(0, 12)}");
+ return false;
+ }
+ else
+ {
+ _logger.Log(LogLevel.Information, $"{Strings.ApplicationName} started with container ID {response.ID.Substring(0, 12)}");
+ return true;
+ }
+ }
+
+ public async Task StopApplication(RunnerState runnerState, CancellationToken cancellationToken = default)
+ {
+ Guard.Against.Null(runnerState, nameof(runnerState));
+
+ _logger.Log(LogLevel.Debug, $"Stopping {Strings.ApplicationName} with container ID {runnerState.IdShort}.");
+ var result = await _dockerClient.Containers.StopContainerAsync(runnerState.Id, new ContainerStopParameters() { WaitBeforeKillSeconds = 60 }, cancellationToken);
+ _logger.Log(LogLevel.Information, $"{Strings.ApplicationName} with container ID {runnerState.IdShort} stopped.");
+ return result;
+ }
+ }
+}
diff --git a/src/CLI/Options/ConfigurationOptions.cs b/src/CLI/Services/EmbeddedResource.cs
similarity index 59%
rename from src/CLI/Options/ConfigurationOptions.cs
rename to src/CLI/Services/EmbeddedResource.cs
index c7b83fe49..8ec9f9e29 100644
--- a/src/CLI/Options/ConfigurationOptions.cs
+++ b/src/CLI/Services/EmbeddedResource.cs
@@ -1,4 +1,4 @@
-// Copyright 2021 MONAI Consortium
+// Copyright 2021 MONAI Consortium
// 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
@@ -10,21 +10,21 @@
// limitations under the License.
using Ardalis.GuardClauses;
-using System;
+using System.IO;
namespace Monai.Deploy.InformaticsGateway.CLI
{
- public class ConfigurationOptions
+ public interface IEmbeddedResource
{
- public string Endpoint { get; set; }
+ Stream GetManifestResourceStream(string name);
+ }
- public void Validate()
+ public class EmbeddedResource : IEmbeddedResource
+ {
+ public Stream GetManifestResourceStream(string name)
{
- Guard.Against.NullOrEmpty(Endpoint, nameof(Endpoint));
- if (!Uri.IsWellFormedUriString(Endpoint, UriKind.Absolute))
- {
- throw new ArgumentException($"--endpoint '{Endpoint}' is not a valid URI.");
- }
+ Guard.Against.NullOrWhiteSpace(name, nameof(name));
+ return this.GetType().Assembly.GetManifestResourceStream(Common.AppSettingsResourceName);
}
}
}
diff --git a/src/CLI/Services/IConfigurationService.cs b/src/CLI/Services/IConfigurationService.cs
new file mode 100644
index 000000000..459a1a980
--- /dev/null
+++ b/src/CLI/Services/IConfigurationService.cs
@@ -0,0 +1,46 @@
+// Copyright 2021 MONAI Consortium
+// 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.Threading;
+using System.Threading.Tasks;
+
+namespace Monai.Deploy.InformaticsGateway.CLI
+{
+ public interface IConfigurationService
+ {
+ ///
+ /// Gets the configurations inside appsettings.json
+ ///
+ IConfigurationOptionAccessor Configurations { get; }
+
+ ///
+ /// Gets whether the configuration file exists or not.
+ ///
+ bool IsConfigExists { get; }
+
+ ///
+ /// Gets whether the configuration file has been intialized or not.
+ ///
+ bool IsInitialized { get; }
+
+ ///
+ /// Initializes the Informatics Gateway environment.
+ /// Extracts the appsettings.json file to ~/.mig for the application to consume.
+ ///
+ ///
+ Task Initialize(CancellationToken cancellationToken);
+
+ ///
+ /// Creates ~/.mig directory if not already exists.
+ ///
+ void CreateConfigDirectoryIfNotExist();
+ }
+}
diff --git a/src/CLI/Services/IContainerRunner.cs b/src/CLI/Services/IContainerRunner.cs
new file mode 100644
index 000000000..583926fb4
--- /dev/null
+++ b/src/CLI/Services/IContainerRunner.cs
@@ -0,0 +1,96 @@
+// Copyright 2021 MONAI Consortium
+// 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;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Monai.Deploy.InformaticsGateway.CLI
+{
+ ///
+ /// Represents the state of a running application in the orchestration engine.
+ ///
+ public class RunnerState
+ {
+ ///
+ /// Indicates whether the application is running or not.
+ ///
+ public bool IsRunning { get; set; }
+
+ ///
+ /// ID of the running application provided by the orchestration engine.
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// Shorter version of the ID, with 12 characters.
+ ///
+ public string IdShort
+ {
+ get
+ {
+ return Id.Substring(0, Math.Min(12, Id.Length));
+ }
+ }
+ }
+
+ ///
+ /// Represents the image version the container runner detected
+ ///
+ public class ImageVersion
+ {
+ ///
+ /// Version or label of the application/image detected.
+ ///
+ public string Version { get; set; }
+
+ ///
+ /// Unique ID provided by the orchestration engine.
+ ///
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// Shorter version of the ID, with 12 characters.
+ ///
+ public string IdShort
+ {
+ get
+ {
+ var id = Id.Replace("sha256:", "");
+ return id.Substring(0, Math.Min(12, id.Length));
+ }
+ }
+
+ ///
+ /// Date tiem the image was created.
+ ///
+ public DateTime Created { get; set; }
+ }
+
+ public interface IContainerRunner
+ {
+ Task IsApplicationRunning(ImageVersion imageVersion, CancellationToken cancellationToken = default);
+
+ Task GetLatestApplicationVersion(CancellationToken cancellationToken = default);
+
+ Task GetLatestApplicationVersion(string version, CancellationToken cancellationToken = default);
+
+ Task> GetApplicationVersions(CancellationToken cancellationToken = default);
+
+ Task> GetApplicationVersions(string version, CancellationToken cancellationToken = default);
+
+ Task StartApplication(ImageVersion imageVersion, CancellationToken cancellationToken = default);
+
+ Task StopApplication(RunnerState runnerState, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/src/CLI/Services/IContainerRunnerFactory.cs b/src/CLI/Services/IContainerRunnerFactory.cs
new file mode 100644
index 000000000..b3fa4e8eb
--- /dev/null
+++ b/src/CLI/Services/IContainerRunnerFactory.cs
@@ -0,0 +1,18 @@
+// Copyright 2021 MONAI Consortium
+// 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 Monai.Deploy.InformaticsGateway.CLI
+{
+ public interface IContainerRunnerFactory
+ {
+ IContainerRunner GetContainerRunner();
+ }
+}
diff --git a/src/CLI/Test/ConfigurationOptionsTest.cs b/src/CLI/Services/Runner.cs
similarity index 59%
rename from src/CLI/Test/ConfigurationOptionsTest.cs
rename to src/CLI/Services/Runner.cs
index 20a382372..1d997d422 100644
--- a/src/CLI/Test/ConfigurationOptionsTest.cs
+++ b/src/CLI/Services/Runner.cs
@@ -9,19 +9,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-using System;
-using Xunit;
-
-namespace Monai.Deploy.InformaticsGateway.CLI.Test
+namespace Monai.Deploy.InformaticsGateway.CLI
{
- public class ConfigurationOptionsTest
+ public enum Runner
{
- [Fact(DisplayName = "Validate with malformed Uri")]
- public void Validate_MalformedUri()
- {
- var options = new ConfigurationOptions { Endpoint = "http:///bad-uri" };
-
- Assert.Throws(() => options.Validate());
- }
+ Docker,
+ Kubernetes,
+ Helm,
}
}
diff --git a/src/CLI/Strings.cs b/src/CLI/Strings.cs
index d407da40b..399615a97 100644
--- a/src/CLI/Strings.cs
+++ b/src/CLI/Strings.cs
@@ -14,5 +14,6 @@ namespace Monai.Deploy.InformaticsGateway.CLI
internal static class Strings
{
public const string ApplicationName = "MONAI Deploy Informatics Gateway";
+ public const string WorkloadManagerName = "MONAI Deploy Workload Manager";
}
}
diff --git a/src/CLI/Test/AetCommandTest.cs b/src/CLI/Test/AetCommandTest.cs
index 8f77bc4d6..aa61f32de 100644
--- a/src/CLI/Test/AetCommandTest.cs
+++ b/src/CLI/Test/AetCommandTest.cs
@@ -66,8 +66,10 @@ public AetCommandTest()
_paser = _commandLineBuilder.Build();
_loggerFactory.Setup(p => p.CreateLogger(It.IsAny())).Returns(_logger.Object);
- _configurationService.Setup(p => p.ConfigurationExists()).Returns(true);
- _configurationService.Setup(p => p.Load(It.IsAny())).Returns(new ConfigurationOptions { Endpoint = "http://test" });
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(true);
+ _configurationService.SetupGet(p => p.IsConfigExists).Returns(true);
+ _configurationService.Setup(p => p.Configurations.InformaticsGatewayServerUri).Returns(new Uri("http://test"));
+ _configurationService.Setup(p => p.Configurations.InformaticsGatewayServerEndpoint).Returns("http://test");
}
[Fact(DisplayName = "aet comand")]
@@ -108,8 +110,6 @@ public async Task AetAdd_Command()
Assert.Equal(ExitCodes.Success, exitCode);
- _configurationService.Verify(p => p.ConfigurationExists(), Times.Once());
- _configurationService.Verify(p => p.Load(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(
p => p.MonaiScpAeTitle.Create(
@@ -128,14 +128,28 @@ public async Task AetAdd_Command_Exception()
Assert.Equal(ExitCodes.MonaiScp_ErrorCreate, exitCode);
- _configurationService.Verify(p => p.ConfigurationExists(), Times.Once());
- _configurationService.Verify(p => p.Load(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.Create(It.IsAny(), It.IsAny()), Times.Once());
_logger.VerifyLoggingMessageBeginsWith("Error creating MONAI SCP AE Title", LogLevel.Critical, Times.Once());
}
+ [Fact(DisplayName = "aet add comand configuration exception")]
+ public async Task AetAdd_Command_ConfigurationException()
+ {
+ var command = "aet add -n MyName -a MyAET --apps App MyCoolApp TheApp";
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(false);
+
+ int exitCode = await _paser.InvokeAsync(command);
+
+ Assert.Equal(ExitCodes.Config_NotConfigured, exitCode);
+
+ _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Never());
+ _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.List(It.IsAny()), Times.Never());
+
+ _logger.VerifyLoggingMessageBeginsWith("Please execute `testhost config init` to intialize Informatics Gateway.", LogLevel.Critical, Times.Once());
+ }
+
[Fact(DisplayName = "aet remove comand")]
public async Task AetRemove_Command()
{
@@ -152,8 +166,6 @@ public async Task AetRemove_Command()
Assert.Equal(ExitCodes.Success, exitCode);
- _configurationService.Verify(p => p.ConfigurationExists(), Times.Once());
- _configurationService.Verify(p => p.Load(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.Delete(It.Is(o => o.Equals(name)), It.IsAny()), Times.Once());
}
@@ -169,14 +181,28 @@ public async Task AetRemove_Command_Exception()
Assert.Equal(ExitCodes.MonaiScp_ErrorDelete, exitCode);
- _configurationService.Verify(p => p.ConfigurationExists(), Times.Once());
- _configurationService.Verify(p => p.Load(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.Delete(It.IsAny(), It.IsAny()), Times.Once());
_logger.VerifyLoggingMessageBeginsWith("Error deleting MONAI SCP AE Title", LogLevel.Critical, Times.Once());
}
+ [Fact(DisplayName = "aet list comand configuration exception")]
+ public async Task AetRemove_Command_ConfigurationException()
+ {
+ var command = "aet rm -n MyName";
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(false);
+
+ int exitCode = await _paser.InvokeAsync(command);
+
+ Assert.Equal(ExitCodes.Config_NotConfigured, exitCode);
+
+ _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Never());
+ _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.List(It.IsAny()), Times.Never());
+
+ _logger.VerifyLoggingMessageBeginsWith("Please execute `testhost config init` to intialize Informatics Gateway.", LogLevel.Critical, Times.Once());
+ }
+
[Fact(DisplayName = "aet list comand")]
public async Task AetList_Command()
{
@@ -198,8 +224,6 @@ public async Task AetList_Command()
Assert.Equal(ExitCodes.Success, exitCode);
- _configurationService.Verify(p => p.ConfigurationExists(), Times.Once());
- _configurationService.Verify(p => p.Load(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.List(It.IsAny()), Times.Once());
}
@@ -215,14 +239,28 @@ public async Task AetList_Command_Exception()
Assert.Equal(ExitCodes.MonaiScp_ErrorList, exitCode);
- _configurationService.Verify(p => p.ConfigurationExists(), Times.Once());
- _configurationService.Verify(p => p.Load(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.List(It.IsAny()), Times.Once());
_logger.VerifyLoggingMessageBeginsWith("Error retrieving MONAI SCP AE Titles", LogLevel.Critical, Times.Once());
}
+ [Fact(DisplayName = "aet list comand configuration exception")]
+ public async Task AetList_Command_ConfigurationException()
+ {
+ var command = "aet list";
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(false);
+
+ int exitCode = await _paser.InvokeAsync(command);
+
+ Assert.Equal(ExitCodes.Config_NotConfigured, exitCode);
+
+ _informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Never());
+ _informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.List(It.IsAny()), Times.Never());
+
+ _logger.VerifyLoggingMessageBeginsWith("Please execute `testhost config init` to intialize Informatics Gateway.", LogLevel.Critical, Times.Once());
+ }
+
[Fact(DisplayName = "aet list comand empty")]
public async Task AetList_Command_Empty()
{
@@ -234,8 +272,6 @@ public async Task AetList_Command_Empty()
Assert.Equal(ExitCodes.Success, exitCode);
- _configurationService.Verify(p => p.ConfigurationExists(), Times.Once());
- _configurationService.Verify(p => p.Load(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.ConfigureServiceUris(It.IsAny()), Times.Once());
_informaticsGatewayClient.Verify(p => p.MonaiScpAeTitle.List(It.IsAny()), Times.Once());
diff --git a/src/CLI/Test/ConfigCommandTest.cs b/src/CLI/Test/ConfigCommandTest.cs
index e2cf22059..0d16866d0 100644
--- a/src/CLI/Test/ConfigCommandTest.cs
+++ b/src/CLI/Test/ConfigCommandTest.cs
@@ -12,12 +12,15 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Monai.Deploy.InformaticsGateway.CLI.Services;
using Monai.Deploy.InformaticsGateway.Shared.Test;
using Moq;
+using System;
using System.CommandLine.Builder;
using System.CommandLine.Hosting;
using System.CommandLine.Parsing;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using Xunit;
@@ -31,9 +34,11 @@ public class ConfigCommandTest
private readonly Parser _paser;
private readonly Mock _loggerFactory;
private readonly Mock _logger;
+ private readonly Mock _confirmationPrompt;
public ConfigCommandTest()
{
+ _confirmationPrompt = new Mock();
_loggerFactory = new Mock();
_logger = new Mock();
_configurationService = new Mock();
@@ -44,6 +49,7 @@ public ConfigCommandTest()
{
host.ConfigureServices(services =>
{
+ services.AddSingleton(p => _confirmationPrompt.Object);
services.AddSingleton(p => _loggerFactory.Object);
services.AddSingleton(p => _configurationService.Object);
});
@@ -58,10 +64,10 @@ public async Task Config_Command()
{
var command = "config";
var result = _paser.Parse(command);
- Assert.Equal("Option '-e' is required.", result.Errors.First().Message);
+ Assert.Equal("Required command was not provided.", result.Errors.First().Message);
int exitCode = await _paser.InvokeAsync(command);
- Assert.Equal(ExitCodes.Config_NotConfigured, exitCode);
+ Assert.Equal(ExitCodes.Success, exitCode);
}
[Fact(DisplayName = "config show comand when not yet configured")]
@@ -71,15 +77,11 @@ public async Task ConfigShow_Command_NotConfigured()
var result = _paser.Parse(command);
Assert.Equal(0, result.Errors.Count);
- _configurationService.Setup(p => p.ConfigurationExists()).Returns(false);
- _configurationService.Setup(p => p.Load(It.IsAny())).Returns(new ConfigurationOptions { Endpoint = "http://test" });
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(false);
int exitCode = await _paser.InvokeAsync(command);
Assert.Equal(ExitCodes.Config_NotConfigured, exitCode);
-
- _configurationService.Verify(p => p.ConfigurationExists(), Times.Once());
- _configurationService.Verify(p => p.Load(It.IsAny()), Times.Never());
}
[Fact(DisplayName = "config show comand ")]
@@ -89,37 +91,215 @@ public async Task ConfigShow_Command()
var result = _paser.Parse(command);
Assert.Equal(0, result.Errors.Count);
- _configurationService.Setup(p => p.ConfigurationExists()).Returns(true);
- _configurationService.Setup(p => p.Load(It.IsAny())).Returns(new ConfigurationOptions { Endpoint = "http://test" });
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(true);
+ _configurationService.SetupGet(p => p.Configurations.InformaticsGatewayServerEndpoint).Returns("http://test");
+ _configurationService.SetupGet(p => p.Configurations.DicomListeningPort).Returns(100);
+ _configurationService.SetupGet(p => p.Configurations.Runner).Returns(Runner.Docker);
+ _configurationService.SetupGet(p => p.Configurations.HostDatabaseStorageMount).Returns("DB");
+ _configurationService.SetupGet(p => p.Configurations.HostDataStorageMount).Returns("Data");
+ _configurationService.SetupGet(p => p.Configurations.HostLogsStorageMount).Returns("Logs");
+ _configurationService.SetupGet(p => p.Configurations.WorkloadManagerRestEndpoint).Returns("REST");
+ _configurationService.SetupGet(p => p.Configurations.WorkloadManagerGrpcEndpoint).Returns("GRPC");
+
+ int exitCode = await _paser.InvokeAsync(command);
+
+ Assert.Equal(ExitCodes.Success, exitCode);
+
+ _logger.VerifyLogging("Informatics Gateway API: http://test", LogLevel.Information, Times.Once());
+ _logger.VerifyLogging("DICOM SCP Listening Port: 100", LogLevel.Information, Times.Once());
+ _logger.VerifyLogging("Container Runner: Docker", LogLevel.Information, Times.Once());
+ _logger.VerifyLogging("Host:", LogLevel.Information, Times.Once());
+ _logger.VerifyLogging(" Database storage mount: DB", LogLevel.Information, Times.Once());
+ _logger.VerifyLogging(" Data storage mount: Data", LogLevel.Information, Times.Once());
+ _logger.VerifyLogging(" Logs storage mount: Logs", LogLevel.Information, Times.Once());
+ _logger.VerifyLogging("Workload Manager:", LogLevel.Information, Times.Once());
+ _logger.VerifyLogging(" REST API: REST", LogLevel.Information, Times.Once());
+ _logger.VerifyLogging(" gRPC API: GRPC", LogLevel.Information, Times.Once());
+ }
+
+ [Fact(DisplayName = "config show comand exception")]
+ public async Task ConfigShow_Command_Exception()
+ {
+ var command = "config show";
+ var result = _paser.Parse(command);
+ Assert.Equal(0, result.Errors.Count);
+
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(true);
+ _configurationService.SetupGet(p => p.Configurations.InformaticsGatewayServerEndpoint).Throws(new System.Exception("error"));
+
+ int exitCode = await _paser.InvokeAsync(command);
+
+ Assert.Equal(ExitCodes.Config_ErrorShowing, exitCode);
+ }
+
+ [Fact(DisplayName = "config wgrpc command")]
+ public async Task ConfigWmGrpc_Command()
+ {
+ var command = "config wmgrpc http://test:123";
+
+ var result = _paser.Parse(command);
+ Assert.Equal(0, result.Errors.Count);
+
+ var callbackResult = string.Empty;
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(true);
+ _configurationService.SetupSet(p => p.Configurations.WorkloadManagerGrpcEndpoint = It.IsAny())
+ .Callback(value => callbackResult = value);
+
+ int exitCode = await _paser.InvokeAsync(command);
+
+ Assert.Equal(ExitCodes.Success, exitCode);
+ Assert.Equal("http://test:123", callbackResult);
+ }
+
+ [Fact(DisplayName = "config wgrest command")]
+ public async Task ConfigWmRest_Command()
+ {
+ var command = "config wmrest http://test:123";
+
+ var result = _paser.Parse(command);
+ Assert.Equal(0, result.Errors.Count);
+
+ var callbackResult = string.Empty;
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(true);
+ _configurationService.SetupSet(p => p.Configurations.WorkloadManagerRestEndpoint = It.IsAny())
+ .Callback(value => callbackResult = value);
int exitCode = await _paser.InvokeAsync(command);
Assert.Equal(ExitCodes.Success, exitCode);
+ Assert.Equal("http://test:123", callbackResult);
+ }
+
+ [Fact(DisplayName = "config runner command")]
+ public async Task ConfigRunner_Command()
+ {
+ var command = "config runner helm";
- _configurationService.Verify(p => p.ConfigurationExists(), Times.Once());
- _configurationService.Verify(p => p.Load(It.IsAny()), Times.Once());
+ var result = _paser.Parse(command);
+ Assert.Equal(0, result.Errors.Count);
- _logger.VerifyLogging("Endpoint: http://test", LogLevel.Information, Times.Once());
+ var callbackResult = Runner.Docker;
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(true);
+ _configurationService.SetupSet(p => p.Configurations.Runner = It.IsAny())
+ .Callback(value => callbackResult = value);
+
+ int exitCode = await _paser.InvokeAsync(command);
+
+ Assert.Equal(ExitCodes.Success, exitCode);
+ Assert.Equal(Runner.Helm, callbackResult);
}
- [Fact(DisplayName = "config with options")]
- public async Task Config_Command_WithOptions()
+ [Fact(DisplayName = "config endpoint command")]
+ public async Task ConfigEndpoint_Command()
{
- var command = "config -e http://new";
+ var command = "config endpoint http://test:123";
+
var result = _paser.Parse(command);
Assert.Equal(0, result.Errors.Count);
- _configurationService.Setup(p => p.Load(It.IsAny())).Returns(new ConfigurationOptions { Endpoint = "http://old" });
- _configurationService.Setup(p => p.CreateConfigDirectoryIfNotExist());
+ var callbackResult = string.Empty;
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(true);
+ _configurationService.SetupSet(p => p.Configurations.InformaticsGatewayServerEndpoint = It.IsAny())
+ .Callback(value => callbackResult = value);
int exitCode = await _paser.InvokeAsync(command);
Assert.Equal(ExitCodes.Success, exitCode);
+ Assert.Equal("http://test:123", callbackResult);
+ }
+
+ [Fact(DisplayName = "config endpoint command exception")]
+ public async Task ConfigEndpoint_Command_Exception()
+ {
+ var command = "config endpoint http://test:123";
+
+ var result = _paser.Parse(command);
+ Assert.Equal(0, result.Errors.Count);
- _configurationService.Verify(p => p.Load(It.IsAny()), Times.Once());
- _configurationService.Verify(p => p.CreateConfigDirectoryIfNotExist(), Times.Once());
- _configurationService.Verify(p => p.Save(It.Is(
- c => c.Endpoint.Equals("http://new"))), Times.Once());
+ var callbackResult = string.Empty;
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(true);
+ _configurationService.SetupSet(p => p.Configurations.InformaticsGatewayServerEndpoint = It.IsAny())
+ .Throws(new Exception("error"));
+
+ int exitCode = await _paser.InvokeAsync(command);
+
+ Assert.Equal(ExitCodes.Config_ErrorSaving, exitCode);
+ }
+
+ [Fact(DisplayName = "config endpoint command config exception")]
+ public async Task ConfigEndpoint_Command_CopnfigException()
+ {
+ var command = "config endpoint http://test:123";
+
+ var result = _paser.Parse(command);
+ Assert.Equal(0, result.Errors.Count);
+
+ var callbackResult = string.Empty;
+ _configurationService.SetupGet(p => p.IsInitialized).Returns(false);
+
+ int exitCode = await _paser.InvokeAsync(command);
+
+ Assert.Equal(ExitCodes.Config_NotConfigured, exitCode);
+ }
+
+ [Fact(DisplayName = "config init command")]
+ public async Task ConfigInit_Command()
+ {
+ var command = "config init";
+
+ var result = _paser.Parse(command);
+ Assert.Equal(0, result.Errors.Count);
+
+ _configurationService.SetupGet(p => p.IsConfigExists).Returns(false);
+ _configurationService.Setup(p => p.Initialize(It.IsAny()));
+
+ int exitCode = await _paser.InvokeAsync(command);
+ Assert.Equal(ExitCodes.Success, exitCode);
+ _configurationService.Verify(p => p.Initialize(It.IsAny()), Times.Once());
+ }
+
+ [Fact(DisplayName = "config init command bypass prompt")]
+ public async Task ConfigInit_Command_BypassPrompt()
+ {
+ var command = "config init -y";
+
+ var result = _paser.Parse(command);
+ Assert.Equal(0, result.Errors.Count);
+
+ _configurationService.Setup(p => p.Initialize(It.IsAny()));
+
+ int exitCode = await _paser.InvokeAsync(command);
+ Assert.Equal(ExitCodes.Success, exitCode);
+ _configurationService.Verify(p => p.Initialize(It.IsAny()), Times.Once());
+ }
+
+ [Fact(DisplayName = "config init command exception")]
+ public async Task ConfigInit_Command_Exception()
+ {
+ var command = "config init -y";
+
+ var result = _paser.Parse(command);
+ Assert.Equal(0, result.Errors.Count);
+
+ _configurationService.Setup(p => p.Initialize(It.IsAny())).Throws(new Exception("error"));
+
+ int exitCode = await _paser.InvokeAsync(command);
+ Assert.Equal(ExitCodes.Config_ErrorInitializing, exitCode);
+ _configurationService.Verify(p => p.Initialize(It.IsAny()), Times.Once());
+ }
+
+ [Fact(DisplayName = "config init command cancelled")]
+ public async Task ConfigInit_Command_Cancelled()
+ {
+ var command = "config init";
+ _confirmationPrompt.Setup(p => p.ShowConfirmationPrompt(It.IsAny())).Returns(false);
+ _configurationService.SetupGet(p => p.IsConfigExists).Returns(true);
+
+ var result = _paser.Parse(command);
+ Assert.Equal(0, result.Errors.Count);
+
+ int exitCode = await _paser.InvokeAsync(command);
+ Assert.Equal(ExitCodes.Stop_Cancelled, exitCode);
}
}
}
diff --git a/src/CLI/Test/ConfigurationServiceTest.cs b/src/CLI/Test/ConfigurationServiceTest.cs
index a4769794b..18d30f18b 100644
--- a/src/CLI/Test/ConfigurationServiceTest.cs
+++ b/src/CLI/Test/ConfigurationServiceTest.cs
@@ -10,13 +10,13 @@
// limitations under the License.
using Microsoft.Extensions.Logging;
-using Monai.Deploy.InformaticsGateway.Shared.Test;
using Moq;
-using Newtonsoft.Json;
using System;
using System.IO;
using System.IO.Abstractions;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
using Xunit;
namespace Monai.Deploy.InformaticsGateway.CLI.Test
@@ -25,17 +25,30 @@ public class ConfigurationServiceTest
{
private readonly Mock> _logger;
private readonly Mock _fileSystem;
+ private readonly Mock _embeddedResource;
public ConfigurationServiceTest()
{
_logger = new Mock>();
_fileSystem = new Mock();
+ _embeddedResource = new Mock();
+ }
+
+ [Fact(DisplayName = "ConfigurationServiceTest constructor")]
+ public void ConfigurationServiceTest_Constructor()
+ {
+ Assert.Throws(() => new ConfigurationService(null, null, null));
+ Assert.Throws(() => new ConfigurationService(_logger.Object, null, null));
+ Assert.Throws(() => new ConfigurationService(_logger.Object, _fileSystem.Object, null));
+
+ var svc = new ConfigurationService(_logger.Object, _fileSystem.Object, _embeddedResource.Object);
+ Assert.NotNull(svc.Configurations);
}
[Fact(DisplayName = "CreateConfigDirectoryIfNotExist creates directory")]
public void CreateConfigDirectoryIfNotExist_CreateDirectory()
{
- var svc = new ConfigurationService(_logger.Object, _fileSystem.Object);
+ var svc = new ConfigurationService(_logger.Object, _fileSystem.Object, _embeddedResource.Object);
_fileSystem.Setup(p => p.Directory.Exists(It.IsAny())).Returns(false);
svc.CreateConfigDirectoryIfNotExist();
@@ -46,7 +59,7 @@ public void CreateConfigDirectoryIfNotExist_CreateDirectory()
[Fact(DisplayName = "CreateConfigDirectoryIfNotExist skips creating directory")]
public void CreateConfigDirectoryIfNotExist_SkipsCreation()
{
- var svc = new ConfigurationService(_logger.Object, _fileSystem.Object);
+ var svc = new ConfigurationService(_logger.Object, _fileSystem.Object, _embeddedResource.Object);
_fileSystem.Setup(p => p.Directory.Exists(It.IsAny())).Returns(true);
svc.CreateConfigDirectoryIfNotExist();
@@ -54,91 +67,69 @@ public void CreateConfigDirectoryIfNotExist_SkipsCreation()
_fileSystem.Verify(p => p.Directory.CreateDirectory(It.IsAny()), Times.Never());
}
- [Fact(DisplayName = "ConfigurationExists")]
- public void ConfigurationExists()
+ [Fact(DisplayName = "IsInitialized")]
+ public void IsInitialized()
{
- var svc = new ConfigurationService(_logger.Object, _fileSystem.Object);
+ var svc = new ConfigurationService(_logger.Object, _fileSystem.Object, _embeddedResource.Object);
+ _fileSystem.Setup(p => p.Directory.Exists(It.IsAny())).Returns(true);
_fileSystem.Setup(p => p.File.Exists(It.IsAny())).Returns(true);
- Assert.True(svc.ConfigurationExists());
- _fileSystem.Setup(p => p.File.Exists(It.IsAny())).Returns(false);
- Assert.False(svc.ConfigurationExists());
+ Assert.True(svc.IsInitialized);
+ _fileSystem.Setup(p => p.Directory.Exists(It.IsAny())).Returns(false);
+ Assert.False(svc.IsInitialized);
}
- [Fact(DisplayName = "Load verbose logging")]
- public void Load_Verbose()
+ [Fact(DisplayName = "IsConfigExists")]
+ public void ConfigurationExists()
{
- var svc = new ConfigurationService(_logger.Object, _fileSystem.Object);
-
- var stream = new System.IO.StringWriter();
- var config = new ConfigurationOptions { Endpoint = "http://test" };
- var json = JsonConvert.SerializeObject(config);
- _fileSystem.Setup(p => p.File.OpenText(It.IsAny())).Returns(new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(json))));
-
- var result = svc.Load(true);
+ var svc = new ConfigurationService(_logger.Object, _fileSystem.Object, _embeddedResource.Object);
- Assert.Equal(config.Endpoint, result.Endpoint);
- _logger.VerifyLogging(LogLevel.Debug, Times.AtLeastOnce());
+ _fileSystem.Setup(p => p.File.Exists(It.IsAny())).Returns(true);
+ Assert.True(svc.IsConfigExists);
+ _fileSystem.Setup(p => p.File.Exists(It.IsAny())).Returns(false);
+ Assert.False(svc.IsConfigExists);
}
- [Fact(DisplayName = "Load no verbose logging")]
- public void Load_NoVerbose()
+ [Fact(DisplayName = "Initialize with missing config resource")]
+ public void Initialize_ShallThrowWhenConfigReousrceIsMissing()
{
- var svc = new ConfigurationService(_logger.Object, _fileSystem.Object);
-
- var stream = new System.IO.StringWriter();
- var config = new ConfigurationOptions { Endpoint = "http://test" };
- var json = JsonConvert.SerializeObject(config);
- _fileSystem.Setup(p => p.File.OpenText(It.IsAny())).Returns(new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(json))));
+ _embeddedResource.Setup(p => p.GetManifestResourceStream(It.IsAny())).Returns(default(Stream));
- var result = svc.Load();
-
- Assert.Equal(config.Endpoint, result.Endpoint);
- _logger.VerifyLogging(LogLevel.Debug, Times.Never());
+ var svc = new ConfigurationService(_logger.Object, _fileSystem.Object, _embeddedResource.Object);
+ Assert.ThrowsAsync(async () => await svc.Initialize(CancellationToken.None));
}
- [Fact(DisplayName = "Load throws")]
- public void Load_Throws()
+ [Fact(DisplayName = "Initialize creates the config file")]
+ public async Task Initialize_CreatesTheConfigFile()
{
- var svc = new ConfigurationService(_logger.Object, _fileSystem.Object);
-
- using var stream = new System.IO.StringWriter();
- _fileSystem.Setup(p => p.File.OpenText(It.IsAny())).Throws(new Exception("error"));
+ var testString = "hello world";
+ var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(testString));
+ var mockStream = new Mock();
+ byte[] bytesWritten = null;
+
+ mockStream.Setup(p => p.FlushAsync(It.IsAny()));
+ mockStream.Setup(p => p.Close());
+ mockStream.Setup(p => p.CanWrite).Returns(true);
+ mockStream.Setup(s => s.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((byte[] bytes, int offset, int count, CancellationToken cancellationToken) =>
+ {
+ bytesWritten = new byte[bytes.Length];
+ bytes.CopyTo(bytesWritten, 0);
+ });
- var result = svc.Load();
+ _fileSystem.Setup(p => p.Directory.Exists(It.IsAny