using Amazon.Common.DotNetCli.Tools; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Text; namespace Amazon.Common.DotNetCli.Tools { public class DockerCLIWrapper : AbstractCLIWrapper { public static readonly string WorkingDirectoryMountLocation = "/tmp/source/"; string _dockerCLI; public DockerCLIWrapper(IToolLogger logger, string workingDirectory) : base(logger, workingDirectory) { this._dockerCLI = FindExecutableInPath("docker.exe"); if (this._dockerCLI == null) this._dockerCLI = FindExecutableInPath("docker"); if (string.IsNullOrEmpty(this._dockerCLI)) throw new Exception("Failed to locate docker CLI executable. Make sure the docker CLI is installed in the environment PATH."); } public int Build(string workingDirectory, string dockerFile, string imageTag, string additionalBuildOptions, bool arm64Build = false) { _logger?.WriteLine($"... invoking 'docker build', working folder '{workingDirectory}, docker file {dockerFile}, image name {imageTag}'"); var arguments = new StringBuilder(); #if NETCOREAPP3_1_OR_GREATER var runningOnLinuxArm64 = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; #else var runningOnLinuxArm64 = false; #endif if (arm64Build && !runningOnLinuxArm64) { _logger?.WriteLine("The docker CLI \"buildx\" command is used to build ARM64 images. This requires version 20 or later of the docker CLI."); arguments.Append($"buildx build --platform linux/arm64 "); } else { arguments.Append($"build "); } arguments.Append($" -f \"{dockerFile}\" -t {imageTag}"); if(!string.IsNullOrEmpty(additionalBuildOptions)) { arguments.Append($" {additionalBuildOptions}"); } arguments.Append(" ."); _logger?.WriteLine($"... docker {arguments.ToString()}"); var psi = new ProcessStartInfo { FileName = this._dockerCLI, Arguments = arguments.ToString(), WorkingDirectory = workingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; return base.ExecuteCommand(psi, "docker build"); } public string GetImageId(string imageTag) { var psi = new ProcessStartInfo { FileName = this._dockerCLI, // Make sure to have the space after the "{{.ID}}" and the closing quote. Arguments = "images --format \"{{.ID}}\" " + imageTag, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; var process = Process.Start(psi); var imageId = process.StandardOutput.ReadToEnd()?.Trim(); // To prevent a unlikely hang give 10 seconds as max for waiting for the docker CLI to execute and return back the image id. process.WaitForExit(10000); if (imageId.Length != 12) return null; return imageId; } public int Login(string username, string password, string proxy) { _logger?.WriteLine($"... invoking 'docker login'"); var arguments = $"login --username {username} --password {password} {proxy}"; var psi = new ProcessStartInfo { FileName = this._dockerCLI, Arguments = arguments.ToString(), WorkingDirectory = this._workingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; return base.ExecuteCommand(psi, "docker login"); } public int Tag(string sourceTagName, string targetTagName) { _logger?.WriteLine($"... invoking 'docker tag'"); var arguments = $"tag {sourceTagName} {targetTagName}"; var psi = new ProcessStartInfo { FileName = this._dockerCLI, Arguments = arguments.ToString(), WorkingDirectory = this._workingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; return base.ExecuteCommand(psi, "docker tag"); } public int Push(string targetTagName) { _logger?.WriteLine($"... invoking 'docker push'"); var arguments = $"push {targetTagName}"; var psi = new ProcessStartInfo { FileName = this._dockerCLI, Arguments = arguments.ToString(), WorkingDirectory = this._workingDirectory }; return base.ExecuteCommand(psi, "docker push"); } /// /// Executes the Docker run command on the given image locally /// /// Tells Docker which pre-built image to run. Can be local or remote. /// A custom name to give the generated container. This is useful so the caller of this method knows what container to look for after. /// Tells Docker what commands to invoke on the container once it is running. Can be left blank. /// public int Run(string imageId, string containerName, string commandToRun = "") { var argumentList = new List() { "run", "--name", containerName, // Automatically remove the container once it's done running "--rm", // This allows the container access to the working directory in a virtual mapped path located at /tmp/source // That means that when the container is finished running, anything it leaves in /tmp/source (e.g. the binaries), // will just exist in the working directory "--volume", $"\"{this._workingDirectory}\":{WorkingDirectoryMountLocation}", "-i" }; // For Linux or MacOS, running a .NET image as non-root requires some special // handling. We have to pass the user's UID and GID, but we also have to specify // the location of where .NET and Nuget put their files, because by default, they // go to the /.dotnet and /.local, which a non-root user can't write to in a container if (PosixUserHelper.IsRunningInPosix) { var posixUser = PosixUserHelper.GetEffectiveUser(_logger); if (posixUser != null && (posixUser?.UserID > 0 || posixUser.Value.GroupID > 0)) { argumentList.AddRange(new [] { // Set Docker user and group IDs "-u", $"{posixUser.Value.UserID}:{posixUser.Value.GroupID}", // Set .NET CLI home directory "-e", "DOTNET_CLI_HOME=/tmp/dotnet", // Set NuGet data home directory to non-root directory "-e", "XDG_DATA_HOME=/tmp/xdg" }); } } argumentList.Add(imageId); var arguments = string.Join(" ", argumentList); if (!string.IsNullOrEmpty(commandToRun)) { arguments += $" {commandToRun}"; } var psi = new ProcessStartInfo { FileName = this._dockerCLI, Arguments = arguments, WorkingDirectory = this._workingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; _logger?.WriteLine($"... invoking 'docker {arguments}' from directory {this._workingDirectory}"); return base.ExecuteCommand(psi, "docker run"); } } }