diff --git a/.gitignore b/.gitignore
index 7459e648dc..f8734414dc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -313,4 +313,5 @@ __pycache__/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-Tools/
\ No newline at end of file
+Tools/
+context/
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Actions/DockerExecution.cs b/src/VirtualClient/VirtualClient.Actions/DockerExecution.cs
new file mode 100644
index 0000000000..c374fad0b1
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Actions/DockerExecution.cs
@@ -0,0 +1,210 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient.Actions
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Logging;
+ using VirtualClient.Common;
+ using VirtualClient.Common.Docker;
+ using VirtualClient.Common.Extensions;
+ using VirtualClient.Common.Telemetry;
+ using VirtualClient.Contracts;
+
+ ///
+ /// Executes child components within a Docker container environment.
+ ///
+ [SupportedPlatforms("linux-x64,linux-arm64")]
+ internal class DockerExecution : VirtualClientComponentCollection
+ {
+ private ISystemManagement systemManager;
+ private DockerContainerClient dockerClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Provides all of the required dependencies to the Virtual Client component.
+ /// Parameters defined in the execution profile or supplied to the Virtual Client on the command line.
+ public DockerExecution(IServiceCollection dependencies, IDictionary parameters = null)
+ : base(dependencies, parameters)
+ {
+ this.systemManager = dependencies.GetService();
+ this.dockerClient = new DockerContainerClient(this.Logger);
+ }
+
+ ///
+ /// The Docker image to use for container execution.
+ ///
+ public string Image
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.Image));
+ }
+ }
+
+ ///
+ /// Optional volume mount paths. Format: "/host/path:/container/path".
+ ///
+ public string VolumeMounts
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.VolumeMounts), string.Empty);
+ }
+ }
+
+ ///
+ /// Optional environment variables for the container. Format: "VAR1=value1,VAR2=value2".
+ ///
+ public string EnvironmentVariables
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.EnvironmentVariables), string.Empty);
+ }
+ }
+
+ ///
+ /// Initializes Docker container execution requirements.
+ ///
+ protected override Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken)
+ {
+ // Validate Docker image is specified
+ if (string.IsNullOrWhiteSpace(this.Image))
+ {
+ throw new ArgumentException("Docker image (Image parameter) is required for DockerExecution.");
+ }
+
+ // Validate that at least one child component is defined
+ if (this.Count == 0)
+ {
+ throw new ArgumentException("DockerExecution must contain at least one child component.");
+ }
+
+ this.Logger?.LogInformation(
+ $"DockerExecution: Initializing container execution. Image={this.Image}, Components={this.Count}");
+
+ return base.InitializeAsync(telemetryContext, cancellationToken);
+ }
+
+ ///
+ /// Executes child components within the Docker container via docker exec.
+ ///
+ protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
+ {
+ this.Logger?.LogInformation(
+ $"DockerExecution: Starting container execution. Image={this.Image}");
+
+ // Get container ID from environment (set by DockerCommand)
+ string containerId = Environment.GetEnvironmentVariable("VC_DOCKER_CONTAINER_ID");
+
+ if (string.IsNullOrWhiteSpace(containerId))
+ {
+ this.Logger?.LogWarning(
+ "DockerExecution: No container ID found. Executing components on host (non-container mode).");
+
+ // Fallback: execute on host if no container context
+ await this.ExecuteComponentsOnHostAsync(cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ // Execute components inside container via docker exec
+ await this.ExecuteComponentsInContainerAsync(containerId, cancellationToken).ConfigureAwait(false);
+ }
+
+ this.Logger?.LogInformation($"DockerExecution: Container execution completed.");
+ }
+
+ ///
+ /// Cleans up Docker container resources.
+ ///
+ protected override Task CleanupAsync(EventContext telemetryContext, CancellationToken cancellationToken)
+ {
+ this.Logger?.LogInformation($"DockerExecution: Cleaning up container resources.");
+ return base.CleanupAsync(telemetryContext, cancellationToken);
+ }
+
+ ///
+ /// Executes components on the host (fallback mode).
+ ///
+ private async Task ExecuteComponentsOnHostAsync(CancellationToken cancellationToken)
+ {
+ foreach (VirtualClientComponent component in this)
+ {
+ this.Logger?.LogInformation(
+ $"DockerExecution: Executing child component on host. Component={component.GetType().Name}");
+
+ await component.ExecuteAsync(cancellationToken).ConfigureAwait(false);
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Executes components inside the Docker container.
+ ///
+ private async Task ExecuteComponentsInContainerAsync(string containerId, CancellationToken cancellationToken)
+ {
+ foreach (VirtualClientComponent component in this)
+ {
+ string componentName = component.GetType().Name;
+ this.Logger?.LogInformation(
+ $"DockerExecution: Executing child component in container. Component={componentName}, Container={containerId}");
+
+ try
+ {
+ // Build command to execute component in container
+ // Note: For MVP, we execute components individually
+ // Full implementation would handle parameters and profiles
+ string command = $"/app/VirtualClient.Main --component={componentName}";
+
+ this.Logger?.LogInformation($"DockerExecution: Running docker exec: {command}");
+
+ var execResult = await this.dockerClient.ExecuteInContainerAsync(
+ containerId,
+ command,
+ cancellationToken).ConfigureAwait(false);
+
+ // Capture output for logging and telemetry
+ if (!execResult.Success)
+ {
+ this.Logger?.LogError(
+ $"DockerExecution: Component execution failed in container. " +
+ $"Component={componentName}, ExitCode={execResult.ExitCode}, " +
+ $"Error={execResult.StandardError}");
+
+ // Emit telemetry event for error
+ this.Logger?.LogError($"Container Error Output: {execResult.StandardError}");
+
+ throw new InvalidOperationException(
+ $"Component {componentName} failed inside container. Exit code: {execResult.ExitCode}. " +
+ $"Error: {execResult.StandardError}");
+ }
+
+ if (!string.IsNullOrWhiteSpace(execResult.StandardOutput))
+ {
+ this.Logger?.LogInformation($"Container Output: {execResult.StandardOutput}");
+ }
+ }
+ catch (Exception ex)
+ {
+ this.Logger?.LogError($"Exception executing component in container: {ex.Message}");
+ throw;
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/VirtualClient/VirtualClient.Common.UnitTests/Docker/DockerContainerClientTests.cs b/src/VirtualClient/VirtualClient.Common.UnitTests/Docker/DockerContainerClientTests.cs
new file mode 100644
index 0000000000..ecccba022c
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Common.UnitTests/Docker/DockerContainerClientTests.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient.Common.Docker
+{
+ using System;
+ using System.Runtime.InteropServices;
+ using NUnit.Framework;
+
+ [TestFixture]
+ [Category("Unit")]
+ public class DockerContainerClientTests
+ {
+ [Test]
+ [TestCase("linux", "amd64", "", PlatformID.Unix, Architecture.X64)]
+ [TestCase("linux", "arm64", "", PlatformID.Unix, Architecture.Arm64)]
+ [TestCase("linux", "arm", "v8", PlatformID.Unix, Architecture.Arm64)]
+ [TestCase("linux", "arm", "v7", PlatformID.Unix, Architecture.Arm)]
+ [TestCase("linux", "386", "", PlatformID.Unix, Architecture.X86)]
+ [TestCase("linux", "x86_64", "", PlatformID.Unix, Architecture.X64)]
+ [TestCase("linux", "aarch64", "", PlatformID.Unix, Architecture.Arm64)]
+ [TestCase("windows", "amd64", "", PlatformID.Win32NT, Architecture.X64)]
+ public void ParsePlatformFromInspectJsonReturnsExpectedPlatformAndArchitecture(
+ string os, string arch, string variant, PlatformID expectedPlatform, Architecture expectedArch)
+ {
+ string json = $@"[{{""Os"":""{os}"",""Architecture"":""{arch}"",""Variant"":""{variant}""}}]";
+
+ var (platform, architecture) = DockerContainerClient.ParsePlatformFromInspectJson(json);
+
+ Assert.AreEqual(expectedPlatform, platform);
+ Assert.AreEqual(expectedArch, architecture);
+ }
+
+ [Test]
+ public void ParsePlatformFromInspectJsonThrowsOnUnsupportedOs()
+ {
+ string json = @"[{""Os"":""freebsd"",""Architecture"":""amd64"",""Variant"":""""}]";
+
+ Assert.Throws(() =>
+ DockerContainerClient.ParsePlatformFromInspectJson(json));
+ }
+
+ [Test]
+ public void ParsePlatformFromInspectJsonThrowsOnUnsupportedArchitecture()
+ {
+ string json = @"[{""Os"":""linux"",""Architecture"":""mips"",""Variant"":""""}]";
+
+ Assert.Throws(() =>
+ DockerContainerClient.ParsePlatformFromInspectJson(json));
+ }
+
+ [Test]
+ public void ParsePlatformFromInspectJsonThrowsOnInvalidJson()
+ {
+ Assert.Throws(() =>
+ DockerContainerClient.ParsePlatformFromInspectJson("not json"));
+ }
+
+ [Test]
+ public void ParsePlatformFromInspectJsonThrowsOnEmptyArray()
+ {
+ Assert.Throws(() =>
+ DockerContainerClient.ParsePlatformFromInspectJson("[]"));
+ }
+
+ [Test]
+ public void ParsePlatformFromInspectJsonHandlesRealUbuntuInspectOutput()
+ {
+ // Mirrors actual 'docker image inspect ubuntu:24.04' JSON structure
+ string json = @"[{
+ ""Id"": ""sha256:abc123"",
+ ""Os"": ""linux"",
+ ""Architecture"": ""amd64"",
+ ""Variant"": """",
+ ""Config"": {}
+ }]";
+
+ var (platform, architecture) = DockerContainerClient.ParsePlatformFromInspectJson(json);
+
+ Assert.AreEqual(PlatformID.Unix, platform);
+ Assert.AreEqual(Architecture.X64, architecture);
+ }
+ }
+}
diff --git a/src/VirtualClient/VirtualClient.Common/Docker/DockerContainerClient.cs b/src/VirtualClient/VirtualClient.Common/Docker/DockerContainerClient.cs
new file mode 100644
index 0000000000..fabc31c31a
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Common/Docker/DockerContainerClient.cs
@@ -0,0 +1,344 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient.Common.Docker
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Linq;
+ using System.Runtime.InteropServices;
+ using System.Text;
+ using System.Text.Json.Nodes;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Extensions.Logging;
+ using VirtualClient.Common.Extensions;
+
+ ///
+ /// Manages Docker container operations: creation, execution, cleanup.
+ ///
+ public class DockerContainerClient
+ {
+ private const string DockerCommand = "docker";
+ private readonly ILogger logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DockerContainerClient(ILogger logger)
+ {
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// Checks if Docker is available and running on the system.
+ ///
+ public async Task IsDockerAvailableAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var result = await this.ExecuteDockerCommandAsync("version", null, cancellationToken).ConfigureAwait(false);
+ return result.ExitCode == 0;
+ }
+ catch (Exception ex)
+ {
+ this.logger?.LogWarning($"Docker availability check failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Inspects a Docker image and returns the container platform and architecture.
+ ///
+ public async Task<(PlatformID Platform, Architecture Architecture)> InspectImageAsync(
+ string imageName,
+ CancellationToken cancellationToken)
+ {
+ this.LogDocker($"Inspecting Docker image platform: {imageName}");
+
+ var result = await this.ExecuteDockerCommandAsync($"image inspect {imageName}", null, cancellationToken).ConfigureAwait(false);
+
+ if (result.ExitCode != 0)
+ {
+ throw new InvalidOperationException(
+ $"Failed to inspect Docker image '{imageName}'. Error: {result.StandardError}");
+ }
+
+ return DockerContainerClient.ParsePlatformFromInspectJson(result.StandardOutput);
+ }
+
+ ///
+ /// Parses the output of 'docker image inspect' JSON to determine the container platform and architecture.
+ ///
+ public static (PlatformID Platform, Architecture Architecture) ParsePlatformFromInspectJson(string inspectJson)
+ {
+ JsonArray array;
+
+ try
+ {
+ array = JsonNode.Parse(inspectJson)?.AsArray()
+ ?? throw new ArgumentException("Invalid docker inspect JSON output.");
+ }
+ catch (System.Text.Json.JsonException ex)
+ {
+ throw new ArgumentException("Invalid docker inspect JSON output.", ex);
+ }
+
+ if (array.Count == 0)
+ {
+ throw new ArgumentException("Docker inspect output is empty.");
+ }
+
+ var root = array[0]
+ ?? throw new ArgumentException("Docker inspect output is empty.");
+
+ string os = root["Os"]?.GetValue() ?? string.Empty;
+ string arch = root["Architecture"]?.GetValue() ?? string.Empty;
+ string variant = root["Variant"]?.GetValue() ?? string.Empty;
+
+ PlatformID platform = os.ToLowerInvariant() switch
+ {
+ "linux" => PlatformID.Unix,
+ "windows" => PlatformID.Win32NT,
+ _ => throw new NotSupportedException($"Unsupported container OS: '{os}'")
+ };
+
+ Architecture architecture = arch.ToLowerInvariant() switch
+ {
+ "amd64" or "x86_64" => Architecture.X64,
+ "arm64" or "aarch64" => Architecture.Arm64,
+ "arm" when variant.ToLowerInvariant() == "v8" => Architecture.Arm64,
+ "arm" => Architecture.Arm,
+ "386" or "i386" => Architecture.X86,
+ _ => throw new NotSupportedException($"Unsupported architecture: '{arch}'")
+ };
+
+ return (platform, architecture);
+ }
+
+ ///
+ /// Creates a Docker container from the specified image with volume mounts.
+ ///
+ public async Task CreateContainerAsync(
+ string image,
+ IDictionary volumeMounts,
+ IDictionary environmentVariables,
+ CancellationToken cancellationToken)
+ {
+ // Build docker run command with volume mounts and environment variables
+ var arguments = new List { "run", "-d" };
+
+ // Add volume mounts
+ if (volumeMounts != null && volumeMounts.Count > 0)
+ {
+ foreach (var mount in volumeMounts)
+ {
+ arguments.Add("-v");
+ arguments.Add($"{mount.Key}:{mount.Value}");
+ }
+ }
+
+ // Add environment variables
+ if (environmentVariables != null && environmentVariables.Count > 0)
+ {
+ foreach (var env in environmentVariables)
+ {
+ arguments.Add("-e");
+ arguments.Add($"{env.Key}={env.Value}");
+ }
+ }
+
+ // Keep container running by default (tail -f /dev/null)
+ arguments.Add(image);
+ arguments.Add("tail");
+ arguments.Add("-f");
+ arguments.Add("/dev/null");
+
+ var argumentsString = string.Join(" ", arguments.Select(a => $"\"{a}\""));
+ this.LogDocker($"Creating Docker container: docker {argumentsString}");
+
+ var result = await this.ExecuteDockerCommandAsync(string.Join(" ", arguments), null, cancellationToken).ConfigureAwait(false);
+
+ if (result.ExitCode != 0)
+ {
+ throw new InvalidOperationException(
+ $"Failed to create Docker container from image '{image}'. Error: {result.StandardError}");
+ }
+
+ string containerId = result.StandardOutput?.Trim();
+ if (string.IsNullOrWhiteSpace(containerId))
+ {
+ throw new InvalidOperationException("Docker container creation succeeded but no container ID was returned.");
+ }
+
+ this.LogDocker($"Docker container created successfully. Container ID: {containerId}");
+ return containerId;
+ }
+
+ ///
+ /// Executes a command inside a running Docker container.
+ ///
+ public async Task ExecuteInContainerAsync(
+ string containerId,
+ string command,
+ CancellationToken cancellationToken)
+ {
+ var arguments = $"exec {containerId} {command}";
+
+ this.LogDocker($"Executing command in container {containerId}: {command}");
+
+ var result = await this.ExecuteDockerCommandAsync(arguments, null, cancellationToken).ConfigureAwait(false);
+
+ return new DockerExecResult
+ {
+ ExitCode = result.ExitCode,
+ StandardOutput = result.StandardOutput,
+ StandardError = result.StandardError,
+ Success = result.ExitCode == 0
+ };
+ }
+
+ ///
+ /// Stops a running Docker container.
+ ///
+ public async Task StopContainerAsync(string containerId, CancellationToken cancellationToken)
+ {
+ try
+ {
+ this.LogDocker($"Stopping Docker container: {containerId}");
+ var result = await this.ExecuteDockerCommandAsync($"stop {containerId}", null, cancellationToken).ConfigureAwait(false);
+ return result.ExitCode == 0;
+ }
+ catch (Exception ex)
+ {
+ this.logger?.LogWarning($"Failed to stop Docker container {containerId}: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Removes a Docker container.
+ ///
+ public async Task RemoveContainerAsync(string containerId, CancellationToken cancellationToken)
+ {
+ try
+ {
+ this.LogDocker($"Removing Docker container: {containerId}");
+ var result = await this.ExecuteDockerCommandAsync($"rm {containerId}", null, cancellationToken).ConfigureAwait(false);
+ return result.ExitCode == 0;
+ }
+ catch (Exception ex)
+ {
+ this.logger?.LogWarning($"Failed to remove Docker container {containerId}: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Logs docker-related information with yellow color.
+ ///
+ private void LogDocker(string message)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ this.logger?.LogInformation(message);
+ Console.ResetColor();
+ }
+
+ ///
+ /// Executes a docker command and returns the result.
+ ///
+ private async Task ExecuteDockerCommandAsync(
+ string arguments,
+ string workingDirectory,
+ CancellationToken cancellationToken)
+ {
+ var processInfo = new ProcessStartInfo
+ {
+ FileName = DockerCommand,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory
+ };
+
+ var process = new Process { StartInfo = processInfo };
+
+ try
+ {
+ process.Start();
+
+ var stdoutTask = process.StandardOutput.ReadToEndAsync();
+ var stderrTask = process.StandardError.ReadToEndAsync();
+
+ await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false);
+
+ // Wait for process exit asynchronously to satisfy AsyncFixer
+ while (!process.HasExited)
+ {
+ await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+ }
+
+ return new DockerCommandResult
+ {
+ ExitCode = process.ExitCode,
+ StandardOutput = stdoutTask.Result,
+ StandardError = stderrTask.Result
+ };
+ }
+ finally
+ {
+ process?.Dispose();
+ }
+ }
+
+ ///
+ /// Result of a Docker command execution.
+ ///
+ private class DockerCommandResult
+ {
+ ///
+ /// Gets or sets the exit code from the command execution.
+ ///
+ public int ExitCode { get; set; }
+
+ ///
+ /// Gets or sets the standard output from the command.
+ ///
+ public string StandardOutput { get; set; }
+
+ ///
+ /// Gets or sets the standard error from the command.
+ ///
+ public string StandardError { get; set; }
+ }
+ }
+
+ ///
+ /// Result of executing a command inside a Docker container.
+ ///
+ public class DockerExecResult
+ {
+ ///
+ /// Gets or sets the exit code from the command execution.
+ ///
+ public int ExitCode { get; set; }
+
+ ///
+ /// Gets or sets the standard output from the command.
+ ///
+ public string StandardOutput { get; set; }
+
+ ///
+ /// Gets or sets the standard error from the command.
+ ///
+ public string StandardError { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the command succeeded (exit code 0).
+ ///
+ public bool Success { get; set; }
+ }
+}
diff --git a/src/VirtualClient/VirtualClient.Contracts.UnitTests/VirtualClientComponentTests.cs b/src/VirtualClient/VirtualClient.Contracts.UnitTests/VirtualClientComponentTests.cs
index 561f0ddfb5..88a3acf888 100644
--- a/src/VirtualClient/VirtualClient.Contracts.UnitTests/VirtualClientComponentTests.cs
+++ b/src/VirtualClient/VirtualClient.Contracts.UnitTests/VirtualClientComponentTests.cs
@@ -724,6 +724,75 @@ public void VirtualClientComponentIsSupportedRespectsSupportedPlatformAttribute(
Assert.IsFalse(VirtualClientComponent.IsSupported(component));
}
+ [Test]
+ public void VirtualClientComponentPlatformReturnsContainerPlatformWhenDockerEnvVarSet()
+ {
+ this.mockFixture.Setup(PlatformID.Win32NT, System.Runtime.InteropServices.Architecture.X64);
+ TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);
+
+ try
+ {
+ Environment.SetEnvironmentVariable("VC_DOCKER_PLATFORM", "Unix");
+
+ Assert.AreEqual(PlatformID.Unix, component.Platform);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("VC_DOCKER_PLATFORM", null);
+ }
+ }
+
+ [Test]
+ public void VirtualClientComponentPlatformReturnsHostPlatformWhenDockerEnvVarNotSet()
+ {
+ this.mockFixture.Setup(PlatformID.Win32NT, System.Runtime.InteropServices.Architecture.X64);
+ TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);
+
+ Environment.SetEnvironmentVariable("VC_DOCKER_PLATFORM", null);
+
+ Assert.AreEqual(PlatformID.Win32NT, component.Platform);
+ }
+
+ [Test]
+ public void VirtualClientComponentCpuArchitectureReturnsContainerArchWhenDockerEnvVarSet()
+ {
+ this.mockFixture.Setup(PlatformID.Win32NT, System.Runtime.InteropServices.Architecture.X64);
+ TestVirtualClientComponent component = new TestVirtualClientComponent(this.mockFixture.Dependencies, this.mockFixture.Parameters);
+
+ try
+ {
+ Environment.SetEnvironmentVariable("VC_DOCKER_ARCH", "Arm64");
+
+ Assert.AreEqual(System.Runtime.InteropServices.Architecture.Arm64, component.CpuArchitecture);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("VC_DOCKER_ARCH", null);
+ }
+ }
+
+ [Test]
+ public void VirtualClientComponentIsSupportedUsesContainerPlatformWhenDockerEnvVarsSet()
+ {
+ // Windows host targeting a Linux container — component supports linux-x64 only
+ this.mockFixture.Setup(PlatformID.Win32NT, System.Runtime.InteropServices.Architecture.X64);
+ TestVirtualClientComponent2 component = new TestVirtualClientComponent2(this.mockFixture.Dependencies, this.mockFixture.Parameters);
+
+ try
+ {
+ Environment.SetEnvironmentVariable("VC_DOCKER_PLATFORM", "Unix");
+ Environment.SetEnvironmentVariable("VC_DOCKER_ARCH", "X64");
+
+ // linux-x64 is in [SupportedPlatforms("linux-x64,win-arm64,win-x64")]
+ Assert.IsTrue(component.IsSupported());
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("VC_DOCKER_PLATFORM", null);
+ Environment.SetEnvironmentVariable("VC_DOCKER_ARCH", null);
+ }
+ }
+
private class TestVirtualClientComponent : VirtualClientComponent
{
public TestVirtualClientComponent(VirtualClientComponent component)
diff --git a/src/VirtualClient/VirtualClient.Contracts/VirtualClientComponent.cs b/src/VirtualClient/VirtualClient.Contracts/VirtualClientComponent.cs
index cfd2d4352b..931bc6b464 100644
--- a/src/VirtualClient/VirtualClient.Contracts/VirtualClientComponent.cs
+++ b/src/VirtualClient/VirtualClient.Contracts/VirtualClientComponent.cs
@@ -99,14 +99,12 @@ protected VirtualClientComponent(IServiceCollection dependencies, IDictionary();
this.AgentId = this.systemInfo.AgentId;
- this.CpuArchitecture = this.systemInfo.CpuArchitecture;
this.Dependencies = dependencies;
this.ExperimentId = this.systemInfo.ExperimentId;
this.Logger = NullLogger.Instance;
this.Metadata = new Dictionary(StringComparer.OrdinalIgnoreCase);
this.MetadataContract = new MetadataContract();
this.PlatformSpecifics = this.systemInfo.PlatformSpecifics;
- this.Platform = this.systemInfo.Platform;
this.SupportedRoles = new List();
this.CleanupTasks = new List();
this.Extensions = new Dictionary();
@@ -192,9 +190,22 @@ public string ContentPathTemplate
}
///
- /// The CPU/processor architecture (e.g. amd64, arm).
+ /// The CPU/processor architecture (e.g. amd64, arm). Returns the container architecture when
+ /// running in Docker context (VC_DOCKER_ARCH env var set), otherwise returns host architecture.
///
- public Architecture CpuArchitecture { get; }
+ public Architecture CpuArchitecture
+ {
+ get
+ {
+ string envArch = Environment.GetEnvironmentVariable("VC_DOCKER_ARCH");
+ if (!string.IsNullOrEmpty(envArch) && Enum.TryParse(envArch, out Architecture containerArch))
+ {
+ return containerArch;
+ }
+
+ return this.systemInfo.CpuArchitecture;
+ }
+ }
///
/// True/false whether log files upload request processing should be deferred
@@ -442,9 +453,22 @@ protected set
public bool ParametersEvaluated { get; protected set; }
///
- /// The OS/system platform (e.g. Windows, Unix).
+ /// The OS/system platform (e.g. Windows, Unix). Returns the container platform when running
+ /// in Docker context (VC_DOCKER_PLATFORM env var set), otherwise returns host platform.
///
- public PlatformID Platform { get; }
+ public PlatformID Platform
+ {
+ get
+ {
+ string envPlatform = Environment.GetEnvironmentVariable("VC_DOCKER_PLATFORM");
+ if (!string.IsNullOrEmpty(envPlatform) && Enum.TryParse(envPlatform, out PlatformID containerPlatform))
+ {
+ return containerPlatform;
+ }
+
+ return this.systemInfo.Platform;
+ }
+ }
///
/// Provides OS/system platform specific information.
@@ -675,13 +699,14 @@ public IEnumerable Tags
///
/// The name of the platform/architecture for the system on which the application is
- /// running (e.g. linux-x64, linux-arm64, win-x64, win-arm64).
+ /// running (e.g. linux-x64, linux-arm64, win-x64, win-arm64). Returns the container
+ /// platform name when running in Docker context (VC_DOCKER_PLATFORM env var set).
///
protected string PlatformArchitectureName
{
get
{
- return this.PlatformSpecifics.PlatformArchitectureName;
+ return PlatformSpecifics.GetPlatformArchitectureName(this.Platform, this.CpuArchitecture);
}
}
diff --git a/src/VirtualClient/VirtualClient.Dependencies.UnitTests/DockerInstallationTests.cs b/src/VirtualClient/VirtualClient.Dependencies.UnitTests/DockerInstallationTests.cs
index 88cd90c416..a49011fc39 100644
--- a/src/VirtualClient/VirtualClient.Dependencies.UnitTests/DockerInstallationTests.cs
+++ b/src/VirtualClient/VirtualClient.Dependencies.UnitTests/DockerInstallationTests.cs
@@ -87,4 +87,4 @@ public TestDockerInstallation(IServiceCollection dependencies, IDictionary args, CancellationTok
CommandLineParser.CreateApiSubcommand(commandLineArgs, cancellationTokenSource),
CommandLineParser.CreateBootstrapSubcommand(commandLineArgs, cancellationTokenSource),
CommandLineParser.CreateConvertSubcommand(commandLineArgs, cancellationTokenSource),
+ CommandLineParser.CreateDockerSubcommand(commandLineArgs, cancellationTokenSource),
CommandLineParser.CreateGetTokenSubcommand(commandLineArgs, cancellationTokenSource),
CommandLineParser.CreateUploadFilesSubcommand(commandLineArgs, cancellationTokenSource),
CommandLineParser.CreateUploadTelemetrySubcommand(commandLineArgs, cancellationTokenSource)
@@ -468,6 +469,89 @@ private static Command CreateConvertSubcommand(string[] args, CancellationTokenS
return convertCommand;
}
+ private static Command CreateDockerSubcommand(string[] args, CancellationTokenSource cancellationTokenSource)
+ {
+ Command dockerCommand = new Command(
+ "docker",
+ "Executes a workload profile inside a Docker container.")
+ {
+ // REQUIRED
+ // -------------------------------------------------------------------
+ // --image
+ OptionFactory.CreateDockerImageOption(required: true),
+
+ // --profile
+ OptionFactory.CreateProfileOption(required: true),
+
+ // OPTIONAL
+ // -------------------------------------------------------------------
+ // --parameters
+ OptionFactory.CreateParametersOption(required: false),
+
+ // --timeout
+ OptionFactory.CreateTimeoutOption(required: false),
+
+ // --log-dir
+ OptionFactory.CreateLogDirectoryOption(required: false),
+
+ // --log-level
+ OptionFactory.CreateLogLevelOption(required: false, LogLevel.Information),
+
+ // --verbose
+ OptionFactory.CreateVerboseFlag(required: false, false),
+
+ // --keep-container-alive
+ OptionFactory.CreateKeepContainerAliveFlag(required: false, false),
+
+ // --logger
+ OptionFactory.CreateLoggerOption(required: false),
+
+ // --content
+ OptionFactory.CreateContentStoreOption(required: false),
+
+ // --client-id
+ OptionFactory.CreateClientIdOption(required: false, Environment.MachineName),
+
+ // --clean
+ OptionFactory.CreateCleanOption(required: false),
+
+ // --event-hub
+ OptionFactory.CreateEventHubStoreOption(required: false),
+
+ // --experiment-id
+ OptionFactory.CreateExperimentIdOption(required: false, Guid.NewGuid().ToString().ToLowerInvariant()),
+
+ // --iteration
+ OptionFactory.CreateIterationsOption(required: false),
+
+ // --key-vault
+ OptionFactory.CreateKeyVaultStoreOption(required: false),
+
+ // --metadata
+ OptionFactory.CreateMetadataOption(required: false),
+
+ // --layout
+ OptionFactory.CreateLayoutOption(required: false),
+
+ // --log-to-file
+ OptionFactory.CreateLogToFileFlag(required: false),
+
+ // --package-dir
+ OptionFactory.CreatePackageDirectoryOption(required: false),
+
+ // --scenarios
+ OptionFactory.CreateScenariosOption(required: false),
+
+ // --version
+ OptionFactory.CreateVersionOption(required: false)
+ };
+
+ dockerCommand.WithOptionValidation(args);
+ dockerCommand.Handler = CommandHandler.Create(cmd => cmd.ExecuteAsync(args, cancellationTokenSource));
+
+ return dockerCommand;
+ }
+
private static Command CreateGetTokenSubcommand(string[] args, CancellationTokenSource cancellationTokenSource)
{
Command getTokenCommand = new Command(
diff --git a/src/VirtualClient/VirtualClient.Main/DockerCommand.cs b/src/VirtualClient/VirtualClient.Main/DockerCommand.cs
new file mode 100644
index 0000000000..5727e247b4
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/DockerCommand.cs
@@ -0,0 +1,294 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Text.Json.Nodes;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Logging;
+ using VirtualClient.Common.Docker;
+ using VirtualClient.Common.Extensions;
+ using VirtualClient.Common.Telemetry;
+ using VirtualClient.Contracts;
+
+ ///
+ /// Command executes a workload profile inside a Docker container.
+ ///
+ internal class DockerCommand : ExecuteProfileCommand
+ {
+ private DockerContainerClient dockerClient;
+ private string containerId;
+
+ ///
+ /// The Docker image to use for container execution (e.g. ubuntu:noble, redis:7.0-alpine).
+ ///
+ public string DockerImage { get; set; }
+
+ ///
+ /// Whether to keep the container alive after execution for debugging.
+ ///
+ public bool KeepContainerAlive { get; set; }
+
+ ///
+ /// Initializes the command runtime before dependency initialization and execution.
+ ///
+ protected override void Initialize(string[] args, PlatformSpecifics platformSpecifics)
+ {
+ // Validate docker image is provided
+ if (string.IsNullOrWhiteSpace(this.DockerImage))
+ {
+ throw new ArgumentException("Docker image (--image) is required for docker command execution.");
+ }
+
+ // Validate that at least one profile is specified
+ if (this.Profiles == null || !this.Profiles.Any())
+ {
+ throw new ArgumentException("At least one profile (--profile) is required for docker command execution.");
+ }
+
+ // Set timeout to reasonable default if not specified
+ if (this.Timeout == null)
+ {
+ this.Timeout = new ProfileTiming(TimeSpan.FromMinutes(30));
+ }
+
+ // Call parent initialization
+ base.Initialize(args, platformSpecifics);
+ }
+
+ ///
+ /// Executes the docker command: creates container, runs profile, cleans up, then flushes telemetry.
+ ///
+ protected override async Task ExecuteAsync(string[] args, IServiceCollection dependencies, CancellationTokenSource cancellationTokenSource)
+ {
+ ILogger logger = dependencies.GetService();
+ this.dockerClient = new DockerContainerClient(logger);
+ CancellationToken cancellationToken = cancellationTokenSource.Token;
+ int exitCode = 0;
+
+ try
+ {
+ // Step 1: Check Docker availability
+ this.LogDockerInfo(logger, "Checking Docker availability...");
+ bool dockerAvailable = await this.dockerClient.IsDockerAvailableAsync(cancellationToken).ConfigureAwait(false);
+
+ if (!dockerAvailable)
+ {
+ throw new InvalidOperationException(
+ "Docker is not available. Please ensure Docker is installed and the daemon is running. " +
+ "Run 'docker version' to verify your Docker installation.");
+ }
+
+ this.LogDockerInfo(logger, "Docker is available and running.");
+
+ // Step 2: Validate profile — fail immediately if DockerExecution is in any action (double Docker)
+ await this.ValidateProfileForDockerSubcommandAsync(dependencies, cancellationToken).ConfigureAwait(false);
+
+ // Step 3: Create Docker container with volume mounts
+ this.LogDockerInfo(logger, $"Creating Docker container from image: {this.DockerImage}");
+
+ var volumeMounts = this.PrepareVolumeMounts(logger);
+ var environmentVariables = new Dictionary();
+
+ this.containerId = await this.dockerClient.CreateContainerAsync(
+ this.DockerImage,
+ volumeMounts,
+ environmentVariables,
+ cancellationToken).ConfigureAwait(false);
+
+ this.LogDockerInfo(logger, $"Docker container created successfully. Container ID: {this.containerId}");
+
+ // Step 4: Set container ID in environment for child components
+ Environment.SetEnvironmentVariable("VC_DOCKER_CONTAINER_ID", this.containerId);
+
+ // Step 5: Inspect image to detect container platform and set env vars for package resolution
+ this.LogDockerInfo(logger, $"Detecting container platform via: docker image inspect {this.DockerImage}");
+
+ var (containerPlatform, containerArch) = await this.dockerClient.InspectImageAsync(
+ this.DockerImage, cancellationToken).ConfigureAwait(false);
+
+ Environment.SetEnvironmentVariable("VC_DOCKER_PLATFORM", containerPlatform.ToString());
+ Environment.SetEnvironmentVariable("VC_DOCKER_ARCH", containerArch.ToString());
+
+ this.LogDockerInfo(logger,
+ $"Container platform detected: {containerPlatform}/{containerArch}. " +
+ $"Package resolution will use container platform.");
+
+ // Step 6: Install profile dependencies on host (packages are volume-mounted into container)
+ this.LogDockerInfo(logger, "Installing profile dependencies on host...");
+ this.InstallDependencies = true;
+ await base.ExecuteAsync(args, dependencies, cancellationTokenSource).ConfigureAwait(false);
+ this.InstallDependencies = false;
+
+ // Step 7: Execute profile actions inside container via docker exec
+ this.LogDockerInfo(logger, "Executing profile actions inside container...");
+ exitCode = await this.ExecuteActionsInContainerAsync(dependencies, logger, cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ // Step 8: Cleanup container
+ if (!string.IsNullOrWhiteSpace(this.containerId))
+ {
+ await this.CleanupContainerAsync(logger, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ return exitCode;
+ }
+
+ ///
+ /// Validates that none of the profile actions use DockerExecution, which conflicts with the docker subcommand.
+ ///
+ private async Task ValidateProfileForDockerSubcommandAsync(IServiceCollection dependencies, CancellationToken cancellationToken)
+ {
+ IEnumerable profilePaths = await this.EvaluateProfilesAsync(dependencies);
+
+ foreach (string profilePath in profilePaths)
+ {
+ if (!File.Exists(profilePath))
+ continue;
+
+ string json = await File.ReadAllTextAsync(profilePath, cancellationToken).ConfigureAwait(false);
+ JsonNode root = JsonNode.Parse(json);
+ JsonArray actions = root?["Actions"]?.AsArray();
+
+ if (actions == null)
+ continue;
+
+ foreach (JsonNode action in actions)
+ {
+ string type = action?["Type"]?.GetValue() ?? string.Empty;
+ if (type.Equals("DockerExecution", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException(
+ $"Profile '{Path.GetFileName(profilePath)}' contains a 'DockerExecution' component " +
+ $"which conflicts with the 'docker' subcommand (double Docker). " +
+ $"Remove DockerExecution from the profile when using the docker subcommand.");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Executes each action component from the profiles inside the container via docker exec.
+ ///
+ private async Task ExecuteActionsInContainerAsync(
+ IServiceCollection dependencies, ILogger logger, CancellationToken cancellationToken)
+ {
+ IEnumerable profilePaths = await this.EvaluateProfilesAsync(dependencies);
+
+ foreach (string profilePath in profilePaths)
+ {
+ if (!File.Exists(profilePath))
+ continue;
+
+ string json = await File.ReadAllTextAsync(profilePath, cancellationToken).ConfigureAwait(false);
+ JsonNode root = JsonNode.Parse(json);
+ JsonArray actions = root?["Actions"]?.AsArray();
+
+ if (actions == null)
+ continue;
+
+ foreach (JsonNode action in actions)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ return 1;
+
+ string componentType = action?["Type"]?.GetValue() ?? string.Empty;
+ if (string.IsNullOrEmpty(componentType))
+ continue;
+
+ this.LogDockerInfo(logger, $"Executing component in container: {componentType}");
+ string command = $"/app/VirtualClient.Main --component={componentType}";
+
+ var result = await this.dockerClient.ExecuteInContainerAsync(
+ this.containerId, command, cancellationToken).ConfigureAwait(false);
+
+ if (!result.Success)
+ {
+ throw new InvalidOperationException(
+ $"Component '{componentType}' failed in container. " +
+ $"Exit code: {result.ExitCode}. Error: {result.StandardError}");
+ }
+
+ if (!string.IsNullOrWhiteSpace(result.StandardOutput))
+ this.LogDockerInfo(logger, $"Container output: {result.StandardOutput}");
+ }
+ }
+
+ return 0;
+ }
+
+ ///
+ /// Prepares volume mounts for container.
+ ///
+ private Dictionary PrepareVolumeMounts(ILogger logger)
+ {
+ var volumeMounts = new Dictionary();
+ var currentDirectory = Environment.CurrentDirectory;
+
+ // Standard mount points
+ volumeMounts[Path.Combine(currentDirectory, "packages")] = "/mnt/packages";
+ volumeMounts[Path.Combine(currentDirectory, "logs")] = "/mnt/logs";
+ volumeMounts[Path.Combine(currentDirectory, "state")] = "/mnt/state";
+
+ this.LogDockerInfo(logger,
+ $"Volume mounts configured: packages, logs, state directories mounted at /mnt/");
+
+ return volumeMounts;
+ }
+
+ ///
+ /// Cleans up Docker container after execution.
+ ///
+ private async Task CleanupContainerAsync(ILogger logger, CancellationToken cancellationToken)
+ {
+ try
+ {
+ if (this.KeepContainerAlive)
+ {
+ this.LogDockerInfo(logger,
+ $"Container is being kept alive for debugging. Container ID: {this.containerId}. " +
+ $"To manually inspect: docker exec -it {this.containerId} bash. " +
+ $"To cleanup: docker stop {this.containerId} && docker rm {this.containerId}");
+ }
+ else
+ {
+ this.LogDockerInfo(logger, $"Stopping container: {this.containerId}");
+ await this.dockerClient.StopContainerAsync(this.containerId, cancellationToken).ConfigureAwait(false);
+
+ this.LogDockerInfo(logger, $"Removing container: {this.containerId}");
+ await this.dockerClient.RemoveContainerAsync(this.containerId, cancellationToken).ConfigureAwait(false);
+
+ this.LogDockerInfo(logger, "Container cleaned up successfully.");
+ }
+ }
+ catch (Exception ex)
+ {
+ logger?.LogWarning($"Container cleanup encountered an error: {ex.Message}");
+ if (!this.KeepContainerAlive)
+ {
+ logger?.LogWarning(
+ $"Manual cleanup may be needed. Container ID: {this.containerId}. " +
+ $"Run: docker stop {this.containerId} && docker rm {this.containerId}");
+ }
+ }
+ }
+
+ ///
+ /// Logs docker-related information with yellow color.
+ ///
+ private void LogDockerInfo(ILogger logger, string message)
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ logger?.LogInformation(message);
+ Console.ResetColor();
+ }
+ }
+}
diff --git a/src/VirtualClient/VirtualClient.Main/OptionFactory.cs b/src/VirtualClient/VirtualClient.Main/OptionFactory.cs
index 6eb71375c3..546abd22cc 100644
--- a/src/VirtualClient/VirtualClient.Main/OptionFactory.cs
+++ b/src/VirtualClient/VirtualClient.Main/OptionFactory.cs
@@ -1914,6 +1914,47 @@ private static ProfileTiming ParseProfileTimeout(ArgumentResult parsedResult)
return timing;
}
+ ///
+ /// Command line option defines the Docker image to use for container execution.
+ ///
+ /// Sets this option as required.
+ /// Sets the default value when none is provided.
+ public static Option CreateDockerImageOption(bool required = true, object defaultValue = null)
+ {
+ Option option = new Option(
+ new string[] { "--image" },
+ description: "The Docker image to use for container execution (e.g. ubuntu:noble, redis:7.0-alpine).")
+ {
+ Name = "DockerImage",
+ ArgumentHelpName = "image"
+ };
+
+ OptionFactory.SetOptionRequirements(option, required, defaultValue);
+
+ return option;
+ }
+
+ ///
+ /// Creates the '--keep-container-alive' option for the docker subcommand.
+ ///
+ /// Whether the option is required.
+ /// The default value for the option.
+ /// An option that handles keep container alive flag.
+ public static Option CreateKeepContainerAliveFlag(bool required = false, object defaultValue = null)
+ {
+ Option option = new Option(
+ new string[] { "--keep-container-alive" },
+ description: "Keeps the Docker container alive after execution for debugging purposes. Default behavior is to clean up the container.")
+ {
+ Name = "KeepContainerAlive",
+ IsRequired = false
+ };
+
+ option.SetDefaultValue(defaultValue ?? false);
+
+ return option;
+ }
+
private static Option SetOptionRequirements(Option option, bool required = false, object defaultValue = null, ValidateSymbol validator = null)
{
option.IsRequired = required;
diff --git a/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-DOCKER.json b/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-DOCKER.json
new file mode 100644
index 0000000000..5b4c25537f
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-DOCKER.json
@@ -0,0 +1,20 @@
+{
+ "Description": "Installs Docker Engine on Linux systems for container-based workloads.",
+ "Metadata": {
+ "RecommendedMinimumExecutionTime": "00:05:00",
+ "SupportedPlatforms": "linux-x64,linux-arm64",
+ "SupportedOperatingSystems": "Ubuntu"
+ },
+ "Parameters": {
+ "DockerVersion": ""
+ },
+ "Dependencies": [
+ {
+ "Type": "DockerInstallation",
+ "Parameters": {
+ "Scenario": "InstallDocker",
+ "Version": "$.Parameters.DockerVersion"
+ }
+ }
+ ]
+}
diff --git a/src/VirtualClient/VirtualClient.Main/profiles/PERF-OPENSSL-DOCKER.json b/src/VirtualClient/VirtualClient.Main/profiles/PERF-OPENSSL-DOCKER.json
new file mode 100644
index 0000000000..a42a4c94f6
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/profiles/PERF-OPENSSL-DOCKER.json
@@ -0,0 +1,44 @@
+{
+ "Description": "OpenSSL CPU performance benchmark running inside a Docker container.",
+ "Metadata": {
+ "RecommendedMinimumExecutionTime": "00:05:00",
+ "SupportedPlatforms": "linux-x64,linux-arm64",
+ "SupportedOperatingSystems": "Ubuntu"
+ },
+ "Parameters": {
+ "DockerImage": "ubuntu:noble",
+ "OpenSSLScenario": "SHA256"
+ },
+ "Actions": [
+ {
+ "Type": "DockerExecution",
+ "Parameters": {
+ "Image": "$.Parameters.DockerImage"
+ },
+ "Components": [
+ {
+ "Type": "OpenSslExecutor",
+ "Parameters": {
+ "Scenario": "$.Parameters.OpenSSLScenario",
+ "CommandArguments": "speed -elapsed -seconds 10 sha256",
+ "PackageName": "openssl",
+ "Tags": "CPU,OpenSSL,Cryptography,Docker"
+ }
+ }
+ ]
+ }
+ ],
+ "Dependencies": [
+ {
+ "Type": "DependencyPackageInstallation",
+ "Parameters": {
+ "Scenario": "InstallOpenSSLWorkloadPackage",
+ "BlobContainer": "packages",
+ "BlobName": "openssl.3.0.0.zip",
+ "PackageName": "openssl",
+ "Extract": true
+ }
+ }
+ ]
+}
+