Skip to content

Commit f7c40bc

Browse files
committed
add podman plugin
Signed-off-by: will tepe <will.tepe@cchmc.org>
1 parent 9e10e95 commit f7c40bc

22 files changed

Lines changed: 2281 additions & 3 deletions

src/Common/Miscellaneous/ValidationConstants.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ public enum NotificationValues
133133
/// </summary>
134134
public const string Email = "email";
135135

136+
/// <summary>
137+
/// Key for the podman task type.
138+
/// </summary>
139+
public const string PodmanTaskType = "podman";
140+
136141
public static readonly string[] AcceptableTasksToReview = { ArgoTaskType, ExternalAppTaskType };
137142

138143
/// <summary>
@@ -147,7 +152,8 @@ public enum NotificationValues
147152
DockerTaskType,
148153
Email,
149154
ExternalAppTaskType,
150-
HL7ExportTask
155+
HL7ExportTask,
156+
PodmanTaskType
151157
};
152158
}
153159
}

src/Monai.Deploy.WorkflowManager.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.WorkflowManage
7676
EndProject
7777
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.WorkflowManager.TaskManager.Docker.Tests", "..\tests\UnitTests\TaskManager.Docker.Tests\Monai.Deploy.WorkflowManager.TaskManager.Docker.Tests.csproj", "{BF6569A1-1A5A-4358-9C02-1A6A5F0FBFD9}"
7878
EndProject
79+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.WorkflowManager.TaskManager.Podman", "TaskManager\Plug-ins\Podman\Monai.Deploy.WorkflowManager.TaskManager.Podman.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
80+
EndProject
81+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.WorkflowManager.TaskManager.Podman.Tests", "..\tests\UnitTests\TaskManager.Podman.Tests\Monai.Deploy.WorkflowManager.TaskManager.Podman.Tests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
82+
EndProject
7983
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.WorkflowManager.Shared.Tests", "..\tests\UnitTests\Monai.Deploy.WorkflowManager.Shared.Tests\Monai.Deploy.WorkflowManager.Shared.Tests.csproj", "{C853A9E3-C53D-4B1A-BFDA-228689A8C94C}"
8084
EndProject
8185
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monai.Deploy.WorkflowManager.Common.Tests", "..\tests\UnitTests\Common.Tests\Monai.Deploy.WorkflowManager.Common.Tests.csproj", "{75A4AEDA-0386-4B2D-9DBA-BC9AE733660E}"
@@ -224,6 +228,14 @@ Global
224228
{BF6569A1-1A5A-4358-9C02-1A6A5F0FBFD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
225229
{BF6569A1-1A5A-4358-9C02-1A6A5F0FBFD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
226230
{BF6569A1-1A5A-4358-9C02-1A6A5F0FBFD9}.Release|Any CPU.Build.0 = Release|Any CPU
231+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
232+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
233+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
234+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
235+
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
236+
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
237+
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
238+
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
227239
{C853A9E3-C53D-4B1A-BFDA-228689A8C94C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
228240
{C853A9E3-C53D-4B1A-BFDA-228689A8C94C}.Debug|Any CPU.Build.0 = Debug|Any CPU
229241
{C853A9E3-C53D-4B1A-BFDA-228689A8C94C}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -290,6 +302,8 @@ Global
290302
{EFECF826-B036-4689-B223-D791CD2C0F10} = {71DDEE7B-E213-4E39-A7F4-4646783A27F7}
291303
{47C31FB9-C862-4770-83B6-E2DDF260CC67} = {541C5347-5D7D-44B7-95D3-B6FB3D9EB955}
292304
{BF6569A1-1A5A-4358-9C02-1A6A5F0FBFD9} = {71DDEE7B-E213-4E39-A7F4-4646783A27F7}
305+
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {541C5347-5D7D-44B7-95D3-B6FB3D9EB955}
306+
{B2C3D4E5-F6A7-8901-BCDE-F12345678901} = {71DDEE7B-E213-4E39-A7F4-4646783A27F7}
293307
{C853A9E3-C53D-4B1A-BFDA-228689A8C94C} = {71DDEE7B-E213-4E39-A7F4-4646783A27F7}
294308
{75A4AEDA-0386-4B2D-9DBA-BC9AE733660E} = {71DDEE7B-E213-4E39-A7F4-4646783A27F7}
295309
{0D40F7D6-3747-4280-8EB3-9F3A18AC1125} = {71DDEE7B-E213-4E39-A7F4-4646783A27F7}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.Runtime.CompilerServices;
18+
19+
[assembly: InternalsVisibleTo("Monai.Deploy.WorkflowManager.TaskManager.Podman.Tests")]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
namespace Monai.Deploy.WorkflowManager.TaskManager.Podman
18+
{
19+
internal class ContainerMonitorException : Exception
20+
{
21+
public ContainerMonitorException()
22+
{
23+
}
24+
25+
public ContainerMonitorException(string? message) : base(message)
26+
{
27+
}
28+
29+
public ContainerMonitorException(string? message, Exception? innerException) : base(message, innerException)
30+
{
31+
}
32+
}
33+
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.IO.Abstractions;
18+
using Ardalis.GuardClauses;
19+
using Docker.DotNet.Models;
20+
using Microsoft.AspNetCore.StaticFiles;
21+
using Microsoft.Extensions.DependencyInjection;
22+
using Microsoft.Extensions.Logging;
23+
using Microsoft.Extensions.Options;
24+
using Monai.Deploy.Messaging.API;
25+
using Monai.Deploy.Messaging.Events;
26+
using Monai.Deploy.Messaging.Messages;
27+
using Monai.Deploy.Storage.API;
28+
using Monai.Deploy.WorkflowManager.Common.Configuration;
29+
using Monai.Deploy.WorkflowManager.TaskManager.API;
30+
using Monai.Deploy.WorkflowManager.TaskManager.Podman.Logging;
31+
32+
namespace Monai.Deploy.WorkflowManager.TaskManager.Podman
33+
{
34+
public interface IContainerStatusMonitor
35+
{
36+
Task Start(TaskDispatchEvent taskDispatchEvent,
37+
TimeSpan containerTimeout,
38+
string containerId,
39+
ContainerVolumeMount intermediateVolumeMount,
40+
IReadOnlyList<ContainerVolumeMount> outputVolumeMounts,
41+
CancellationToken cancellationToken = default);
42+
}
43+
44+
public class ContainerStatusMonitor : IContainerStatusMonitor, IDisposable
45+
{
46+
private readonly IOptions<WorkflowManagerOptions> _options;
47+
private readonly IServiceScope _scope;
48+
private readonly ILogger<ContainerStatusMonitor> _logger;
49+
private readonly IFileSystem _fileSystem;
50+
private bool _disposedValue;
51+
52+
public ContainerStatusMonitor(
53+
IServiceScopeFactory serviceScopeFactory,
54+
ILogger<ContainerStatusMonitor> logger,
55+
IFileSystem fileSystem,
56+
IOptions<WorkflowManagerOptions> options)
57+
{
58+
if (serviceScopeFactory is null)
59+
{
60+
throw new ArgumentNullException(nameof(serviceScopeFactory));
61+
}
62+
63+
_scope = serviceScopeFactory.CreateScope();
64+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
65+
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
66+
_options = options ?? throw new ArgumentNullException(nameof(options));
67+
}
68+
69+
public async Task Start(
70+
TaskDispatchEvent taskDispatchEvent,
71+
TimeSpan containerTimeout,
72+
string containerId,
73+
ContainerVolumeMount intermediateVolumeMount,
74+
IReadOnlyList<ContainerVolumeMount> outputVolumeMounts,
75+
CancellationToken cancellationToken = default)
76+
{
77+
ArgumentNullException.ThrowIfNull(taskDispatchEvent, nameof(taskDispatchEvent));
78+
ArgumentNullException.ThrowIfNull(containerTimeout, nameof(containerTimeout));
79+
ArgumentNullException.ThrowIfNullOrWhiteSpace(containerId, nameof(containerId));
80+
81+
var podmanClientFactory = _scope.ServiceProvider.GetService<IPodmanClientFactory>() ?? throw new ServiceNotFoundException(nameof(IPodmanClientFactory));
82+
var dockerClient = podmanClientFactory.CreateClient(new Uri(taskDispatchEvent.TaskPluginArguments[Keys.BaseUrl]));
83+
84+
var pollingPeriod = TimeSpan.FromSeconds(1);
85+
86+
var timeToRetry = (int)containerTimeout.TotalSeconds;
87+
while (timeToRetry-- > 0)
88+
{
89+
try
90+
{
91+
var response = await dockerClient.Containers.InspectContainerAsync(containerId, cancellationToken).ConfigureAwait(false);
92+
93+
if (IsContainerCompleted(response.State))
94+
{
95+
await UploadOutputArtifacts(intermediateVolumeMount, outputVolumeMounts, cancellationToken).ConfigureAwait(false);
96+
await SendCallbackMessage(taskDispatchEvent, containerId).ConfigureAwait(false);
97+
return;
98+
}
99+
}
100+
catch (Exception ex)
101+
{
102+
_logger.ErrorMonitoringContainerStatus(containerId, ex);
103+
}
104+
finally
105+
{
106+
await Task.Delay(pollingPeriod, cancellationToken).ConfigureAwait(false);
107+
}
108+
}
109+
110+
_logger.TimedOutMonitoringContainerStatus(containerId);
111+
}
112+
113+
internal static bool IsContainerCompleted(ContainerState state)
114+
{
115+
if (Strings.DockerEndStates.Contains(state.Status, StringComparer.InvariantCultureIgnoreCase) &&
116+
!string.IsNullOrWhiteSpace(state.FinishedAt))
117+
{
118+
return true;
119+
}
120+
return false;
121+
}
122+
123+
private async Task UploadOutputArtifacts(ContainerVolumeMount intermediateVolumeMount, IReadOnlyList<ContainerVolumeMount> outputVolumeMounts, CancellationToken cancellationToken)
124+
{
125+
ArgumentNullException.ThrowIfNull(outputVolumeMounts, nameof(outputVolumeMounts));
126+
127+
var storageService = _scope.ServiceProvider.GetService<IStorageService>() ?? throw new ServiceNotFoundException(nameof(IStorageService));
128+
var contentTypeProvider = _scope.ServiceProvider.GetService<IContentTypeProvider>() ?? throw new ServiceNotFoundException(nameof(IContentTypeProvider));
129+
130+
if (intermediateVolumeMount is not null)
131+
{
132+
await UploadOutputArtifacts(storageService, contentTypeProvider, intermediateVolumeMount.Source, intermediateVolumeMount.TaskManagerPath, cancellationToken).ConfigureAwait(false);
133+
}
134+
135+
foreach (var output in outputVolumeMounts)
136+
{
137+
await UploadOutputArtifacts(storageService, contentTypeProvider, output.Source, output.TaskManagerPath, cancellationToken).ConfigureAwait(false);
138+
}
139+
}
140+
141+
private async Task UploadOutputArtifacts(IStorageService storageService, IContentTypeProvider contentTypeProvider, Messaging.Common.Storage destination, string artifactsPath, CancellationToken cancellationToken)
142+
{
143+
ArgumentNullException.ThrowIfNull(destination, nameof(destination));
144+
ArgumentNullException.ThrowIfNullOrWhiteSpace(artifactsPath, nameof(artifactsPath));
145+
146+
IEnumerable<string> files;
147+
try
148+
{
149+
files = _fileSystem.Directory.EnumerateFiles(artifactsPath, "*", SearchOption.AllDirectories);
150+
}
151+
catch (Exception ex)
152+
{
153+
throw new ContainerMonitorException("Directory doesn't exist or no permission to access the directory.", ex);
154+
}
155+
156+
if (!files.Any())
157+
{
158+
_logger.NoFilesFoundForUpload(artifactsPath);
159+
}
160+
foreach (var file in files)
161+
{
162+
try
163+
{
164+
var objectName = file.Replace(artifactsPath, string.Empty).TrimStart('/');
165+
objectName = _fileSystem.Path.Combine(destination.RelativeRootPath, objectName);
166+
_logger.UploadingFile(file, destination.Bucket, objectName);
167+
if (!contentTypeProvider.TryGetContentType(file, out var contentType))
168+
{
169+
contentType = GetContentType(_fileSystem.Path.GetExtension(file));
170+
}
171+
_logger.ContentTypeForFile(objectName, contentType);
172+
using var stream = _fileSystem.File.OpenRead(file);
173+
await storageService.PutObjectAsync(destination.Bucket, objectName, stream, stream.Length, contentType, new Dictionary<string, string>(), cancellationToken).ConfigureAwait(false);
174+
}
175+
catch (Exception ex)
176+
{
177+
_logger.ErrorUploadingFile(file, ex);
178+
}
179+
}
180+
}
181+
182+
private static string GetContentType(string? ext)
183+
{
184+
if (string.IsNullOrWhiteSpace(ext))
185+
{
186+
return Strings.MimeTypeUnknown;
187+
}
188+
189+
return ext.ToLowerInvariant() switch
190+
{
191+
Strings.FileExtensionDicom => Strings.MimeTypeDicom,
192+
_ => Strings.MimeTypeUnknown
193+
};
194+
}
195+
196+
private async Task SendCallbackMessage(TaskDispatchEvent taskDispatchEvent, string containerId)
197+
{
198+
ArgumentNullException.ThrowIfNull(taskDispatchEvent, nameof(taskDispatchEvent));
199+
ArgumentNullException.ThrowIfNullOrWhiteSpace(containerId, nameof(containerId));
200+
201+
_logger.SendingCallbackMessage(containerId);
202+
var message = new JsonMessage<TaskCallbackEvent>(new TaskCallbackEvent
203+
{
204+
CorrelationId = taskDispatchEvent.CorrelationId,
205+
ExecutionId = taskDispatchEvent.ExecutionId,
206+
Identity = containerId,
207+
Outputs = taskDispatchEvent.Outputs ?? new List<Messaging.Common.Storage>(),
208+
TaskId = taskDispatchEvent.TaskId,
209+
WorkflowInstanceId = taskDispatchEvent.WorkflowInstanceId,
210+
}, applicationId: Strings.ApplicationId, correlationId: taskDispatchEvent.CorrelationId);
211+
212+
var messageBrokerPublisherService = _scope.ServiceProvider.GetService<IMessageBrokerPublisherService>() ?? throw new ServiceNotFoundException(nameof(IMessageBrokerPublisherService));
213+
await messageBrokerPublisherService.Publish(_options.Value.Messaging.Topics.TaskCallbackRequest, message.ToMessage()).ConfigureAwait(false);
214+
}
215+
216+
protected virtual void Dispose(bool disposing)
217+
{
218+
if (!_disposedValue)
219+
{
220+
if (disposing)
221+
{
222+
_scope.Dispose();
223+
}
224+
225+
_disposedValue = true;
226+
}
227+
}
228+
229+
public void Dispose()
230+
{
231+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
232+
Dispose(disposing: true);
233+
GC.SuppressFinalize(this);
234+
}
235+
}
236+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2022 MONAI Consortium
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Ardalis.GuardClauses;
18+
19+
namespace Monai.Deploy.WorkflowManager.TaskManager.Podman
20+
{
21+
public class ContainerVolumeMount
22+
{
23+
public ContainerVolumeMount(Messaging.Common.Storage source, string containerPath, string hostPath, string taskManagerPath)
24+
{
25+
ArgumentNullException.ThrowIfNull(source, nameof(source));
26+
ArgumentNullException.ThrowIfNullOrWhiteSpace(containerPath, nameof(containerPath));
27+
ArgumentNullException.ThrowIfNullOrWhiteSpace(hostPath, nameof(hostPath));
28+
ArgumentNullException.ThrowIfNullOrWhiteSpace(taskManagerPath, nameof(taskManagerPath));
29+
30+
Source = source;
31+
ContainerPath = containerPath;
32+
HostPath = hostPath;
33+
TaskManagerPath = taskManagerPath;
34+
}
35+
36+
public Messaging.Common.Storage Source { get; }
37+
public string ContainerPath { get; }
38+
public string HostPath { get; }
39+
public string TaskManagerPath { get; }
40+
}
41+
}

0 commit comments

Comments
 (0)