diff --git a/src/VirtualClient/VirtualClient.Contracts/ContainerExecutionContext.cs b/src/VirtualClient/VirtualClient.Contracts/ContainerExecutionContext.cs
new file mode 100644
index 0000000000..e20579690d
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Contracts/ContainerExecutionContext.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient.Contracts
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Runtime.InteropServices;
+
+ ///
+ /// Tracks whether execution is happening inside a container context
+ /// and provides the effective platform for component execution.
+ ///
+ public class ContainerExecutionContext
+ {
+ private static ContainerExecutionContext current;
+
+ ///
+ /// Gets or sets the current container execution context.
+ ///
+ public static ContainerExecutionContext Current
+ {
+ get => current ??= new ContainerExecutionContext();
+ set => current = value;
+ }
+
+ ///
+ /// True if running with container mode enabled (--image was passed).
+ ///
+ public bool IsContainerMode { get; set; }
+
+ ///
+ /// The container image being used.
+ ///
+ public string Image { get; set; }
+
+ ///
+ /// The platform inside the container (typically Linux).
+ ///
+ public PlatformID ContainerPlatform { get; set; } = PlatformID.Unix;
+
+ ///
+ /// The CPU architecture inside the container.
+ ///
+ public Architecture ContainerArchitecture { get; set; } = Architecture.X64;
+
+ ///
+ /// Gets the effective platform - container platform if in container mode,
+ /// otherwise the host platform.
+ ///
+ public PlatformID EffectivePlatform => this.IsContainerMode
+ ? this.ContainerPlatform
+ : Environment.OSVersion.Platform;
+
+ ///
+ /// Gets the effective architecture.
+ ///
+ public Architecture EffectiveArchitecture => this.IsContainerMode
+ ? this.ContainerArchitecture
+ : RuntimeInformation.ProcessArchitecture;
+
+ ///
+ /// Container configuration from profile.
+ ///
+ public ContainerConfiguration Configuration { get; set; }
+ }
+
+ ///
+ /// Container configuration from profile's Container section.
+ ///
+ public class ContainerConfiguration
+ {
+ ///
+ /// Default image (can be overridden by --image CLI).
+ ///
+ public string Image { get; set; }
+
+ ///
+ /// Standard mount configuration.
+ ///
+ public ContainerMountConfig Mounts { get; set; } = new ContainerMountConfig();
+
+ ///
+ /// Working directory inside container.
+ ///
+ public string WorkingDirectory { get; set; } = "/vc";
+
+ ///
+ /// Environment variables to pass to container.
+ ///
+ public IDictionary EnvironmentVariables { get; set; }
+
+ ///
+ /// Additional mount paths beyond the defaults.
+ ///
+ public IList AdditionalMounts { get; set; }
+
+ ///
+ /// Pull policy: Always, IfNotPresent, Never.
+ ///
+ public string PullPolicy { get; set; } = "IfNotPresent";
+ }
+
+ ///
+ /// Standard VC directory mounts configuration.
+ ///
+ public class ContainerMountConfig
+ {
+ ///
+ /// Mount the packages directory (/vc/packages).
+ ///
+ public bool Packages { get; set; } = true;
+
+ ///
+ /// Mount the logs directory (/vc/logs).
+ ///
+ public bool Logs { get; set; } = true;
+
+ ///
+ /// Mount the state directory (/vc/state).
+ ///
+ public bool State { get; set; } = true;
+
+ ///
+ /// Mount the temp directory (/vc/temp).
+ ///
+ public bool Temp { get; set; } = true;
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Contracts/VirtualClientComponent.cs b/src/VirtualClient/VirtualClient.Contracts/VirtualClientComponent.cs
index 1e6c25f2d7..3b45d7594a 100644
--- a/src/VirtualClient/VirtualClient.Contracts/VirtualClientComponent.cs
+++ b/src/VirtualClient/VirtualClient.Contracts/VirtualClientComponent.cs
@@ -109,7 +109,7 @@ protected VirtualClientComponent(IServiceCollection dependencies, IDictionary(StringComparer.OrdinalIgnoreCase);
this.MetadataContract = new MetadataContract();
this.PlatformSpecifics = this.systemInfo.PlatformSpecifics;
- this.Platform = this.systemInfo.Platform;
+ // this.Platform = this.systemInfo.Platform;
this.SupportedRoles = new List();
this.CleanupTasks = new List();
this.Extensions = new Dictionary();
@@ -400,7 +400,19 @@ protected set
///
/// The OS/system platform (e.g. Windows, Unix).
///
- public PlatformID Platform { get; }
+ public PlatformID Platform
+ {
+ get
+ {
+ // If container mode is active, report container platform
+ if (ContainerExecutionContext.Current.IsContainerMode)
+ {
+ return ContainerExecutionContext.Current.ContainerPlatform;
+ }
+
+ return this.systemInfo.Platform;
+ }
+ }
///
/// Provides OS/system platform specific information.
@@ -637,6 +649,19 @@ protected string PlatformArchitectureName
{
get
{
+ // If container mode is active, report container platform/architecture
+ if (ContainerExecutionContext.Current.IsContainerMode)
+ {
+ string os = ContainerExecutionContext.Current.ContainerPlatform == PlatformID.Unix ? "linux" : "win";
+ string arch = ContainerExecutionContext.Current.ContainerArchitecture switch
+ {
+ Architecture.X64 => "x64",
+ Architecture.Arm64 => "arm64",
+ _ => "x64"
+ };
+ return $"{os}-{arch}";
+ }
+
return this.PlatformSpecifics.PlatformArchitectureName;
}
}
diff --git a/src/VirtualClient/VirtualClient.Core/ContainerAwareProcessManager.cs b/src/VirtualClient/VirtualClient.Core/ContainerAwareProcessManager.cs
new file mode 100644
index 0000000000..ae3f11bc2c
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Core/ContainerAwareProcessManager.cs
@@ -0,0 +1,258 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient
+{
+ using System;
+ using System.Collections.Specialized;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using VirtualClient.Common;
+ using VirtualClient.Contracts;
+
+ ///
+ /// Process manager that wraps commands in Docker when container mode is active.
+ ///
+ public class ContainerAwareProcessManager : ProcessManager
+ {
+ private readonly DockerRuntime dockerRuntime;
+ private readonly PlatformSpecifics platformSpecifics;
+ private readonly ProcessManager innerProcessManager;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public ContainerAwareProcessManager(
+ DockerRuntime dockerRuntime,
+ PlatformSpecifics platformSpecifics,
+ ProcessManager innerProcessManager)
+ {
+ this.dockerRuntime = dockerRuntime ?? throw new ArgumentNullException(nameof(dockerRuntime));
+ this.platformSpecifics = platformSpecifics ?? throw new ArgumentNullException(nameof(platformSpecifics));
+ this.innerProcessManager = innerProcessManager ?? throw new ArgumentNullException(nameof(innerProcessManager));
+ }
+
+ ///
+ public override PlatformID Platform => this.innerProcessManager.Platform;
+
+ ///
+ /// Creates a process. If in container mode, the process runs inside Docker.
+ ///
+ public override IProcessProxy CreateProcess(string command, string arguments = null, string workingDirectory = null)
+ {
+ if (ContainerExecutionContext.Current.IsContainerMode)
+ {
+ // Wrap in Docker execution
+ return new ContainerProcessProxy(
+ this.dockerRuntime,
+ ContainerExecutionContext.Current.Image,
+ command,
+ arguments,
+ workingDirectory,
+ ContainerExecutionContext.Current.Configuration,
+ this.platformSpecifics);
+ }
+
+ // Normal host execution - delegate to inner manager
+ return this.innerProcessManager.CreateProcess(command, arguments, workingDirectory);
+ }
+ }
+
+ ///
+ /// Process proxy that executes inside a container.
+ ///
+ public class ContainerProcessProxy : IProcessProxy
+ {
+ private readonly DockerRuntime runtime;
+ private readonly string image;
+ private readonly string command;
+ private readonly string arguments;
+ private readonly string workingDirectory;
+ private readonly ContainerConfiguration config;
+ private readonly PlatformSpecifics platformSpecifics;
+
+ private DockerRunResult result;
+ private bool hasStarted;
+ private DateTime startTime;
+ private DateTime exitTime;
+ private bool disposed;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public ContainerProcessProxy(
+ DockerRuntime runtime,
+ string image,
+ string command,
+ string arguments,
+ string workingDirectory,
+ ContainerConfiguration config,
+ PlatformSpecifics platformSpecifics)
+ {
+ this.runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
+ this.image = image ?? throw new ArgumentNullException(nameof(image));
+ this.command = command;
+ this.arguments = arguments;
+ this.workingDirectory = workingDirectory;
+ this.config = config;
+ this.platformSpecifics = platformSpecifics;
+
+ this.StandardOutput = new ConcurrentBuffer();
+ this.StandardError = new ConcurrentBuffer();
+ }
+
+ ///
+ public int Id => -1; // Container processes don't have a host PID
+
+ ///
+ public string Name => $"docker:{this.image}";
+
+ ///
+ public StringDictionary EnvironmentVariables => null;
+
+ ///
+ public int ExitCode => this.result?.ExitCode ?? -1;
+
+ ///
+ public DateTime ExitTime
+ {
+ get => this.exitTime;
+ set => this.exitTime = value;
+ }
+
+ ///
+ public IntPtr? Handle => null;
+
+ ///
+ public bool HasExited => this.result != null;
+
+ ///
+ public bool RedirectStandardError { get; set; } = true;
+
+ ///
+ public bool RedirectStandardInput { get; set; } = false;
+
+ ///
+ public bool RedirectStandardOutput { get; set; } = true;
+
+ ///
+ public ConcurrentBuffer StandardOutput { get; }
+
+ ///
+ public ConcurrentBuffer StandardError { get; }
+
+ ///
+ public StreamWriter StandardInput => null;
+
+ ///
+ public ProcessStartInfo StartInfo => new ProcessStartInfo
+ {
+ FileName = "docker",
+ Arguments = $"run {this.image} {this.command} {this.arguments}".Trim()
+ };
+
+ ///
+ public DateTime StartTime
+ {
+ get => this.startTime;
+ set => this.startTime = value;
+ }
+
+ ///
+ public void Close()
+ {
+ // Container is already removed with --rm flag
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ public void Kill()
+ {
+ // TODO: Implement docker stop/kill if needed
+ }
+
+ ///
+ public void Kill(bool entireProcessTree)
+ {
+ this.Kill();
+ }
+
+ ///
+ public bool Start()
+ {
+ this.hasStarted = true;
+ this.startTime = DateTime.UtcNow;
+ return true;
+ }
+
+ ///
+ public async Task WaitForExitAsync(CancellationToken cancellationToken, TimeSpan? timeout = null)
+ {
+ if (!this.hasStarted)
+ {
+ this.Start();
+ }
+
+ var fullCommand = string.IsNullOrWhiteSpace(this.arguments)
+ ? this.command
+ : $"{this.command} {this.arguments}";
+
+ this.result = await this.runtime.RunAsync(
+ this.image,
+ fullCommand,
+ this.config,
+ this.platformSpecifics,
+ cancellationToken);
+
+ this.exitTime = DateTime.UtcNow;
+ this.StandardOutput.Append(this.result.StandardOutput ?? string.Empty);
+ this.StandardError.Append(this.result.StandardError ?? string.Empty);
+ }
+
+ ///
+ public IProcessProxy WriteInput(string input)
+ {
+ // Container stdin not supported in this implementation
+ return this;
+ }
+
+ ///
+ /// Disposes of resources used by the proxy.
+ ///
+ /// True to dispose of unmanaged resources.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!this.disposed)
+ {
+ if (disposing)
+ {
+ // no underlying process defined yet.
+ ////this.UnderlyingProcess.Close();
+ ////this.UnderlyingProcess.Dispose();
+ }
+
+ this.disposed = true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Core/DockerRuntime.cs b/src/VirtualClient/VirtualClient.Core/DockerRuntime.cs
new file mode 100644
index 0000000000..1761f36399
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Core/DockerRuntime.cs
@@ -0,0 +1,247 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.Extensions.Logging;
+ using VirtualClient.Common;
+ using VirtualClient.Common.Extensions;
+ using VirtualClient.Contracts;
+
+ ///
+ /// Docker runtime for executing commands inside containers.
+ ///
+ public class DockerRuntime
+ {
+ private readonly ProcessManager processManager;
+ private readonly PlatformSpecifics platformSpecifics;
+ private readonly ILogger logger;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public DockerRuntime(ProcessManager processManager, PlatformSpecifics platformSpecifics, ILogger logger = null)
+ {
+ processManager.ThrowIfNull(nameof(processManager));
+ platformSpecifics.ThrowIfNull(nameof(platformSpecifics));
+ this.processManager = processManager;
+ this.platformSpecifics = platformSpecifics;
+ this.logger = logger;
+ }
+
+ ///
+ /// Checks if Docker is available and configured for Linux containers.
+ ///
+ public async Task IsAvailableAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ using IProcessProxy process = this.processManager.CreateProcess("docker", "info --format {{.OSType}}");
+ await process.StartAndWaitAsync(cancellationToken);
+
+ string osType = process.StandardOutput.ToString().Trim().ToLowerInvariant();
+ return process.ExitCode == 0 && osType.Contains("linux");
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Checks if an image exists locally.
+ ///
+ public async Task ImageExistsAsync(string image, CancellationToken cancellationToken)
+ {
+ using IProcessProxy process = this.processManager.CreateProcess("docker", $"image inspect {image}");
+ await process.StartAndWaitAsync(cancellationToken);
+ return process.ExitCode == 0;
+ }
+
+ ///
+ /// Pulls an image.
+ ///
+ public async Task PullImageAsync(string image, CancellationToken cancellationToken)
+ {
+ this.logger?.LogInformation("Pulling Docker image: {Image}", image);
+
+ using IProcessProxy process = this.processManager.CreateProcess("docker", $"pull {image}");
+ await process.StartAndWaitAsync(cancellationToken);
+
+ if (process.ExitCode != 0)
+ {
+ throw new DependencyException(
+ $"Failed to pull Docker image '{image}': {process.StandardError}",
+ ErrorReason.DependencyInstallationFailed);
+ }
+ }
+
+ ///
+ /// Executes a command inside a container.
+ ///
+ public async Task RunAsync(
+ string image,
+ string command,
+ ContainerConfiguration config,
+ PlatformSpecifics hostPlatformSpecifics,
+ CancellationToken cancellationToken)
+ {
+ string containerName = $"vc-{Guid.NewGuid():N}"[..32];
+ string args = this.BuildDockerRunArgs(image, command, config, containerName, hostPlatformSpecifics);
+
+ this.logger?.LogInformation("Running container: {ContainerName}", containerName);
+ this.logger?.LogDebug("Docker command: docker {Args}", args);
+
+ var result = new DockerRunResult
+ {
+ ContainerName = containerName,
+ StartTime = DateTime.UtcNow
+ };
+
+ using IProcessProxy process = this.processManager.CreateProcess("docker", args);
+
+ await process.StartAndWaitAsync(cancellationToken);
+
+ result.EndTime = DateTime.UtcNow;
+ result.ExitCode = process.ExitCode;
+ result.StandardOutput = process.StandardOutput.ToString();
+ result.StandardError = process.StandardError.ToString();
+
+ this.logger?.LogDebug("Container {ContainerName} exited with code {ExitCode}", containerName, result.ExitCode);
+
+ return result;
+ }
+
+ private string BuildDockerRunArgs(
+ string image,
+ string command,
+ ContainerConfiguration config,
+ string containerName,
+ PlatformSpecifics hostPlatformSpecifics)
+ {
+ var args = new List
+ {
+ "run",
+ "--rm",
+ $"--name {containerName}"
+ };
+
+ // Working directory
+ string workDir = config?.WorkingDirectory ?? "/vc";
+ args.Add($"-w {workDir}");
+
+ // Standard mounts
+ ContainerMountConfig mounts = config?.Mounts ?? new ContainerMountConfig();
+
+ if (mounts.Packages)
+ {
+ string hostPath = this.ToDockerPath(hostPlatformSpecifics.PackagesDirectory);
+ args.Add($"-v \"{hostPath}:/vc/packages\"");
+ }
+
+ if (mounts.Logs)
+ {
+ string hostPath = this.ToDockerPath(hostPlatformSpecifics.LogsDirectory);
+ args.Add($"-v \"{hostPath}:/vc/logs\"");
+ }
+
+ if (mounts.State)
+ {
+ string hostPath = this.ToDockerPath(hostPlatformSpecifics.StateDirectory);
+ args.Add($"-v \"{hostPath}:/vc/state\"");
+ }
+
+ if (mounts.Temp)
+ {
+ string hostPath = this.ToDockerPath(hostPlatformSpecifics.TempDirectory);
+ args.Add($"-v \"{hostPath}:/vc/temp\"");
+ }
+
+ // Additional mounts
+ if (config?.AdditionalMounts?.Any() == true)
+ {
+ foreach (string mount in config.AdditionalMounts)
+ {
+ args.Add($"-v \"{mount}\"");
+ }
+ }
+
+ // Environment variables
+ if (config?.EnvironmentVariables?.Any() == true)
+ {
+ foreach (KeyValuePair env in config.EnvironmentVariables)
+ {
+ args.Add($"-e \"{env.Key}={env.Value}\"");
+ }
+ }
+
+ // Always pass these VC context vars
+ args.Add("-e \"VC_CONTAINER_MODE=true\"");
+
+ // Image
+ args.Add(image);
+
+ // Command (if provided)
+ if (!string.IsNullOrWhiteSpace(command))
+ {
+ args.Add(command);
+ }
+
+ return string.Join(" ", args);
+ }
+
+ ///
+ /// Converts Windows path to Docker-compatible format.
+ /// C:\path\to\dir -> /c/path/to/dir
+ ///
+ private string ToDockerPath(string path)
+ {
+ if (this.platformSpecifics.Platform == PlatformID.Win32NT && path.Length >= 2 && path[1] == ':')
+ {
+ char drive = char.ToLower(path[0]);
+ return $"/{drive}{path[2..].Replace('\\', '/')}";
+ }
+
+ return path;
+ }
+ }
+
+ ///
+ /// Result of a Docker run operation.
+ ///
+ public class DockerRunResult
+ {
+ /// Container name.
+ public string ContainerName { get; set; }
+
+ /// Exit code from the container.
+ public int ExitCode { get; set; }
+
+ /// Standard output from the container.
+ public string StandardOutput { get; set; }
+
+ /// Standard error from the container.
+ public string StandardError { get; set; }
+
+ /// When the container started.
+ public DateTime StartTime { get; set; }
+
+ /// When the container exited.
+ public DateTime EndTime { get; set; }
+
+ /// Duration of execution.
+ public TimeSpan Duration => this.EndTime - this.StartTime;
+
+ /// True if exit code was 0.
+ public bool Succeeded => this.ExitCode == 0;
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Main/CommandLineParser.cs b/src/VirtualClient/VirtualClient.Main/CommandLineParser.cs
index b21f54fae9..94f67c5144 100644
--- a/src/VirtualClient/VirtualClient.Main/CommandLineParser.cs
+++ b/src/VirtualClient/VirtualClient.Main/CommandLineParser.cs
@@ -156,7 +156,15 @@ public static CommandLineParser Create(IEnumerable args, CancellationTok
OptionFactory.CreateTimeoutOption(required: false),
// --verbose
- OptionFactory.CreateVerboseFlag(required: false, false)
+ OptionFactory.CreateVerboseFlag(required: false, false),
+
+ // CONTAINER OPTIONS
+ // -------------------------------------------------------------------
+ // --image
+ OptionFactory.CreateImageOption(required: false),
+
+ // --pull-policy
+ OptionFactory.CreatePullPolicyOption(required: false)
};
rootCommand.TreatUnmatchedTokensAsErrors = true;
diff --git a/src/VirtualClient/VirtualClient.Main/ExecuteProfileCommand.cs b/src/VirtualClient/VirtualClient.Main/ExecuteProfileCommand.cs
index bf5c5f655b..7ee7ec731d 100644
--- a/src/VirtualClient/VirtualClient.Main/ExecuteProfileCommand.cs
+++ b/src/VirtualClient/VirtualClient.Main/ExecuteProfileCommand.cs
@@ -669,7 +669,7 @@ protected void SetHostMetadataTelemetryProperties(IEnumerable profiles,
new Dictionary
{
{ "exitWait", this.ExitWait },
- { "layout", this.Layout.ToString() },
+ { "layout", this.Layout?.ToString() },
{ "logToFile", this.LogToFile },
{ "iterations", this.Iterations?.ProfileIterations },
{ "profiles", string.Join(",", profiles.Select(p => Path.GetFileName(p))) },
diff --git a/src/VirtualClient/VirtualClient.Main/Images/Dockerfile.ubuntu b/src/VirtualClient/VirtualClient.Main/Images/Dockerfile.ubuntu
new file mode 100644
index 0000000000..c7a9c9eb3b
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/Images/Dockerfile.ubuntu
@@ -0,0 +1,55 @@
+# VirtualClient Ubuntu Base Image
+# This image provides a Linux environment for running workloads from Windows hosts
+
+FROM ubuntu:22.04
+
+LABEL maintainer="VirtualClient Team"
+LABEL description="Ubuntu base image for VirtualClient containerized workload execution"
+
+# Prevent interactive prompts during package installation
+ENV DEBIAN_FRONTEND=noninteractive
+
+# Install common dependencies needed for most workloads
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ # Build essentials
+ build-essential \
+ gcc \
+ g++ \
+ make \
+ cmake \
+ # Common utilities
+ curl \
+ wget \
+ git \
+ unzip \
+ tar \
+ gzip \
+ # Python (many workloads need it)
+ python3 \
+ python3-pip \
+ # Libraries
+ libc6-dev \
+ libssl-dev \
+ libffi-dev \
+ libnuma-dev \
+ # Debugging tools
+ strace \
+ htop \
+ procps \
+ # Cleanup
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+# Create standard VC directories
+RUN mkdir -p /vc/packages /vc/logs /vc/state /vc/temp /vc/output
+
+# Set working directory
+WORKDIR /vc
+
+# Default environment variables
+ENV VC_CONTAINER_MODE=true
+ENV LC_ALL=C.UTF-8
+ENV LANG=C.UTF-8
+
+# Default command (can be overridden)
+CMD ["/bin/bash"]
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Main/Images/build-image.ps1 b/src/VirtualClient/VirtualClient.Main/Images/build-image.ps1
new file mode 100644
index 0000000000..08f0999f69
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/Images/build-image.ps1
@@ -0,0 +1,23 @@
+# Build script for VirtualClient Ubuntu image
+param(
+ [string]$ImageName = "vc-ubuntu",
+ [string]$Tag = "22.04"
+)
+
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$DockerfilePath = Join-Path $ScriptDir "Dockerfile.ubuntu"
+
+Write-Host "Building VirtualClient Ubuntu image..." -ForegroundColor Cyan
+Write-Host "Dockerfile: $DockerfilePath" -ForegroundColor Gray
+
+docker build -t "${ImageName}:${Tag}" -f $DockerfilePath $ScriptDir
+
+if ($LASTEXITCODE -eq 0) {
+ Write-Host "`nImage built successfully!" -ForegroundColor Green
+ Write-Host "Image: ${ImageName}:${Tag}" -ForegroundColor Green
+ Write-Host "`nTo run VirtualClient with this image:" -ForegroundColor Yellow
+ Write-Host " VirtualClient.exe --profile=PERF-CPU-OPENSSL.json --image=${ImageName}:${Tag}" -ForegroundColor White
+} else {
+ Write-Host "`nBuild failed!" -ForegroundColor Red
+ exit 1
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Main/OptionFactory.Container.cs b/src/VirtualClient/VirtualClient.Main/OptionFactory.Container.cs
new file mode 100644
index 0000000000..e07f2a171c
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/OptionFactory.Container.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient
+{
+ using System.CommandLine;
+
+ public static partial class OptionFactory
+ {
+ ///
+ /// Container image for workload execution.
+ /// When provided, VC runs workloads inside this container.
+ ///
+ public static Option CreateImageOption(bool required = false)
+ {
+ return new Option(
+ aliases: new[] { "--image", "-i" },
+ description: "Docker image for containerized execution. When provided, workloads run inside the container.")
+ {
+ IsRequired = required
+ };
+ }
+
+ ///
+ /// Image pull policy.
+ ///
+ public static Option CreatePullPolicyOption(bool required = false)
+ {
+ return new Option(
+ aliases: new[] { "--pull-policy" },
+ getDefaultValue: () => "IfNotPresent",
+ description: "Image pull policy: Always, IfNotPresent, Never. Default: IfNotPresent");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Main/OptionFactory.cs b/src/VirtualClient/VirtualClient.Main/OptionFactory.cs
index a69948c09c..7aed8d7397 100644
--- a/src/VirtualClient/VirtualClient.Main/OptionFactory.cs
+++ b/src/VirtualClient/VirtualClient.Main/OptionFactory.cs
@@ -26,7 +26,7 @@ namespace VirtualClient
/// Provides a factory for the creation of Command Options used by application command line operations.
///
[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Allow for longer description text.")]
- public static class OptionFactory
+ public static partial class OptionFactory
{
internal const string HtmlQuote = """;
private static readonly ICertificateManager defaultCertificateManager = new CertificateManager();