diff --git a/guidelines/mwm-developer-setup.md b/guidelines/mwm-developer-setup.md index e1a3e8891..2d9ce1380 100755 --- a/guidelines/mwm-developer-setup.md +++ b/guidelines/mwm-developer-setup.md @@ -32,6 +32,17 @@ Note. if you already have docker container for Minio Rabbit etc running Stop the - `kubectl apply -n argo -f https://raw.githubusercontent.com/argoproj/argo-workflows/master/manifests/quick-start-postgres.yaml` - `kubectl config set-context --current --namespace=argo` +To disable argo authentication run + + kubectl patch deployment \ + argo-server \ + --namespace argo \ + --type='json' \ + -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/args", "value": [ + "server", + "--auth-mode=server" + ]}]' + Note. below Im using bash as its my preferred option, But if you to are using bash and your on windows (wsl2) you MUST make sure you windows .kube/config is also pointing to the same K8's cluster, this is because the code running in vs will look in there for the context to write k8's secrets too! now in a bash window (can be cmd or powershell) diff --git a/src/TaskManager/API/Migrations/M001_TaskDispatchEventInfo_addVerion.cs b/src/TaskManager/API/Migrations/M001_TaskDispatchEventInfo_addVerion.cs index 3001a76d0..1665d4143 100755 --- a/src/TaskManager/API/Migrations/M001_TaskDispatchEventInfo_addVerion.cs +++ b/src/TaskManager/API/Migrations/M001_TaskDispatchEventInfo_addVerion.cs @@ -1,18 +1,18 @@ -// -// Copyright 2023 Guy’s and St Thomas’ NHS Foundation Trust -// -// 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. - +/* + * Copyright 2022 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 Monai.Deploy.WorkflowManager.TaskManager.API.Models; using Mongo.Migration.Migrations.Document; using MongoDB.Bson; diff --git a/src/TaskManager/API/Migrations/M001_TaskExecutionStats_addVersion.cs b/src/TaskManager/API/Migrations/M001_TaskExecutionStats_addVersion.cs index 3f8948a0d..4cfd9d815 100644 --- a/src/TaskManager/API/Migrations/M001_TaskExecutionStats_addVersion.cs +++ b/src/TaskManager/API/Migrations/M001_TaskExecutionStats_addVersion.cs @@ -1,18 +1,19 @@ -// -// Copyright 2023 Guy’s and St Thomas’ NHS Foundation Trust -// -// 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 Monai.Deploy.WorkflowManager.TaskManager.API.Models; +/* + * Copyright 2022 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 Monai.Deploy.WorkflowManager.TaskManager.API.Models; using Mongo.Migration.Migrations.Document; using MongoDB.Bson; diff --git a/src/TaskManager/Plug-ins/AideClinicalReview/AideClinicalReviewPlugin.cs b/src/TaskManager/Plug-ins/AideClinicalReview/AideClinicalReviewPlugin.cs index 46273179a..838ec0c20 100644 --- a/src/TaskManager/Plug-ins/AideClinicalReview/AideClinicalReviewPlugin.cs +++ b/src/TaskManager/Plug-ins/AideClinicalReview/AideClinicalReviewPlugin.cs @@ -88,9 +88,13 @@ private void Initialize() _workflowName = Event.TaskPluginArguments[Keys.WorkflowName]; } - if (Event.TaskPluginArguments.ContainsKey(Keys.Notifications)) + if (Event.TaskPluginArguments.ContainsKey(Keys.Notifications) && Boolean.TryParse(Event.TaskPluginArguments[Keys.Notifications], out var result)) { - _notifications = Boolean.TryParse(Event.TaskPluginArguments[Keys.Notifications], out bool result); + _notifications = result; + } + else + { + _notifications = true; } if (Event.TaskPluginArguments.ContainsKey(Keys.ReviewedExecutionId)) diff --git a/src/TaskManager/Plug-ins/Argo/ArgoClient.cs b/src/TaskManager/Plug-ins/Argo/ArgoClient.cs index 60b7b5dd3..30f9f5fb8 100755 --- a/src/TaskManager/Plug-ins/Argo/ArgoClient.cs +++ b/src/TaskManager/Plug-ins/Argo/ArgoClient.cs @@ -22,37 +22,9 @@ namespace Monai.Deploy.WorkflowManager.TaskManager.Argo { - public interface IArgoClient + public class ArgoClient : BaseArgoClient, IArgoClient { - Task Argo_CreateWorkflowAsync(string argoNamespace, WorkflowCreateRequest body, CancellationToken cancellationToken); - - Task Argo_GetWorkflowAsync(string argoNamespace, string name, string getOptions_resourceVersion, string fields, CancellationToken cancellationToken); - - Task Argo_GetWorkflowTemplateAsync(string argoNamespace, string name, string getOptions_resourceVersion); - - Task Argo_StopWorkflowAsync(string argoNamespace, string name, WorkflowStopRequest body); - - Task Argo_TerminateWorkflowAsync(string argoNamespace, string name, WorkflowTerminateRequest body); - - Task Argo_GetVersionAsync(); - - Task Argo_Get_WorkflowLogsAsync(string argoNamespace, string name, string podName, string logOptions_container); - - Task Argo_CreateWorkflowTemplateAsync(string argoNamespace, WorkflowTemplateCreateRequest body, CancellationToken cancellationToken); - } - - public class ArgoClient : IArgoClient - { - private readonly HttpClient _httpClient; - - public string BaseUrl { get; set; } = "http://localhost:2746"; - - private string FormattedBaseUrl { get { return BaseUrl != null ? BaseUrl.TrimEnd('/') : ""; } } - - public ArgoClient(HttpClient httpClient) - { - _httpClient = httpClient; - } + public ArgoClient(HttpClient httpClient) : base(httpClient) { } public async Task Argo_CreateWorkflowAsync(string argoNamespace, WorkflowCreateRequest body, CancellationToken cancellationToken) { @@ -164,27 +136,113 @@ public async Task Argo_GetWorkflowTemplateAsync(string argoNam urlBuilder.Length--; return await GetRequest(urlBuilder, true).ConfigureAwait(false); } - private async Task SendRequest(StringContent stringContent, StringBuilder urlBuilder, string Method, CancellationToken cancellationToken) + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A successful response. + /// A server side error occurred. + public virtual async Task Argo_CreateWorkflowTemplateAsync(string argoNamespace, WorkflowTemplateCreateRequest body, CancellationToken cancellationToken) + { + Guard.Against.NullOrWhiteSpace(argoNamespace); + Guard.Against.Null(body); + + var urlBuilder = new StringBuilder(); + urlBuilder.Append(CultureInfo.InvariantCulture, $"{FormattedBaseUrl}/api/v1/workflow-templates/{argoNamespace}"); + + var method = "POST"; + var content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(body)); + return await SendRequest(content, urlBuilder, method, cancellationToken).ConfigureAwait(false); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A successful response. + /// A server side error occurred. + public virtual async Task Argo_DeleteWorkflowTemplateAsync(string argoNamespace, string templateName, CancellationToken cancellationToken) { - using (var request = new HttpRequestMessage()) + Guard.Against.NullOrWhiteSpace(argoNamespace); + + var urlBuilder = new StringBuilder(); + urlBuilder.Append(CultureInfo.InvariantCulture, $"{FormattedBaseUrl}/api/v1/workflow-templates/{argoNamespace}/{templateName}"); + + var method = "DELETE"; + var response = await HttpClient.SendAsync(new HttpRequestMessage(new HttpMethod(method), urlBuilder.ToString()), cancellationToken).ConfigureAwait(false); + return (int)response.StatusCode == 200; + } + + public static string ConvertToString(object value, CultureInfo cultureInfo) + { + if (value == null) { - stringContent.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); - request.Content = stringContent; - request.Method = new HttpMethod(Method); - request.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); - request.RequestUri = new Uri(urlBuilder.ToString(), UriKind.RelativeOrAbsolute); + return ""; + } - HttpResponseMessage? response = null; - try + if (value is Enum) + { + var name = Enum.GetName(value.GetType(), value); + if (name != null) { - response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name); + if (field != null) + { + if (System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) is System.Runtime.Serialization.EnumMemberAttribute attribute) + { + return attribute.Value ?? name; + } + } + + var converted = Convert.ToString(Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), cultureInfo)); + return converted ?? string.Empty; } - catch (Exception ex) + } + else if (value is bool boolean) + { + return Convert.ToString(boolean, cultureInfo).ToLowerInvariant(); + } + else if (value is byte[] v) + { + return Convert.ToBase64String(v); + } + else if (value.GetType().IsArray) + { + var array = Enumerable.OfType((Array)value); + return string.Join(",", Enumerable.Select(array, o => ConvertToString(o, cultureInfo))); + } + + var result = Convert.ToString(value, cultureInfo); + return result ?? ""; + } + } + + /// + /// generic functions relating to argo requests + /// + public class BaseArgoClient + { + public string BaseUrl { get; set; } = "http://localhost:2746"; + + protected string FormattedBaseUrl { get { return BaseUrl != null ? BaseUrl.TrimEnd('/') : ""; } } + + protected readonly HttpClient HttpClient; + + public BaseArgoClient(HttpClient httpClient) + { + HttpClient = httpClient; + } + + protected async Task SendRequest(StringContent stringContent, StringBuilder urlBuilder, string method, CancellationToken cancellationToken) + { + using (var request = new HttpRequestMessage()) + { + if (stringContent is not null) { - var mess = ex.Message; - throw; + stringContent.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request.Content = stringContent; } + request.Method = new HttpMethod(method); + request.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + request.RequestUri = new Uri(urlBuilder.ToString(), UriKind.RelativeOrAbsolute); + HttpResponseMessage? response = null; + response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); try { @@ -222,7 +280,7 @@ private async Task SendRequest(StringContent stringContent, StringBuilder } } - private async Task GetRequest(StringBuilder urlBuilder, bool isLogs = false) + protected async Task GetRequest(StringBuilder urlBuilder, bool isLogs = false) { using (var request = new HttpRequestMessage()) @@ -231,7 +289,7 @@ private async Task SendRequest(StringContent stringContent, StringBuilder request.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); request.RequestUri = new Uri(urlBuilder.ToString(), UriKind.RelativeOrAbsolute); - var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); try { var headers_ = Enumerable.ToDictionary(response.Headers, h_ => h_.Key, h_ => h_.Value); @@ -271,10 +329,9 @@ private async Task SendRequest(StringContent stringContent, StringBuilder } } - protected virtual async Task> ReadObjectResponseAsync(HttpResponseMessage response, IReadOnlyDictionary> headers, bool isLogs = false) { - if (response == null || response.Content == null) + if (response == null || response.Content == null || response.Content.GetType().Name == "EmptyContent") { return new ObjectResponseResult(default, string.Empty); } @@ -306,7 +363,7 @@ private async Task SendRequest(StringContent stringContent, StringBuilder } } - protected static string DecodeLogs(string logInput) + public static string DecodeLogs(string logInput) { var rows = logInput.Split(new String[] { "\n" }, StringSplitOptions.None); var jsonBody = $"[{string.Join(",", rows)}]"; @@ -348,23 +405,7 @@ protected virtual async Task> ReadLogResponseAsync( } } - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - /// A successful response. - /// A server side error occurred. - public virtual async Task Argo_CreateWorkflowTemplateAsync(string argoNamespace, WorkflowTemplateCreateRequest body, CancellationToken cancellationToken) - { - Guard.Against.NullOrWhiteSpace(argoNamespace); - Guard.Against.Null(body); - - var urlBuilder = new StringBuilder(); - urlBuilder.Append(CultureInfo.InvariantCulture, $"{FormattedBaseUrl}/api/v1/workflow-templates/{argoNamespace}"); - - var Method = "POST"; - var content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(body)); - return await SendRequest(content, urlBuilder, Method, cancellationToken).ConfigureAwait(false); - } - - protected struct ObjectResponseResult + protected readonly struct ObjectResponseResult { public ObjectResponseResult(T responseObject, string responseText) { @@ -377,61 +418,19 @@ public ObjectResponseResult(T responseObject, string responseText) public string Text { get; } } - private string ConvertToString(object value, CultureInfo cultureInfo) - { - if (value == null) - { - return ""; - } - - if (value is Enum) - { - var name = Enum.GetName(value.GetType(), value); - if (name != null) - { - var field = System.Reflection.IntrospectionExtensions.GetTypeInfo(value.GetType()).GetDeclaredField(name); - if (field != null) - { - if (System.Reflection.CustomAttributeExtensions.GetCustomAttribute(field, typeof(System.Runtime.Serialization.EnumMemberAttribute)) is System.Runtime.Serialization.EnumMemberAttribute attribute) - { - return attribute.Value ?? name; - } - } - - var converted = Convert.ToString(Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), cultureInfo)); - return converted ?? string.Empty; - } - } - else if (value is bool boolean) - { - return Convert.ToString(boolean, cultureInfo).ToLowerInvariant(); - } - else if (value is byte[] v) - { - return Convert.ToBase64String(v); - } - else if (value.GetType().IsArray) - { - var array = Enumerable.OfType((Array)value); - return string.Join(",", Enumerable.Select(array, o => ConvertToString(o, cultureInfo))); - } - - var result = Convert.ToString(value, cultureInfo); - return result ?? ""; - } class ArgoLogEntry { public string Content { get; set; } = ""; public string PodName { get; set; } = ""; } + class ArgoLogEntryResult { public ArgoLogEntry Result { get; set; } = new ArgoLogEntry(); } } - public class Version { [Newtonsoft.Json.JsonProperty("buildDate", Required = Newtonsoft.Json.Required.Always)] diff --git a/src/TaskManager/Plug-ins/Argo/ArgoPlugin.cs b/src/TaskManager/Plug-ins/Argo/ArgoPlugin.cs index 8c950665d..cce1ad4e7 100755 --- a/src/TaskManager/Plug-ins/Argo/ArgoPlugin.cs +++ b/src/TaskManager/Plug-ins/Argo/ArgoPlugin.cs @@ -434,7 +434,7 @@ private void ProcessTaskPluginArguments(Workflow workflow) Guard.Against.Null(workflow); var resources = Event.GetTaskPluginArgumentsParameter>(Keys.ArgoResource); - var priorityClassName = Event.GetTaskPluginArgumentsParameter(Keys.TaskPriorityClassName); + var priorityClassName = Event.GetTaskPluginArgumentsParameter(Keys.TaskPriorityClassName) ?? "standard"; var argoParameters = Event.GetTaskPluginArgumentsParameter>(Keys.ArgoParameters); if (argoParameters is not null) @@ -462,12 +462,9 @@ private void ProcessTaskPluginArguments(Workflow workflow) AddRequest(resources, template, ResourcesKeys.CpuReservation); AddRequest(resources, template, ResourcesKeys.MemoryReservation); AddRequest(resources, template, ResourcesKeys.GpuLimit); - - if (priorityClassName is not null) - { - template.PriorityClassName = priorityClassName; - } + template.PriorityClassName = priorityClassName; } + workflow.Spec.PodPriorityClassName = priorityClassName; } private static void AddLimit(Dictionary? resources, Template2 template, ResourcesKey key) @@ -981,5 +978,19 @@ public async Task CreateArgoTemplate(string template) throw; } } + + public async Task DeleteArgoTemplate(string templateName) + { + try + { + var client = _argoProvider.CreateClient(_baseUrl, _apiToken, _allowInsecure); + return await client.Argo_DeleteWorkflowTemplateAsync(_namespace, templateName, new CancellationToken()).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorDeletingWorkflowTemplate(ex); + throw; + } + } } } diff --git a/src/TaskManager/Plug-ins/Argo/Controllers/TemplateController.cs b/src/TaskManager/Plug-ins/Argo/Controllers/TemplateController.cs index 1a47526bc..8d121ffa7 100644 --- a/src/TaskManager/Plug-ins/Argo/Controllers/TemplateController.cs +++ b/src/TaskManager/Plug-ins/Argo/Controllers/TemplateController.cs @@ -30,14 +30,15 @@ namespace Monai.Deploy.WorkflowManager.TaskManager.Argo.Controllers public class TemplateController : ControllerBase { private readonly ArgoPlugin _argoPlugin; - private readonly ILogger _tempLogger; + private readonly ILogger _logger; + public TemplateController( IServiceScopeFactory scopeFactory, ILogger tempLogger, ILogger argoLogger, IOptions options) { - _tempLogger = tempLogger; + _logger = tempLogger; _argoPlugin = new ArgoPlugin(scopeFactory, argoLogger, options, new Messaging.Events.TaskDispatchEvent()); @@ -50,8 +51,9 @@ public async Task> CreateArgoTemplate() using StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8); var value2 = await reader.ReadToEndAsync(); + _logger.LogDebug($"value passed into template :{value2}"); - if (String.IsNullOrWhiteSpace(value2)) + if (string.IsNullOrWhiteSpace(value2)) { return BadRequest("No file recieved"); } @@ -68,5 +70,24 @@ public async Task> CreateArgoTemplate() return Ok(workflowTemplate); } + + [Route("{name}")] + [HttpDelete] + public async Task> DeleteArgoTemplate(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return BadRequest("No name parameter provided"); + } + + try + { + return Ok(await _argoPlugin.DeleteArgoTemplate(name)); + } + catch (Exception) + { + return BadRequest("message: Argo unable to process template"); + } + } } } diff --git a/src/TaskManager/Plug-ins/Argo/IArgoClient.cs b/src/TaskManager/Plug-ins/Argo/IArgoClient.cs new file mode 100644 index 000000000..ea15b34bb --- /dev/null +++ b/src/TaskManager/Plug-ins/Argo/IArgoClient.cs @@ -0,0 +1,42 @@ +/* + * Copyright 2022 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 Argo; + + +namespace Monai.Deploy.WorkflowManager.TaskManager.Argo +{ + public interface IArgoClient + { + Task Argo_CreateWorkflowAsync(string argoNamespace, WorkflowCreateRequest body, CancellationToken cancellationToken); + + Task Argo_GetWorkflowAsync(string argoNamespace, string name, string getOptions_resourceVersion, string fields, CancellationToken cancellationToken); + + Task Argo_GetWorkflowTemplateAsync(string argoNamespace, string name, string getOptions_resourceVersion); + + Task Argo_StopWorkflowAsync(string argoNamespace, string name, WorkflowStopRequest body); + + Task Argo_TerminateWorkflowAsync(string argoNamespace, string name, WorkflowTerminateRequest body); + + Task Argo_GetVersionAsync(); + + Task Argo_Get_WorkflowLogsAsync(string argoNamespace, string name, string podName, string logOptions_container); + + Task Argo_CreateWorkflowTemplateAsync(string argoNamespace, WorkflowTemplateCreateRequest body, CancellationToken cancellationToken); + + Task Argo_DeleteWorkflowTemplateAsync(string argoNamespace, string templateName, CancellationToken cancellationToken); + } +} diff --git a/src/TaskManager/Plug-ins/Argo/Logging/Log.cs b/src/TaskManager/Plug-ins/Argo/Logging/Log.cs index 0e9f90804..3ac7bcf45 100755 --- a/src/TaskManager/Plug-ins/Argo/Logging/Log.cs +++ b/src/TaskManager/Plug-ins/Argo/Logging/Log.cs @@ -78,5 +78,8 @@ public static partial class Log [LoggerMessage(EventId = 1018, Level = LogLevel.Error, Message = "Error deserializing WorkflowTemplateCreateRequest. {message}")] public static partial void ErrorDeserializingWorkflowTemplateCreateRequest(this ILogger logger, string message, Exception ex); + [LoggerMessage(EventId = 1017, Level = LogLevel.Error, Message = "Error deleting Template in Argo.")] + public static partial void ErrorDeletingWorkflowTemplate(this ILogger logger, Exception ex); + } } diff --git a/src/TaskManager/Plug-ins/Argo/StaticValues/Keys.cs b/src/TaskManager/Plug-ins/Argo/StaticValues/Keys.cs index c0c6edb16..5e78c2f15 100644 --- a/src/TaskManager/Plug-ins/Argo/StaticValues/Keys.cs +++ b/src/TaskManager/Plug-ins/Argo/StaticValues/Keys.cs @@ -86,7 +86,7 @@ internal static class Keys /// /// Key for priority classnames on task plugin arguments side /// - public static readonly string TaskPriorityClassName = "priorityClass"; + public static readonly string TaskPriorityClassName = "priority"; /// /// Required arguments to run the Argo workflow. diff --git a/src/TaskManager/TaskManager/appsettings.Development.json b/src/TaskManager/TaskManager/appsettings.Development.json index 0c6232794..381725308 100755 --- a/src/TaskManager/TaskManager/appsettings.Development.json +++ b/src/TaskManager/TaskManager/appsettings.Development.json @@ -21,7 +21,7 @@ "test": "Monai.Deploy.WorkflowManager.TaskManager.TestPlugin.Repositories.TestPluginRepository, Monai.Deploy.WorkflowManager.TaskManager.TestPlugin" }, "argoPluginArguments": { - "server_url": "http://localhost:2746" + "server_url": "https://localhost:2746" } }, "storage": { diff --git a/src/TaskManager/TaskManager/appsettings.Local.json b/src/TaskManager/TaskManager/appsettings.Local.json index fb6d52109..f58a147a7 100755 --- a/src/TaskManager/TaskManager/appsettings.Local.json +++ b/src/TaskManager/TaskManager/appsettings.Local.json @@ -21,7 +21,7 @@ "test": "Monai.Deploy.WorkflowManager.TaskManager.TestPlugin.Repositories.TestPluginRepository, Monai.Deploy.WorkflowManager.TaskManager.TestPlugin" }, "argoPluginArguments": { - "server_url": "http://localhost:2746" + "server_url": "https://localhost:2746" } }, "storage": { diff --git a/src/WorkflowManager/Common/Interfaces/IWorkflowService.cs b/src/WorkflowManager/Common/Interfaces/IWorkflowService.cs index 1b052f23f..d89ee985c 100644 --- a/src/WorkflowManager/Common/Interfaces/IWorkflowService.cs +++ b/src/WorkflowManager/Common/Interfaces/IWorkflowService.cs @@ -50,5 +50,21 @@ public interface IWorkflowService : IPaginatedApi /// /// Workflow to delete. Task DeleteWorkflowAsync(WorkflowRevision workflow); + + /// + /// get all workflows with AeTitle + /// + /// the title to get + /// skip x num of records + /// limit to x number + /// + Task> GetByAeTitleAsync(string aeTitle, int? skip = null, int? limit = null); + + /// + /// returns the number of workflows with this aetitle + /// + /// the title to count + /// + Task GetCountByAeTitleAsync(string aeTitle); } } diff --git a/src/WorkflowManager/Common/Services/WorkflowService.cs b/src/WorkflowManager/Common/Services/WorkflowService.cs index 26b44f210..40d7d87c3 100644 --- a/src/WorkflowManager/Common/Services/WorkflowService.cs +++ b/src/WorkflowManager/Common/Services/WorkflowService.cs @@ -88,5 +88,13 @@ public Task DeleteWorkflowAsync(WorkflowRevision workflow) public async Task> GetAllAsync(int? skip = null, int? limit = null) => await _workflowRepository.GetAllAsync(skip, limit); + + + public async Task> GetByAeTitleAsync(string aeTitle, int? skip = null, int? limit = null) + => await _workflowRepository.GetAllByAeTitleAsync(aeTitle, skip, limit); + + public async Task GetCountByAeTitleAsync(string aeTitle) + => await _workflowRepository.GetCountByAeTitleAsync(aeTitle); + } } diff --git a/src/WorkflowManager/Database/Interfaces/IWorkflowRepository.cs b/src/WorkflowManager/Database/Interfaces/IWorkflowRepository.cs index 29b2a4757..5cf6b7d52 100644 --- a/src/WorkflowManager/Database/Interfaces/IWorkflowRepository.cs +++ b/src/WorkflowManager/Database/Interfaces/IWorkflowRepository.cs @@ -52,6 +52,15 @@ public interface IWorkflowRepository /// An aeTitle to retrieve. Task GetByAeTitleAsync(string aeTitle); + Task> GetAllByAeTitleAsync(string aeTitle, int? skip, int? limit); + + /// + /// Retrieves a count of workflows based on an aeTitle. + /// + /// + /// + Task GetCountByAeTitleAsync(string aeTitle); + /// /// Retrieves a list of workflows based on an aeTitle. /// diff --git a/src/WorkflowManager/Database/Repositories/WorkflowRepository.cs b/src/WorkflowManager/Database/Repositories/WorkflowRepository.cs index f45bbe250..df0e1d0c8 100755 --- a/src/WorkflowManager/Database/Repositories/WorkflowRepository.cs +++ b/src/WorkflowManager/Database/Repositories/WorkflowRepository.cs @@ -43,6 +43,33 @@ public WorkflowRepository( var mongoDatabase = client.GetDatabase(databaseSettings.Value.DatabaseName); _workflowCollection = mongoDatabase.GetCollection("Workflows"); + EnsureIndex().GetAwaiter().GetResult(); + } + + private async Task EnsureIndex() + { + Guard.Against.Null(_workflowCollection, "WorkflowCollection"); + + var asyncCursor = (await _workflowCollection.Indexes.ListAsync()); + var bsonDocuments = (await asyncCursor.ToListAsync()); + var indexes = bsonDocuments.Select(_ => _.GetElement("name").Value.ToString()).ToList(); + + // If index not present create it else skip. + if (!indexes.Any(i => i is not null && i.Equals("AeTitleIndex"))) + { + // Create Index here + + var options = new CreateIndexOptions() + { + Name = "AeTitleIndex" + }; + var model = new CreateIndexModel( + Builders.IndexKeys.Ascending(s => s.Workflow.InformaticsGateway.AeTitle), + options + ); + + await _workflowCollection.Indexes.CreateOneAsync(model); + } } public List GetWorkflowsList() @@ -123,6 +150,29 @@ public async Task GetByAeTitleAsync(string aeTitle) return workflow; } + public async Task> GetAllByAeTitleAsync(string aeTitle, int? skip, int? limit) + { + Guard.Against.NullOrWhiteSpace(aeTitle, nameof(aeTitle)); +#pragma warning disable CS8602 // Dereference of a possibly null reference. + var workflows = await _workflowCollection + .Find(x => x.Workflow.InformaticsGateway.AeTitle == aeTitle && x.Deleted == null) + .Skip(skip) + .Limit(limit) + .ToListAsync(); + + return workflows + .GroupBy(w => w.WorkflowId) + .Select(g => g.First()) + .ToList(); ; + } + + public async Task GetCountByAeTitleAsync(string aeTitle) + { + Guard.Against.NullOrWhiteSpace(aeTitle, nameof(aeTitle)); + return await _workflowCollection + .CountDocumentsAsync(x => x.Workflow.InformaticsGateway.AeTitle == aeTitle && x.Deleted == null); + } + public async Task> GetWorkflowsByAeTitleAsync(List aeTitles) { Guard.Against.NullOrEmpty(aeTitles, nameof(aeTitles)); diff --git a/src/WorkflowManager/Logging/Log.100000.Http.cs b/src/WorkflowManager/Logging/Log.100000.Http.cs index 05c59fe61..80e342603 100644 --- a/src/WorkflowManager/Logging/Log.100000.Http.cs +++ b/src/WorkflowManager/Logging/Log.100000.Http.cs @@ -61,5 +61,8 @@ public static partial class Log [LoggerMessage(EventId = 100013, Level = LogLevel.Information, Message = "BYpass authentication.")] public static partial void BypassAuthentication(this ILogger logger); + + [LoggerMessage(EventId = 100014, Level = LogLevel.Error, Message = "Unexpected error occurred in get /workflows/aetitle API.")] + public static partial void WorkflowGetAeTitleAsyncError(this ILogger logger, Exception ex); } } diff --git a/src/WorkflowManager/Storage/Services/DicomService.cs b/src/WorkflowManager/Storage/Services/DicomService.cs index 3d618f87b..c47c2fc65 100644 --- a/src/WorkflowManager/Storage/Services/DicomService.cs +++ b/src/WorkflowManager/Storage/Services/DicomService.cs @@ -15,7 +15,6 @@ */ using System.Globalization; -using System.Linq; using System.Text; using Ardalis.GuardClauses; using Microsoft.Extensions.Logging; diff --git a/src/WorkflowManager/WorkflowManager/Controllers/WorkflowsController.cs b/src/WorkflowManager/WorkflowManager/Controllers/WorkflowsController.cs index c16d3cbc9..ab990a21d 100644 --- a/src/WorkflowManager/WorkflowManager/Controllers/WorkflowsController.cs +++ b/src/WorkflowManager/WorkflowManager/Controllers/WorkflowsController.cs @@ -295,5 +295,52 @@ public async Task DeleteAsync([FromRoute] string id) InternalServerError); } } + + /// + /// + /// + /// + /// + /// A representing the result of the asynchronous operation. + [HttpGet("aetitle/{title}")] + [ProducesResponseType(typeof(WorkflowRevision), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)] + public async Task GetByAeTitle([FromRoute] string title, [FromQuery] PaginationFilter filter) + { + if (string.IsNullOrWhiteSpace(title)) + { + _logger.LogDebug($"{nameof(GetByAeTitle)} - Failed to validate {nameof(title)}"); + + return Problem($"Failed to validate {nameof(title)}, not a valid AE title", $"/workflows/aetitle/{title}", BadRequest); + } + + try + { + var route = Request?.Path.Value ?? string.Empty; + var pageSize = filter.PageSize ?? _options.Value.EndpointSettings.DefaultPageSize; + var validFilter = new PaginationFilter(filter.PageNumber, pageSize, _options.Value.EndpointSettings.MaxPageSize); + + var pagedData = await _workflowService.GetByAeTitleAsync( + title, + (validFilter.PageNumber - 1) * validFilter.PageSize, + validFilter.PageSize); + + var dataTotal = await _workflowService.GetCountByAeTitleAsync(title); + var pagedReponse = CreatePagedReponse(pagedData.ToList(), validFilter, dataTotal, _uriService, route); + + return Ok(pagedReponse); + } + catch (Exception e) + { + _logger.WorkflowGetAeTitleAsyncError(e); + + return Problem( + $"Unexpected error occurred: {e.Message}", + $"/workflows/aetitle", + InternalServerError); + } + } } } diff --git a/src/WorkflowManager/WorkflowManager/Validators/WorkflowValidator.cs b/src/WorkflowManager/WorkflowManager/Validators/WorkflowValidator.cs index d25a3352d..acec57652 100644 --- a/src/WorkflowManager/WorkflowManager/Validators/WorkflowValidator.cs +++ b/src/WorkflowManager/WorkflowManager/Validators/WorkflowValidator.cs @@ -41,6 +41,7 @@ public class WorkflowValidator public static readonly string Separator = ";"; private const string Comma = ", "; private readonly ILogger _logger; + public static readonly string TaskPriorityClassName = "priority"; /// /// Initializes a new instance of the class. @@ -335,6 +336,18 @@ private void ValidateArgoTask(TaskObject currentTask) { Errors.Add($"Task: '{currentTask.Id}' workflow_template_name must be specified{Comma}this corresponds to an Argo template name."); } + + if (currentTask.Args.ContainsKey(TaskPriorityClassName)) + { + switch (currentTask.Args[TaskPriorityClassName].ToLower()) + { + case "high" or "standard" or "low": + break; + default: + Errors.Add($"Task: '{currentTask.Id}' TaskPriorityClassName must be one of \"high\"{Comma} \"standard\" or \"low\""); + break; + } + } } private void ValidateClinicalReviewTask(TaskObject[] tasks, TaskObject currentTask) diff --git a/tests/IntegrationTests/TaskManager.IntegrationTests/Support/Assertions.cs b/tests/IntegrationTests/TaskManager.IntegrationTests/Support/Assertions.cs index 9ab70b518..c68ff8cdd 100644 --- a/tests/IntegrationTests/TaskManager.IntegrationTests/Support/Assertions.cs +++ b/tests/IntegrationTests/TaskManager.IntegrationTests/Support/Assertions.cs @@ -40,12 +40,12 @@ public void AssertClinicalReviewEvent(ClinicalReviewRequestEvent clinicalReviewR clinicalReviewRequestEvent.PatientMetadata.PatientName.Should().Be(GetTaskPluginArguments(taskDispatchEvent, "patient_name")); clinicalReviewRequestEvent.PatientMetadata.PatientSex.Should().Be(GetTaskPluginArguments(taskDispatchEvent, "patient_sex")); clinicalReviewRequestEvent.PatientMetadata.PatientDob.Should().Be(GetTaskPluginArguments(taskDispatchEvent, "patient_dob")); - try - { - var notifications = GetTaskPluginArguments(taskDispatchEvent, "notifications"); + if (Boolean.TryParse(GetTaskPluginArguments(taskDispatchEvent, "notifications"), out bool result)) + { + clinicalReviewRequestEvent.Notifications.Should().Be(result); } - catch (Exception ex) + else { clinicalReviewRequestEvent.Notifications.Should().Be(true); } diff --git a/tests/UnitTests/Common.Tests/Services/WorkflowServiceTests.cs b/tests/UnitTests/Common.Tests/Services/WorkflowServiceTests.cs index d2b135c53..7876bd081 100644 --- a/tests/UnitTests/Common.Tests/Services/WorkflowServiceTests.cs +++ b/tests/UnitTests/Common.Tests/Services/WorkflowServiceTests.cs @@ -53,6 +53,24 @@ public async Task WorkflowService_NoExistingWorkflow_ReturnsNull() Assert.Null(result); } + [Fact] + public async Task WorkflowService_GetAsync_With_Empty() + { + await Assert.ThrowsAsync(() => WorkflowService.GetAsync(string.Empty)); + } + + [Fact] + public async Task WorkflowService_GetByNameAsync_With_Empty() + { + await Assert.ThrowsAsync(() => WorkflowService.GetByNameAsync(string.Empty)); + } + + [Fact] + public async Task WorkflowService_CreateAsync_With_Empty() + { + await Assert.ThrowsAsync(() => WorkflowService.CreateAsync(null)); + } + [Fact] public async Task WorkflowService_WorkflowExists_ReturnsWorkflowId() { @@ -88,5 +106,39 @@ public async Task WorkflowService_WorkflowExists_ReturnsWorkflowId() Assert.Equal(workflowRevision.WorkflowId, result); } + + [Fact] + public async Task WorkflowService_DeleteWorkflow_With_Empty() + { + await Assert.ThrowsAsync(() => WorkflowService.DeleteWorkflowAsync(null)); + } + + [Fact] + public async Task WorkflowService_DeleteWorkflow_Calls_SoftDelete() + { + var result = await WorkflowService.DeleteWorkflowAsync(new WorkflowRevision()); + _workflowRepository.Verify(r => r.SoftDeleteWorkflow(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task WorkflowService_Count_Calls_Count() + { + var result = await WorkflowService.CountAsync(); + _workflowRepository.Verify(r => r.CountAsync(), Times.Once()); + } + + [Fact] + public async Task WorkflowService_GetCountByAeTitleAsync_Calls_Count() + { + var result = await WorkflowService.GetCountByAeTitleAsync("string"); + _workflowRepository.Verify(r => r.GetCountByAeTitleAsync(It.IsAny()), Times.Once()); + } + + [Fact] + public async Task WorkflowService_GetAllAsync_Calls_GetAllAsync() + { + var result = await WorkflowService.GetAllAsync(1, 2); + _workflowRepository.Verify(r => r.GetAllAsync(It.IsAny(), It.IsAny()), Times.Once()); + } } } diff --git a/tests/UnitTests/TaskManager.Argo.Tests/ArgoClientTest.cs b/tests/UnitTests/TaskManager.Argo.Tests/ArgoClientTest.cs new file mode 100644 index 000000000..4d9716573 --- /dev/null +++ b/tests/UnitTests/TaskManager.Argo.Tests/ArgoClientTest.cs @@ -0,0 +1,280 @@ +/* + * Copyright 2022 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.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Argo; +using Moq; +using Moq.Protected; +using Xunit; + +namespace Monai.Deploy.WorkflowManager.TaskManager.Argo.Tests +{ + public class ArgoClientTest + { + [Fact(DisplayName = "Argo_DeleteWorkflowTemplateAsync - Calls Send Async With Delete")] + public async Task Argo_DeleteWorkflowTemplateAsync() + { + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK }); + + var httpclient = new HttpClient(mockHttpMessageHandler.Object); + + ArgoClient argoClient = new(httpclient); + + var result = await argoClient.Argo_DeleteWorkflowTemplateAsync("test", "test", new CancellationToken(false)); + + Assert.True(result); + } + + [Fact(DisplayName = "Argo_GetVersionAsync - Calls Send Async With Get")] + public async Task Argo_GetVersionAsync() + { + var ver = new Argo.Version() + { + BuildDate = "date", + Compiler = "compilse", + GitCommit = "commit", + GitTag = "tag", + GitTreeState = "state", + GoVersion = "gover", + }; + var data = Newtonsoft.Json.JsonConvert.SerializeObject(ver); + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StreamContent(stream) }); + + var httpclient = new HttpClient(mockHttpMessageHandler.Object); + + ArgoClient argoClient = new(httpclient); + + var result = await argoClient.Argo_GetVersionAsync(); + + Assert.NotNull(result); + Assert.Equal(ver.GitCommit, result.GitCommit); + Assert.Equal(ver.BuildDate, result.BuildDate); + Assert.Equal(ver.Compiler, result.Compiler); + Assert.Equal(ver.GitTag, result.GitTag); + Assert.Equal(ver.GitCommit, result.GitCommit); + Assert.Equal(ver.GitTreeState, result.GitTreeState); + } + + [Fact(DisplayName = "Argo_CreateWorkflowAsync")] + public async Task Argo_CreateWorkflowAsync() + { + var data = "{ metadata:{}, spec: {} }"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StreamContent(stream) }); + + var httpclient = new HttpClient(mockHttpMessageHandler.Object); + + ArgoClient argoClient = new(httpclient); + + var result = await argoClient.Argo_CreateWorkflowAsync( + "argo", + new WorkflowCreateRequest { Namespace = "argo", Workflow = new Workflow() }, + CancellationToken.None); + + Assert.NotNull(result); + } + + [Fact(DisplayName = "Argo_GetWorkflowAsync")] + public async Task Argo_GetWorkflowAsync() + { + var data = "{ metadata:{}, spec: {} }"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StreamContent(stream) }); + + var httpclient = new HttpClient(mockHttpMessageHandler.Object); + + ArgoClient argoClient = new(httpclient); + + var result = await argoClient.Argo_GetWorkflowAsync("argo", "name", "version", "fields", + CancellationToken.None); + + Assert.NotNull(result); + } + + [Fact(DisplayName = "Argo_StopWorkflowAsync")] + public async Task Argo_StopWorkflowAsync() + { + var data = "{ metadata:{}, spec: {} }"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StreamContent(stream) }); + + var httpclient = new HttpClient(mockHttpMessageHandler.Object); + + ArgoClient argoClient = new(httpclient); + + var result = await argoClient.Argo_StopWorkflowAsync("argo", "name", + new WorkflowStopRequest { Namespace = "argo" }); + + Assert.NotNull(result); + } + + [Fact(DisplayName = "Argo_TerminateWorkflowAsync")] + public async Task Argo_TerminateWorkflowAsync() + { + var data = "{ metadata:{}, spec: {} }"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StreamContent(stream) }); + + var httpclient = new HttpClient(mockHttpMessageHandler.Object); + + ArgoClient argoClient = new(httpclient); + + var result = await argoClient.Argo_TerminateWorkflowAsync("argo", "name", + new WorkflowTerminateRequest { Namespace = "argo" }); + + Assert.NotNull(result); + } + + [Fact(DisplayName = "Argo_GetWorkflowTemplateAsync")] + public async Task Argo_GetWorkflowTemplateAsync() + { + var data = "{ metadata:{}, spec: {} }"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StreamContent(stream) }); + + var httpclient = new HttpClient(mockHttpMessageHandler.Object); + + ArgoClient argoClient = new(httpclient); + + var result = await argoClient.Argo_GetWorkflowTemplateAsync("argo", "name", "options"); + + Assert.NotNull(result); + } + + [Fact(DisplayName = "Argo_Get_WorkflowLogsAsync")] + public async Task Argo_Get_WorkflowLogsAsync() + { + var data = "{ metadata:{}, spec: {} }"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StreamContent(stream) }); + + var httpclient = new HttpClient(mockHttpMessageHandler.Object); + + ArgoClient argoClient = new(httpclient); + + var result = await argoClient.Argo_Get_WorkflowLogsAsync("argo", "name", "pod", "options"); + + Assert.NotNull(result); + } + + [Fact(DisplayName = "Argo_CreateWorkflowTemplateAsync")] + public async Task Argo_CreateWorkflowTemplateAsync() + { + var data = "{ metadata:{}, spec: {} }"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var mockHttpMessageHandler = new Mock(); + mockHttpMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StreamContent(stream) }); + + var httpclient = new HttpClient(mockHttpMessageHandler.Object); + + ArgoClient argoClient = new(httpclient); + + var result = await argoClient.Argo_CreateWorkflowTemplateAsync("argo", + new WorkflowTemplateCreateRequest { Namespace = "argo" }, + CancellationToken.None); + + Assert.NotNull(result); + } + + [Fact(DisplayName = "DecodeLogs")] + public async Task DecodeLogs() + { + var result = BaseArgoClient.DecodeLogs(""); + + Assert.NotNull(result); + } + + [Fact(DisplayName = "ConvertToString")] + public async Task ConvertToString() + { + //bool + var result = ArgoClient.ConvertToString(true, CultureInfo.InvariantCulture); + Assert.Equal("true", result); + + //enum + result = ArgoClient.ConvertToString(MyEnum.Zero, CultureInfo.InvariantCulture); + Assert.Equal("0", result); + + //enum + result = ArgoClient.ConvertToString(null, CultureInfo.InvariantCulture); + Assert.Equal("", result); + + + //byte array + var data = new byte[3]; + data[0] = byte.MinValue; + data[1] = 0; + data[2] = byte.MaxValue; + result = ArgoClient.ConvertToString(data, CultureInfo.InvariantCulture); + Assert.Equal(Convert.ToBase64String(data), result); + + //string array + var data2 = new string[3]; + data2[0] = "1"; + data2[1] = "2"; + data2[2] = "3"; + result = ArgoClient.ConvertToString(data2, CultureInfo.InvariantCulture); + Assert.Equal(string.Join(",", data2), result); + } + + enum MyEnum + { + Zero + } + } +} + diff --git a/tests/UnitTests/TaskManager.Argo.Tests/ArgoPluginTest.cs b/tests/UnitTests/TaskManager.Argo.Tests/ArgoPluginTest.cs index a0d4f32b6..c65029509 100755 --- a/tests/UnitTests/TaskManager.Argo.Tests/ArgoPluginTest.cs +++ b/tests/UnitTests/TaskManager.Argo.Tests/ArgoPluginTest.cs @@ -25,16 +25,11 @@ using k8s; using k8s.Autorest; using k8s.Models; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Monai.Deploy.Messaging.Configuration; using Monai.Deploy.Messaging.Events; -using Monai.Deploy.TaskManager.API; -using Monai.Deploy.WorkflowManager.Configuration; using Monai.Deploy.WorkflowManager.SharedTest; using Monai.Deploy.WorkflowManager.TaskManager.API; -using Monai.Deploy.WorkflowManager.TaskManager.API.Models; using Monai.Deploy.WorkflowManager.TaskManager.Argo.StaticValues; using Moq; using Moq.Language.Flow; @@ -45,88 +40,23 @@ namespace Monai.Deploy.WorkflowManager.TaskManager.Argo.Tests; -public class ArgoPluginTest +public class ArgoPluginTest : ArgoPluginTestBase { private readonly Mock> _logger; - private readonly Mock _serviceScopeFactory; - private readonly Mock _serviceScope; - private readonly Mock _kubernetesProvider; - private readonly Mock _argoProvider; - private readonly Mock _argoClient; - private readonly Mock _kubernetesClient; - private readonly Mock _taskDispatchEventService; - private readonly Mock _k8sCoreOperations; - private readonly IOptions _options; private Workflow? _submittedArgoTemplate; - private readonly int _argoTtlStatergySeconds = 360; - private readonly int _minAgoTtlStatergySeconds = 30; - private readonly string _initContainerCpuLimit = "100m"; - private readonly string _initContainerMemoryLimit = "200Mi"; - private readonly string _waitContainerCpuLimit = "200m"; - private readonly string _waitContainerMemoryLimit = "300Mi"; - private readonly string _messageGeneratorContainerCpuLimit = "300m"; - private readonly string _messageGeneratorContainerMemoryLimit = "400Mi"; - private readonly string _messageSenderContainerCpuLimit = "400m"; - private readonly string _messageSenderContainerMemoryLimit = "500Mi"; public ArgoPluginTest() { _logger = new Mock>(); - _serviceScopeFactory = new Mock(); - _serviceScope = new Mock(); - _kubernetesProvider = new Mock(); - _taskDispatchEventService = new Mock(); - _argoProvider = new Mock(); - _argoClient = new Mock(); - _kubernetesClient = new Mock(); - _k8sCoreOperations = new Mock(); - - _options = Options.Create(new WorkflowManagerOptions()); - _options.Value.Messaging.PublisherSettings.Add("endpoint", "1.2.2.3/virtualhost"); - _options.Value.Messaging.PublisherSettings.Add("username", "username"); - _options.Value.Messaging.PublisherSettings.Add("password", "password"); - _options.Value.Messaging.PublisherSettings.Add("exchange", "exchange"); - _options.Value.Messaging.PublisherSettings.Add("virtualHost", "vhost"); - _options.Value.Messaging.Topics.TaskCallbackRequest = "md.tasks.callback"; - _options.Value.ArgoTtlStrategyFailureSeconds = _argoTtlStatergySeconds; - _options.Value.ArgoTtlStrategySuccessSeconds = _argoTtlStatergySeconds; - _options.Value.MinArgoTtlStrategySeconds = _minAgoTtlStatergySeconds; - _options.Value.TaskManager.ArgoPluginArguments.InitContainerCpuLimit = _initContainerCpuLimit; - _options.Value.TaskManager.ArgoPluginArguments.InitContainerMemoryLimit = _initContainerMemoryLimit; - _options.Value.TaskManager.ArgoPluginArguments.WaitContainerCpuLimit = _waitContainerCpuLimit; - _options.Value.TaskManager.ArgoPluginArguments.WaitContainerMemoryLimit = _waitContainerMemoryLimit; - _options.Value.TaskManager.ArgoPluginArguments.MessageGeneratorContainerCpuLimit = _messageGeneratorContainerCpuLimit; - _options.Value.TaskManager.ArgoPluginArguments.MessageGeneratorContainerMemoryLimit = _messageGeneratorContainerMemoryLimit; - _options.Value.TaskManager.ArgoPluginArguments.MessageSenderContainerCpuLimit = _messageSenderContainerCpuLimit; - _options.Value.TaskManager.ArgoPluginArguments.MessageSenderContainerMemoryLimit = _messageSenderContainerMemoryLimit; - - _serviceScopeFactory.Setup(p => p.CreateScope()).Returns(_serviceScope.Object); - - var serviceProvider = new Mock(); - serviceProvider - .Setup(x => x.GetService(typeof(IKubernetesProvider))) - .Returns(_kubernetesProvider.Object); - serviceProvider - .Setup(x => x.GetService(typeof(IArgoProvider))) - .Returns(_argoProvider.Object); - serviceProvider - .Setup(x => x.GetService(typeof(ITaskDispatchEventService))) - .Returns(_taskDispatchEventService.Object); - - _serviceScope.Setup(x => x.ServiceProvider).Returns(serviceProvider.Object); _logger.Setup(p => p.IsEnabled(It.IsAny())).Returns(true); - _argoProvider.Setup(p => p.CreateClient(It.IsAny(), It.IsAny(), true)).Returns(_argoClient.Object); - _kubernetesProvider.Setup(p => p.CreateClient()).Returns(_kubernetesClient.Object); - _taskDispatchEventService.Setup(p => p.UpdateTaskPluginArgsAsync(It.IsAny(), It.IsAny>())).ReturnsAsync(new TaskDispatchEventInfo(new TaskDispatchEvent())); - _kubernetesClient.SetupGet(p => p.CoreV1).Returns(_k8sCoreOperations.Object); } [Fact(DisplayName = "Throws when missing required plug-in arguments")] public void ArgoPlugin_ThrowsWhenMissingPluginArguments() { var message = GenerateTaskDispatchEvent(); - Assert.Throws(() => new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message)); + Assert.Throws(() => new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message)); } [Fact(DisplayName = "Throws when missing required settings")] @@ -134,16 +64,16 @@ public void ArgoPlugin_ThrowsWhenMissingRequiredSettings() { var message = GenerateTaskDispatchEventWithValidArguments(); - _options.Value.Messaging.PublisherSettings.Remove("password"); - Assert.Throws(() => new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message)); + Options.Value.Messaging.PublisherSettings.Remove("password"); + Assert.Throws(() => new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message)); foreach (var key in Keys.RequiredSettings.Take(Keys.RequiredSettings.Count - 1)) { message.TaskPluginArguments.Add(key, Guid.NewGuid().ToString()); - Assert.Throws(() => new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message)); + Assert.Throws(() => new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message)); } message.TaskPluginArguments[Keys.RequiredSettings[Keys.RequiredSettings.Count - 1]] = Guid.NewGuid().ToString(); - Assert.Throws(() => new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message)); + Assert.Throws(() => new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message)); } [Fact(DisplayName = "Initializes values")] @@ -151,7 +81,7 @@ public void ArgoPlugin_InitializesValues() { var message = GenerateTaskDispatchEventWithValidArguments(); - _ = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + _ = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); _logger.VerifyLogging($"Argo plugin initialized: namespace=namespace, base URL=http://api-endpoint/, activeDeadlineSeconds=50, apiToken configured=true. allowInsecure=true", LogLevel.Information, Times.Once()); } @@ -160,9 +90,9 @@ public async Task ArgoPlugin_ExecuteTask_ReturnsExecutionStatusOnFailure() { var message = GenerateTaskDispatchEventWithValidArguments(); - _argoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(GenerateWorkflowTemplate(message)); - _argoClient.Setup(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new Exception("error")); SetupKubbernetesSecrets() @@ -172,15 +102,15 @@ public async Task ArgoPlugin_ExecuteTask_ReturnsExecutionStatusOnFailure() }); SetupKubernetesDeleteSecret(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Failed, result.Status); Assert.Equal(FailureReason.PluginError, result.FailureReason); Assert.Equal("error", result.Errors); - _argoClient.Verify(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - _k8sCoreOperations.Verify(p => p.CreateNamespacedSecretWithHttpMessagesAsync( + ArgoClient.Verify(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + K8sCoreOperations.Verify(p => p.CreateNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -191,7 +121,7 @@ public async Task ArgoPlugin_ExecuteTask_ReturnsExecutionStatusOnFailure() It.IsAny()), Times.Exactly(3)); await runner.DisposeAsync().ConfigureAwait(false); - _k8sCoreOperations.Verify(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( + K8sCoreOperations.Verify(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -209,22 +139,22 @@ public async Task ArgoPlugin_ExecuteTask_ReturnsExecutionStatusWhenFailedToGener { var message = GenerateTaskDispatchEventWithValidArguments(); - _argoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(GenerateWorkflowTemplate(message)); SetupKubbernetesSecrets() .Throws(new Exception("error")); SetupKubernetesDeleteSecret(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Failed, result.Status); Assert.Equal(FailureReason.PluginError, result.FailureReason); Assert.Equal("error", result.Errors); - _argoClient.Verify(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); - _k8sCoreOperations.Verify(p => p.CreateNamespacedSecretWithHttpMessagesAsync( + ArgoClient.Verify(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + K8sCoreOperations.Verify(p => p.CreateNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -235,7 +165,7 @@ public async Task ArgoPlugin_ExecuteTask_ReturnsExecutionStatusWhenFailedToGener It.IsAny()), Times.Once()); await runner.DisposeAsync().ConfigureAwait(false); - _k8sCoreOperations.Verify(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( + K8sCoreOperations.Verify(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -253,18 +183,18 @@ public async Task ArgoPlugin_ExecuteTask_ReturnsExecutionStatusWhenFailedToLoadW { var message = GenerateTaskDispatchEventWithValidArguments(); - _argoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new Exception("error")); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Failed, result.Status); Assert.Equal(FailureReason.PluginError, result.FailureReason); Assert.Equal("error", result.Errors); - _argoClient.Verify(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); - _k8sCoreOperations.Verify(p => p.CreateNamespacedSecretWithHttpMessagesAsync( + ArgoClient.Verify(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + K8sCoreOperations.Verify(p => p.CreateNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -275,7 +205,7 @@ public async Task ArgoPlugin_ExecuteTask_ReturnsExecutionStatusWhenFailedToLoadW It.IsAny()), Times.Never()); await runner.DisposeAsync().ConfigureAwait(false); - _k8sCoreOperations.Verify(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( + K8sCoreOperations.Verify(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -295,21 +225,21 @@ public async Task ArgoPlugin_ExecuteTask_ReturnsExecutionStatusWhenFailedToLocat var argoTemplate = GenerateWorkflowTemplate(message); argoTemplate.Spec.Entrypoint = "missing-template"; - _argoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(argoTemplate); SetupKubbernetesSecrets().Throws(new Exception("error")); SetupKubernetesDeleteSecret(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Failed, result.Status); Assert.Equal(FailureReason.PluginError, result.FailureReason); Assert.Equal($"Template '{argoTemplate.Spec.Entrypoint}' cannot be found in the referenced WorkflowTmplate '{message.TaskPluginArguments[Keys.WorkflowTemplateName]}'.", result.Errors); - _argoClient.Verify(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); - _k8sCoreOperations.Verify(p => p.CreateNamespacedSecretWithHttpMessagesAsync( + ArgoClient.Verify(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); + K8sCoreOperations.Verify(p => p.CreateNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -320,7 +250,7 @@ public async Task ArgoPlugin_ExecuteTask_ReturnsExecutionStatusWhenFailedToLocat It.IsAny()), Times.Never()); await runner.DisposeAsync().ConfigureAwait(false); - _k8sCoreOperations.Verify(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( + K8sCoreOperations.Verify(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -343,12 +273,12 @@ public async Task ArgoPlugin_ExecuteTask_WorkflowTemplates(string filename, int var message = GenerateTaskDispatchEventWithValidArguments(withoutDefaultArguments); message.TaskPluginArguments["resources"] = "{\"memory_reservation\": \"string\",\"cpu_reservation\": \"string\",\"gpu_limit\": 1,\"memory_limit\": \"string\",\"cpu_limit\": \"string\"}"; - message.TaskPluginArguments["priorityClass"] = "Helo"; + message.TaskPluginArguments["priority"] = "Helo"; Workflow? submittedArgoTemplate = null; - _argoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(argoTemplate); - _argoClient.Setup(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string ns, WorkflowCreateRequest body, CancellationToken cancellationToken) => { submittedArgoTemplate = body.Workflow; @@ -365,15 +295,15 @@ public async Task ArgoPlugin_ExecuteTask_WorkflowTemplates(string filename, int }); SetupKubernetesDeleteSecret(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Accepted, result.Status); Assert.Equal(FailureReason.None, result.FailureReason); Assert.Empty(result.Errors); - _argoClient.Verify(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); - _k8sCoreOperations.Verify(p => p.CreateNamespacedSecretWithHttpMessagesAsync( + ArgoClient.Verify(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + K8sCoreOperations.Verify(p => p.CreateNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -384,7 +314,7 @@ public async Task ArgoPlugin_ExecuteTask_WorkflowTemplates(string filename, int It.IsAny()), Times.Exactly(secretsCreated)); await runner.DisposeAsync().ConfigureAwait(false); - _k8sCoreOperations.Verify(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( + K8sCoreOperations.Verify(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -463,7 +393,7 @@ private static void ValidateSimpleTemplate(TaskDispatchEvent message, Workflow w public async Task ArgoPlugin_GetStatus_WaitUntilSucceededPhase() { var tryCount = 0; - _argoClient.Setup(p => p.Argo_GetWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string ns, string name, string version, string fields, CancellationToken cancellationToken) => { if (tryCount++ < 2) @@ -490,21 +420,21 @@ public async Task ArgoPlugin_GetStatus_WaitUntilSucceededPhase() var message = GenerateTaskDispatchEventWithValidArguments(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.GetStatus("identity", new TaskCallbackEvent(), CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Succeeded, result.Status); Assert.Equal(FailureReason.None, result.FailureReason); Assert.Empty(result.Errors); - _argoClient.Verify(p => p.Argo_GetWorkflowAsync(It.Is(p => p.Equals("namespace", StringComparison.OrdinalIgnoreCase)), It.Is(p => p.Equals("identity", StringComparison.OrdinalIgnoreCase)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + ArgoClient.Verify(p => p.Argo_GetWorkflowAsync(It.Is(p => p.Equals("namespace", StringComparison.OrdinalIgnoreCase)), It.Is(p => p.Equals("identity", StringComparison.OrdinalIgnoreCase)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); } [Fact(DisplayName = "GetStatus - Stats contains info")] public async Task ArgoPlugin_GetStatus_HasStatsInfo() { var tryCount = 0; - _argoClient.Setup(p => p.Argo_GetWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string ns, string name, string version, string fields, CancellationToken cancellationToken) => { if (tryCount++ < 2) @@ -537,7 +467,7 @@ public async Task ArgoPlugin_GetStatus_HasStatsInfo() var message = GenerateTaskDispatchEventWithValidArguments(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.GetStatus("identity", new TaskCallbackEvent(), CancellationToken.None).ConfigureAwait(false); var objNodeInfo = result?.Stats; @@ -550,14 +480,14 @@ public async Task ArgoPlugin_GetStatus_HasStatsInfo() Assert.Equal("{\"id\":\"firstId\"}", nodeInfo["nodes.first"]); Assert.Empty(result?.Errors); - _argoClient.Verify(p => p.Argo_GetWorkflowAsync(It.Is(p => p.Equals("namespace", StringComparison.OrdinalIgnoreCase)), It.Is(p => p.Equals("identity", StringComparison.OrdinalIgnoreCase)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + ArgoClient.Verify(p => p.Argo_GetWorkflowAsync(It.Is(p => p.Equals("namespace", StringComparison.OrdinalIgnoreCase)), It.Is(p => p.Equals("identity", StringComparison.OrdinalIgnoreCase)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); } [Fact(DisplayName = "GetStatus - Stats contains info")] public async Task ArgoPlugin_GetStatus_Argregates_stats() { var tryCount = 0; - _argoClient.Setup(p => p.Argo_GetWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string ns, string name, string version, string fields, CancellationToken cancellationToken) => { if (tryCount++ < 2) @@ -590,7 +520,7 @@ public async Task ArgoPlugin_GetStatus_Argregates_stats() var message = GenerateTaskDispatchEventWithValidArguments(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.GetStatus("identity", new TaskCallbackEvent(), CancellationToken.None).ConfigureAwait(false); var objNodeInfo = result?.Stats; @@ -604,7 +534,7 @@ public async Task ArgoPlugin_GetStatus_Argregates_stats() Assert.True(nodeInfo.ContainsKey("send-messagepodFinishTime1")); Assert.Equal("03/03/2023 08:00:32 +00:00", nodeInfo["podFinishTime0"]); - _argoClient.Verify(p => p.Argo_GetWorkflowAsync(It.Is(p => p.Equals("namespace", StringComparison.OrdinalIgnoreCase)), It.Is(p => p.Equals("identity", StringComparison.OrdinalIgnoreCase)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); + ArgoClient.Verify(p => p.Argo_GetWorkflowAsync(It.Is(p => p.Equals("namespace", StringComparison.OrdinalIgnoreCase)), It.Is(p => p.Equals("identity", StringComparison.OrdinalIgnoreCase)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(3)); } public static Dictionary ValiateCanConvertToDictionary(object obj) @@ -623,7 +553,7 @@ public static Dictionary ValiateCanConvertToDictionary(object ob [InlineData(Strings.ArgoPhasePending)] public async Task ArgoPlugin_GetStatus_ReturnsExecutionStatusOnSuccess(string phase) { - _argoClient.Setup(p => p.Argo_GetWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string ns, string name, string version, string fields, CancellationToken cancellationToken) => { return new Workflow @@ -638,7 +568,7 @@ public async Task ArgoPlugin_GetStatus_ReturnsExecutionStatusOnSuccess(string ph var message = GenerateTaskDispatchEventWithValidArguments(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.GetStatus("identity", new TaskCallbackEvent(), CancellationToken.None).ConfigureAwait(false); if (phase == Strings.ArgoPhaseSucceeded) @@ -660,25 +590,25 @@ public async Task ArgoPlugin_GetStatus_ReturnsExecutionStatusOnSuccess(string ph Assert.Equal($"Argo status = '{phase}'. Messages = 'error'.", result.Errors); } - _argoClient.Verify(p => p.Argo_GetWorkflowAsync(It.Is(p => p.Equals("namespace", StringComparison.OrdinalIgnoreCase)), It.Is(p => p.Equals("identity", StringComparison.OrdinalIgnoreCase)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + ArgoClient.Verify(p => p.Argo_GetWorkflowAsync(It.Is(p => p.Equals("namespace", StringComparison.OrdinalIgnoreCase)), It.Is(p => p.Equals("identity", StringComparison.OrdinalIgnoreCase)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Fact(DisplayName = "GetStatus - returns ExecutionStatus on failure")] public async Task ArgoPlugin_GetStatus_ReturnsExecutionStatusOnFailure() { - _argoClient.Setup(p => p.Argo_GetWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new Exception("error")); var message = GenerateTaskDispatchEventWithValidArguments(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.GetStatus("identity", new TaskCallbackEvent(), CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Failed, result.Status); Assert.Equal(FailureReason.PluginError, result.FailureReason); Assert.Equal("error", result.Errors); - _argoClient.Verify(p => p.Argo_GetWorkflowAsync(It.Is(p => p.Equals("namespace", StringComparison.OrdinalIgnoreCase)), It.Is(p => p.Equals("identity", StringComparison.OrdinalIgnoreCase)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + ArgoClient.Verify(p => p.Argo_GetWorkflowAsync(It.Is(p => p.Equals("namespace", StringComparison.OrdinalIgnoreCase)), It.Is(p => p.Equals("identity", StringComparison.OrdinalIgnoreCase)), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Fact(DisplayName = "ImagePullSecrets - get copied accross")] @@ -694,7 +624,7 @@ public async Task ArgoPlugin_Copies_ImagePullSecrets() SetUpSimpleArgoWorkFlow(argoTemplate); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Accepted, result.Status); @@ -711,11 +641,11 @@ public async Task ArgoPlugin_Ensures_TTL_Added_If_Not_present() var message = GenerateTaskDispatchEventWithValidArguments(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Accepted, result.Status); - Assert.Equal(_argoTtlStatergySeconds, _submittedArgoTemplate?.Spec.TtlStrategy?.SecondsAfterSuccess); + Assert.Equal(ArgoTtlStatergySeconds, _submittedArgoTemplate?.Spec.TtlStrategy?.SecondsAfterSuccess); } [Fact(DisplayName = "Argo Plugin adds required resource limits")] @@ -728,18 +658,18 @@ public async Task ArgoPlugin_Adds_Container_Resource_Restrictions_Based_On_Confi var message = GenerateTaskDispatchEventWithValidArguments(); - var expectedPodSpecPatch = "{\"initContainers\":[{\"name\":\"init\",\"resources\":{\"limits\":{\"cpu\":\"" + _initContainerCpuLimit + "\",\"memory\": \"" + - _initContainerMemoryLimit + + var expectedPodSpecPatch = "{\"initContainers\":[{\"name\":\"init\",\"resources\":{\"limits\":{\"cpu\":\"" + InitContainerCpuLimit + "\",\"memory\": \"" + + InitContainerMemoryLimit + "\"},\"requests\":{\"cpu\":\"0\",\"memory\":\"0Mi\"}}}],\"containers\":[{\"name\":\"wait\",\"resources\":{\"limits\":{\"cpu\":\"" + - _waitContainerCpuLimit + "\",\"memory\":\"" + _waitContainerMemoryLimit + + WaitContainerCpuLimit + "\",\"memory\":\"" + WaitContainerMemoryLimit + "\"},\"requests\":{\"cpu\":\"0\",\"memory\":\"0Mi\"}}}]}"; - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Accepted, result.Status); - Assert.Equal(_messageGeneratorContainerCpuLimit, _submittedArgoTemplate?.Spec.Templates.FirstOrDefault(p => p.Name == Strings.ExitHookTemplateSendTemplateName).Container.Resources.Limits["cpu"]); - Assert.Equal(_messageGeneratorContainerMemoryLimit, _submittedArgoTemplate?.Spec.Templates.FirstOrDefault(p => p.Name == Strings.ExitHookTemplateSendTemplateName).Container.Resources.Limits["memory"]); + Assert.Equal(MessageGeneratorContainerCpuLimit, _submittedArgoTemplate?.Spec.Templates.FirstOrDefault(p => p.Name == Strings.ExitHookTemplateSendTemplateName).Container.Resources.Limits["cpu"]); + Assert.Equal(MessageGeneratorContainerMemoryLimit, _submittedArgoTemplate?.Spec.Templates.FirstOrDefault(p => p.Name == Strings.ExitHookTemplateSendTemplateName).Container.Resources.Limits["memory"]); Assert.Equal(expectedPodSpecPatch, _submittedArgoTemplate?.Spec.Templates.FirstOrDefault(p => p.Name == Strings.ExitHookTemplateSendTemplateName).PodSpecPatch); } @@ -761,13 +691,13 @@ public async Task ArgoPlugin_Ensures_TTL_Extended_If_Too_Short(int? secondsAfter var message = GenerateTaskDispatchEventWithValidArguments(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Accepted, result.Status); if (secondsAfterCompletion is not null) { - Assert.Equal(Math.Max(_minAgoTtlStatergySeconds, secondsAfterCompletion.Value), _submittedArgoTemplate?.Spec.TtlStrategy.SecondsAfterCompletion); + Assert.Equal(Math.Max(MinAgoTtlStatergySeconds, secondsAfterCompletion.Value), _submittedArgoTemplate?.Spec.TtlStrategy.SecondsAfterCompletion); } else { @@ -776,7 +706,7 @@ public async Task ArgoPlugin_Ensures_TTL_Extended_If_Too_Short(int? secondsAfter if (secondsAfterSuccess is not null) { - Assert.Equal(Math.Max(_minAgoTtlStatergySeconds, secondsAfterSuccess.Value), _submittedArgoTemplate?.Spec.TtlStrategy.SecondsAfterSuccess); + Assert.Equal(Math.Max(MinAgoTtlStatergySeconds, secondsAfterSuccess.Value), _submittedArgoTemplate?.Spec.TtlStrategy.SecondsAfterSuccess); } else { @@ -785,7 +715,7 @@ public async Task ArgoPlugin_Ensures_TTL_Extended_If_Too_Short(int? secondsAfter if (secondsAfterFailure is not null) { - Assert.Equal(Math.Max(_minAgoTtlStatergySeconds, secondsAfterFailure.Value), _submittedArgoTemplate?.Spec.TtlStrategy.SecondsAfterFailure); + Assert.Equal(Math.Max(MinAgoTtlStatergySeconds, secondsAfterFailure.Value), _submittedArgoTemplate?.Spec.TtlStrategy.SecondsAfterFailure); } else { @@ -812,7 +742,7 @@ public async Task ArgoPlugin_Ensures_TTL_Remains(int? secondsAfterCompletion, in var message = GenerateTaskDispatchEventWithValidArguments(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Accepted, result.Status); @@ -833,7 +763,7 @@ public async Task ArgoPlugin_Ensures_podGC_is_removed() var message = GenerateTaskDispatchEventWithValidArguments(); - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, message); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); Assert.Equal(TaskExecutionStatus.Accepted, result.Status); @@ -844,7 +774,7 @@ public async Task ArgoPlugin_CreateArgoTemplate_Invalid_json_Throws_JsonSerializ { var template = "\"name\":\"fred\""; - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, new Messaging.Events.TaskDispatchEvent()); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, new Messaging.Events.TaskDispatchEvent()); await Assert.ThrowsAsync(async () => await runner.CreateArgoTemplate(template).ConfigureAwait(false)); } @@ -854,7 +784,7 @@ public async Task ArgoPlugin_CreateArgoTemplate_Invalid_Object_Throws() { var template = "@"; - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, new Messaging.Events.TaskDispatchEvent()); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, new Messaging.Events.TaskDispatchEvent()); await Assert.ThrowsAsync(async () => await runner.CreateArgoTemplate(template).ConfigureAwait(false)); } @@ -862,28 +792,90 @@ public async Task ArgoPlugin_CreateArgoTemplate_Invalid_Object_Throws() [Fact] public async Task ArgoPlugin_CreateArgoTemplate_Valid_json_Calls_Client() { - _argoClient.Setup(a => + ArgoClient.Setup(a => a.Argo_CreateWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(new WorkflowTemplate())); var template = "{\"name\":\"fred\"}"; - var runner = new ArgoPlugin(_serviceScopeFactory.Object, _logger.Object, _options, new Messaging.Events.TaskDispatchEvent()); + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, new Messaging.Events.TaskDispatchEvent()); await runner.CreateArgoTemplate(template).ConfigureAwait(false); - _argoClient.Verify(a => + ArgoClient.Verify(a => a.Argo_CreateWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } + [Fact(DisplayName = "podPriorityClassName gets set if not given in workflow")] + public async Task ArgoPlugin_Ensures_podPriorityClassName_is_set() + { + var argoTemplate = LoadArgoTemplate("SimpleTemplate.yml"); + Assert.NotNull(argoTemplate); + + SetUpSimpleArgoWorkFlow(argoTemplate); + + var message = GenerateTaskDispatchEventWithValidArguments(); + + WorkflowCreateRequest? requestMade = default; + ArgoClient.Setup(a => a.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((name, request, token) => + { + requestMade = request; + }); + + var defaultClassName = "standard"; + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); + var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); + + Assert.NotNull(requestMade); + Assert.Equal(defaultClassName, requestMade.Workflow.Spec.PodPriorityClassName); + + foreach (var template in requestMade.Workflow.Spec.Templates) + { + Assert.Equal(defaultClassName, template.PriorityClassName); + } + } + + [Fact(DisplayName = "podPriorityClassName gets set if given in workflow")] + public async Task ArgoPlugin_Ensures_podPriorityClassName_is_set_as_given() + { + var argoTemplate = LoadArgoTemplate("SimpleTemplate.yml"); + Assert.NotNull(argoTemplate); + + SetUpSimpleArgoWorkFlow(argoTemplate); + + var message = GenerateTaskDispatchEventWithValidArguments(); + + var givenClassName = "fred"; + message.TaskPluginArguments.Add("priority", givenClassName); + + WorkflowCreateRequest? requestMade = default; + ArgoClient.Setup(a => a.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((name, request, token) => + { + requestMade = request; + }); + + var runner = new ArgoPlugin(ServiceScopeFactory.Object, _logger.Object, Options, message); + var result = await runner.ExecuteTask(CancellationToken.None).ConfigureAwait(false); + + Assert.NotNull(requestMade); + Assert.Equal(givenClassName, requestMade.Workflow.Spec.PodPriorityClassName); + + foreach (var template in requestMade.Workflow.Spec.Templates) + { + Assert.Equal(givenClassName, template.PriorityClassName); + } + } + private void SetUpSimpleArgoWorkFlow(WorkflowTemplate argoTemplate) { Assert.NotNull(argoTemplate); - _argoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_GetWorkflowTemplateAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(argoTemplate); - _argoClient.Setup(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny())) + ArgoClient.Setup(p => p.Argo_CreateWorkflowAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((string ns, WorkflowCreateRequest body, CancellationToken cancellationToken) => { _submittedArgoTemplate = body.Workflow; @@ -1162,7 +1154,7 @@ private static TaskDispatchEvent GenerateTaskDispatchEvent() return deserializer.Deserialize(templateString); } - private ISetup>> SetupKubbernetesSecrets() => _k8sCoreOperations.Setup(p => p.CreateNamespacedSecretWithHttpMessagesAsync( + private ISetup>> SetupKubbernetesSecrets() => K8sCoreOperations.Setup(p => p.CreateNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), @@ -1172,7 +1164,7 @@ private ISetup>> SetupKu It.IsAny>>(), It.IsAny())); - private void SetupKubernetesDeleteSecret() => _k8sCoreOperations.Setup(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( + private void SetupKubernetesDeleteSecret() => K8sCoreOperations.Setup(p => p.DeleteNamespacedSecretWithHttpMessagesAsync( It.IsAny(), It.IsAny(), It.IsAny(), diff --git a/tests/UnitTests/TaskManager.Argo.Tests/ArgoPluginTestBase.cs b/tests/UnitTests/TaskManager.Argo.Tests/ArgoPluginTestBase.cs new file mode 100644 index 000000000..46febc91b --- /dev/null +++ b/tests/UnitTests/TaskManager.Argo.Tests/ArgoPluginTestBase.cs @@ -0,0 +1,105 @@ +/* + * Copyright 2022 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 k8s; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Monai.Deploy.Messaging.Events; +using Monai.Deploy.TaskManager.API; +using Monai.Deploy.WorkflowManager.Configuration; +using Monai.Deploy.WorkflowManager.TaskManager.API.Models; +using Moq; + +namespace Monai.Deploy.WorkflowManager.TaskManager.Argo.Tests +{ + public class ArgoPluginTestBase + { + protected readonly Mock ServiceScopeFactory; + protected readonly Mock ServiceScope; + protected readonly Mock KubernetesProvider; + protected readonly Mock ArgoClient; + protected readonly Mock KubernetesClient; + protected readonly Mock TaskDispatchEventService; + protected readonly IOptions Options; + protected readonly Mock K8sCoreOperations; + protected readonly Mock ArgoProvider; + protected readonly int ArgoTtlStatergySeconds = 360; + protected readonly int MinAgoTtlStatergySeconds = 30; + protected readonly string InitContainerCpuLimit = "100m"; + protected readonly string InitContainerMemoryLimit = "200Mi"; + protected readonly string WaitContainerCpuLimit = "200m"; + protected readonly string WaitContainerMemoryLimit = "300Mi"; + protected readonly string MessageGeneratorContainerCpuLimit = "300m"; + protected readonly string MessageGeneratorContainerMemoryLimit = "400Mi"; + protected readonly string MessageSenderContainerCpuLimit = "400m"; + protected readonly string MessageSenderContainerMemoryLimit = "500Mi"; + + public ArgoPluginTestBase() + { + Options = Microsoft.Extensions.Options.Options.Create(new WorkflowManagerOptions()); + + ArgoProvider = new Mock(); + K8sCoreOperations = new Mock(); + + ServiceScopeFactory = new Mock(); + ServiceScope = new Mock(); + KubernetesProvider = new Mock(); + TaskDispatchEventService = new Mock(); + ArgoClient = new Mock(); + KubernetesClient = new Mock(); + + ServiceScopeFactory.Setup(p => p.CreateScope()).Returns(ServiceScope.Object); + + var serviceProvider = new Mock(); + serviceProvider + .Setup(x => x.GetService(typeof(IKubernetesProvider))) + .Returns(KubernetesProvider.Object); + serviceProvider + .Setup(x => x.GetService(typeof(IArgoProvider))) + .Returns(ArgoProvider.Object); + serviceProvider + .Setup(x => x.GetService(typeof(ITaskDispatchEventService))) + .Returns(TaskDispatchEventService.Object); + + ServiceScope.Setup(x => x.ServiceProvider).Returns(serviceProvider.Object); + + KubernetesProvider.Setup(p => p.CreateClient()).Returns(KubernetesClient.Object); + TaskDispatchEventService.Setup(p => p.UpdateTaskPluginArgsAsync(It.IsAny(), It.IsAny>())).ReturnsAsync(new TaskDispatchEventInfo(new TaskDispatchEvent())); + KubernetesClient.SetupGet(p => p.CoreV1).Returns(K8sCoreOperations.Object); + ArgoProvider.Setup(p => p.CreateClient(It.IsAny(), It.IsAny(), true)).Returns(ArgoClient.Object); + + Options.Value.Messaging.PublisherSettings.Add("endpoint", "1.2.2.3/virtualhost"); + Options.Value.Messaging.PublisherSettings.Add("username", "username"); + Options.Value.Messaging.PublisherSettings.Add("password", "password"); + Options.Value.Messaging.PublisherSettings.Add("exchange", "exchange"); + Options.Value.Messaging.PublisherSettings.Add("virtualHost", "vhost"); + Options.Value.Messaging.Topics.TaskCallbackRequest = "md.tasks.callback"; + Options.Value.ArgoTtlStrategyFailureSeconds = ArgoTtlStatergySeconds; + Options.Value.ArgoTtlStrategySuccessSeconds = ArgoTtlStatergySeconds; + Options.Value.MinArgoTtlStrategySeconds = MinAgoTtlStatergySeconds; + Options.Value.TaskManager.ArgoPluginArguments.InitContainerCpuLimit = InitContainerCpuLimit; + Options.Value.TaskManager.ArgoPluginArguments.InitContainerMemoryLimit = InitContainerMemoryLimit; + Options.Value.TaskManager.ArgoPluginArguments.WaitContainerCpuLimit = WaitContainerCpuLimit; + Options.Value.TaskManager.ArgoPluginArguments.WaitContainerMemoryLimit = WaitContainerMemoryLimit; + Options.Value.TaskManager.ArgoPluginArguments.MessageGeneratorContainerCpuLimit = MessageGeneratorContainerCpuLimit; + Options.Value.TaskManager.ArgoPluginArguments.MessageGeneratorContainerMemoryLimit = MessageGeneratorContainerMemoryLimit; + Options.Value.TaskManager.ArgoPluginArguments.MessageSenderContainerCpuLimit = MessageSenderContainerCpuLimit; + Options.Value.TaskManager.ArgoPluginArguments.MessageSenderContainerMemoryLimit = MessageSenderContainerMemoryLimit; + } + } +} diff --git a/tests/UnitTests/TaskManager.Argo.Tests/ArgoProviderTest.cs b/tests/UnitTests/TaskManager.Argo.Tests/ArgoProviderTest.cs index 9af7679ef..9d2ff83d2 100755 --- a/tests/UnitTests/TaskManager.Argo.Tests/ArgoProviderTest.cs +++ b/tests/UnitTests/TaskManager.Argo.Tests/ArgoProviderTest.cs @@ -20,13 +20,11 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -using Argo; using Microsoft.Extensions.Logging; using Moq; using Moq.Protected; using Newtonsoft.Json; using Xunit; -using Version = Argo.Version; namespace Monai.Deploy.WorkflowManager.TaskManager.Argo.Tests { diff --git a/tests/UnitTests/TaskManager.Argo.Tests/Controller/TemplateControllerTests.cs b/tests/UnitTests/TaskManager.Argo.Tests/Controller/TemplateControllerTests.cs new file mode 100644 index 000000000..3637d068b --- /dev/null +++ b/tests/UnitTests/TaskManager.Argo.Tests/Controller/TemplateControllerTests.cs @@ -0,0 +1,190 @@ +/* + * Copyright 2022 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.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Argo; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Monai.Deploy.WorkflowManager.TaskManager.Argo; +using Monai.Deploy.WorkflowManager.TaskManager.Argo.Controllers; +using Monai.Deploy.WorkflowManager.TaskManager.Argo.Tests; +using Moq; +using Xunit; + +namespace Monai.Deploy.WorkflowManager.Test.Controllers +{ + public class TemplateControllerTests : ArgoPluginTestBase + { + private readonly Mock> _tempLogger; + private readonly Mock> _argoLogger; + + private TemplateController TemplateController { get; set; } + + public TemplateControllerTests() + { + _tempLogger = new Mock>(); + _argoLogger = new Mock>(); + } + + [Fact(DisplayName = "CreateArgoTemplate - ReturnsOk")] + public async Task CreateArgoTemplate_Controller_ReturnsOk() + { + var data = "{}"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Body = stream; + httpContext.Request.ContentLength = stream.Length; + var controllerContext = new ControllerContext() + { + HttpContext = httpContext + }; + + TemplateController = new TemplateController( + ServiceScopeFactory.Object, + _tempLogger.Object, + _argoLogger.Object, + Options) + { + ControllerContext = controllerContext + }; + + var result = await TemplateController.CreateArgoTemplate(); + + Assert.IsType>(result); + var okResult = Assert.IsType(result.Result); + Assert.Equal((int)HttpStatusCode.OK, okResult.StatusCode); + } + + [Fact(DisplayName = "CreateArgoTemplate - argo exception")] + public async Task CreateArgoTemplate_Controller_ReturnsBadRequestOnACeption() + { + var data = "{}"; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Body = stream; + httpContext.Request.ContentLength = stream.Length; + var controllerContext = new ControllerContext() + { + HttpContext = httpContext + }; + + TemplateController = new TemplateController( + ServiceScopeFactory.Object, + _tempLogger.Object, + _argoLogger.Object, + Options) + { + ControllerContext = controllerContext + }; + + ArgoClient.Setup(a => a.Argo_CreateWorkflowTemplateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())).ThrowsAsync(new FileNotFoundException()); + + var result = await TemplateController.CreateArgoTemplate(); + + var reqResult = Assert.IsType(result.Result); + Assert.Equal((int)HttpStatusCode.BadRequest, reqResult.StatusCode); + } + + [Fact(DisplayName = "CreateArgoTemplate - value is empty string")] + public async Task CreateArgoTemplate_Controller_EmptyString() + { + var data = ""; + var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(data)); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Body = stream; + httpContext.Request.ContentLength = stream.Length; + var controllerContext = new ControllerContext() + { + HttpContext = httpContext + }; + + TemplateController = new TemplateController( + ServiceScopeFactory.Object, + _tempLogger.Object, + _argoLogger.Object, + Options) + { + ControllerContext = controllerContext + }; + + var result = await TemplateController.CreateArgoTemplate(); + + var reqResult = Assert.IsType(result.Result); + Assert.Equal((int)HttpStatusCode.BadRequest, reqResult.StatusCode); + } + + [Fact(DisplayName = "DeleteArgoTemplate - ReturnsOk")] + public async Task DeleteArgoTemplate_Controller_ReturnsOk() + { + TemplateController = new TemplateController( + ServiceScopeFactory.Object, + _tempLogger.Object, + _argoLogger.Object, + Options); + + var result = await TemplateController.DeleteArgoTemplate("template"); + + Assert.IsType>(result); + var okResult = Assert.IsType(result.Result); + Assert.Equal((int)HttpStatusCode.OK, okResult.StatusCode); + } + + [Fact(DisplayName = "DeleteArgoTemplate - empty string")] + public async Task DeleteArgoTemplate_Controller_EmptyString() + { + TemplateController = new TemplateController( + ServiceScopeFactory.Object, + _tempLogger.Object, + _argoLogger.Object, + Options); + + var result = await TemplateController.DeleteArgoTemplate(""); + + var reqResult = Assert.IsType(result.Result); + Assert.Equal((int)HttpStatusCode.BadRequest, reqResult.StatusCode); + } + + [Fact(DisplayName = "DeleteArgoTemplate - badrequest on exception")] + public async Task DeleteArgoTemplate_Controller_Exception() + { + TemplateController = new TemplateController( + ServiceScopeFactory.Object, + _tempLogger.Object, + _argoLogger.Object, + Options); + + ArgoClient.Setup(a => a.Argo_DeleteWorkflowTemplateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())).ThrowsAsync(new FileNotFoundException()); + + var result = await TemplateController.DeleteArgoTemplate("template"); + + var reqResult = Assert.IsType(result.Result); + Assert.Equal((int)HttpStatusCode.BadRequest, reqResult.StatusCode); + } + } +} diff --git a/tests/UnitTests/TaskManager.Tests/TaskExecutionStatsRepositoryTests.cs b/tests/UnitTests/TaskManager.Tests/TaskExecutionStatsRepositoryTests.cs index 696b87d2f..727220940 100644 --- a/tests/UnitTests/TaskManager.Tests/TaskExecutionStatsRepositoryTests.cs +++ b/tests/UnitTests/TaskManager.Tests/TaskExecutionStatsRepositoryTests.cs @@ -114,7 +114,6 @@ public async Task TaskExecutionStatsRepository_Insert_Should_Check_For_Null_Even await Assert.ThrowsAsync(() => service.CreateAsync(default)); } - [Fact] public async Task TaskExecutionStatsRepository_update_Should_Check_For_Null_Event() { diff --git a/tests/UnitTests/WorkflowManager.Tests/Controllers/WorkflowsControllerTests.cs b/tests/UnitTests/WorkflowManager.Tests/Controllers/WorkflowsControllerTests.cs index 436e572f4..9084a82da 100644 --- a/tests/UnitTests/WorkflowManager.Tests/Controllers/WorkflowsControllerTests.cs +++ b/tests/UnitTests/WorkflowManager.Tests/Controllers/WorkflowsControllerTests.cs @@ -48,7 +48,7 @@ public class WorkflowsControllerTests public WorkflowsControllerTests() { - _options = Options.Create(new WorkflowManagerOptions()); + _options = Options.Create(new WorkflowManagerOptions { EndpointSettings = new EndpointSettings { MaxPageSize = 99 } }); _workflowService = new Mock(); _logger = new Mock>(); @@ -883,5 +883,68 @@ public async Task DeleteAsync_WorkflowsGivenInvalidId_ShouldBadRequest() const string expectedInstance = "/workflows"; Assert.StartsWith(expectedInstance, ((ProblemDetails)objectResult.Value).Instance); } + + [Fact] + public async Task GetByAeTitle_WorkflowsGivenEmptyTitle_ShouldBadRequest() + { + + var result = await WorkflowsController.GetByAeTitle(string.Empty, null); + + var objectResult = Assert.IsType(result); + Assert.Equal("Failed to validate title, not a valid AE title", result.As().Value.As().Detail); + + Assert.Equal(400, objectResult.StatusCode); + + const string expectedInstance = "/workflows"; + Assert.StartsWith(expectedInstance, ((ProblemDetails)objectResult.Value).Instance); + } + + [Fact] + public async Task GetByAeTitle_ShouldCall_GetByAeTitleAsync() + { + + var result = await WorkflowsController.GetByAeTitle("test", new PaginationFilter()); + var objectResult = Assert.IsType(result); + + _workflowService.Verify(x => x.GetByAeTitleAsync("test", It.IsAny(), It.IsAny()), Times.Once); + + Assert.Equal(200, objectResult.StatusCode); + } + + [Fact] + public async Task GetByAeTitle_ShouldCall_GetByAeTitleAsync_With_Skip() + { + + var result = await WorkflowsController.GetByAeTitle("test", new PaginationFilter { PageSize = 2, PageNumber = 2 }); + var objectResult = Assert.IsType(result); + + _workflowService.Verify(x => x.GetByAeTitleAsync("test", 2, It.IsAny()), Times.Once); + + Assert.Equal(200, objectResult.StatusCode); + } + + [Fact] + public async Task GetByAeTitle_ShouldCall_GetByAeTitleAsync_With_Limit() + { + + var result = await WorkflowsController.GetByAeTitle("test", new PaginationFilter { PageNumber = 2, PageSize = 45 }); + var objectResult = Assert.IsType(result); + + _workflowService.Verify(x => x.GetByAeTitleAsync("test", It.IsAny(), 45), Times.Once); + + Assert.Equal(200, objectResult.StatusCode); + } + + [Fact] + public async Task GetByAeTitle_ShouldCall_GetCountByAeTitleAsync() + { + + var result = await WorkflowsController.GetByAeTitle("test", new PaginationFilter()); + var objectResult = Assert.IsType(result); + + _workflowService.Verify(x => x.GetCountByAeTitleAsync("test"), Times.Once); + + Assert.Equal(200, objectResult.StatusCode); + } } } diff --git a/tests/UnitTests/WorkflowManager.Tests/Validators/WorkflowValidatorTests.cs b/tests/UnitTests/WorkflowManager.Tests/Validators/WorkflowValidatorTests.cs index 318962137..cb9138d53 100644 --- a/tests/UnitTests/WorkflowManager.Tests/Validators/WorkflowValidatorTests.cs +++ b/tests/UnitTests/WorkflowManager.Tests/Validators/WorkflowValidatorTests.cs @@ -17,6 +17,7 @@ using System; using System.Security.Cryptography.Xml; using System.Threading.Tasks; +using Amazon.Runtime.Internal.Transform; using Microsoft.Extensions.Logging; using Monai.Deploy.WorkflowManager.Common.Interfaces; using Monai.Deploy.WorkflowManager.Contracts.Models; @@ -535,5 +536,97 @@ public async Task ValidateWorkflow_ValidateWorkflow_ReturnsNoErrors() Assert.True(errors.Count == 0); } } + + [Fact] + public async Task ValidateWorkflow_Incorrect_podPriorityClassName_ReturnsErrors() + { + var workflow = new Workflow + { + Name = "Workflowname1", + Description = "Workflowdesc1", + Version = "1", + InformaticsGateway = new InformaticsGateway + { + AeTitle = "aetitle", + ExportDestinations = new string[] { "oneDestination", "twoDestination", "threeDestination" } + }, + Tasks = new TaskObject[] + { + new TaskObject + { + Args = new System.Collections.Generic.Dictionary{ + { "priority" ,"god" }, + { "workflow_template_name" ,"spot"} + }, + Id = "rootTask", + Type = "argo", + Description = "TestDesc", + Artifacts = new ArtifactMap + { + Input = new Artifact[]{ + new Artifact + { + Name = "non_unique_artifact", + Value = "Example Value" + } + } + } + }, + } + }; + + _workflowService.Setup(w => w.GetByNameAsync(It.IsAny())) + .ReturnsAsync(null, TimeSpan.FromSeconds(.1)); + + var errors = await _workflowValidator.ValidateWorkflow(workflow); + + Assert.Single(errors); + } + + [Fact] + public async Task ValidateWorkflow_correct_podPriorityClassName_ReturnsNoErrors() + { + var workflow = new Workflow + { + Name = "Workflowname1", + Description = "Workflowdesc1", + Version = "1", + InformaticsGateway = new InformaticsGateway + { + AeTitle = "aetitle", + ExportDestinations = new string[] { "oneDestination", "twoDestination", "threeDestination" } + }, + Tasks = new TaskObject[] + { + new TaskObject + { + Args = new System.Collections.Generic.Dictionary{ + { "priority" ,"high" }, + { "workflow_template_name" ,"spot"} + }, + Id = "rootTask", + Type = "argo", + Description = "TestDesc", + Artifacts = new ArtifactMap + { + Input = new Artifact[]{ + new Artifact + { + Name = "non_unique_artifact", + Value = "Example Value" + } + } + } + }, + } + }; + + _workflowService.Setup(w => w.GetByNameAsync(It.IsAny())) + .ReturnsAsync(null, TimeSpan.FromSeconds(.1)); + + var errors = await _workflowValidator.ValidateWorkflow(workflow); + + Assert.Empty(errors); + } } }