// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using System; using System.Collections.Generic; using System.IO; using System.Linq; using AWS.Deploy.Common; using AWS.Deploy.Common.IO; using AWS.Deploy.Common.Recipes; using AWS.Deploy.Common.Utilities; using Newtonsoft.Json; namespace AWS.Deploy.DockerEngine { public interface IDockerEngine { /// /// Generates a docker file /// void GenerateDockerFile(); /// /// Inspects the Dockerfile associated with the recommendation /// and determines the appropriate Docker Execution Directory, /// if one is not set. /// void DetermineDockerExecutionDirectory(Recommendation recommendation); } /// /// Orchestrates the moving parts involved in creating a dockerfile for a project /// public class DockerEngine : IDockerEngine { private readonly ProjectDefinition _project; private readonly IFileManager _fileManager; private readonly IDirectoryManager _directoryManager; private readonly string _projectPath; public DockerEngine(ProjectDefinition project, IFileManager fileManager, IDirectoryManager directoryManager) { if (project == null) { throw new ArgumentNullException(nameof(project), "Cannot instantiate DockerEngine due to a null ProjectDefinition"); } _project = project; _projectPath = project.ProjectPath; _fileManager = fileManager; _directoryManager = directoryManager; } /// /// Generates a docker file /// public void GenerateDockerFile() { var projectFileName = Path.GetFileName(_projectPath); var imageMapping = GetImageMapping(); if (imageMapping == null) { throw new UnknownDockerImageException(DeployToolErrorCode.NoValidDockerImageForProject, $"Unable to determine a valid docker base and build image for project of type {_project.SdkType} and Target Framework {_project.TargetFramework}"); } var dockerFile = new DockerFile(imageMapping, projectFileName, _project.AssemblyName); var projectDirectory = Path.GetDirectoryName(_projectPath) ?? ""; var projectList = GetProjectList(); dockerFile.WriteDockerFile(projectDirectory, projectList); } /// /// Retrieves a list of projects from a solution file /// private List? GetProjectsFromSolutionFile(string solutionFile) { var projectFileName = Path.GetFileName(_projectPath); if (string.IsNullOrWhiteSpace(solutionFile) || string.IsNullOrWhiteSpace(projectFileName)) { return null; } List lines = File.ReadAllLines(solutionFile).ToList(); var projectLines = lines.Where(x => x.StartsWith("Project")); var projectPaths = projectLines .Select(x => x.Split(',')[1].Replace('\"', ' ').Trim()) .Where(x => x.EndsWith(".csproj") || x.EndsWith(".fsproj")) .Select(x => x.Replace('\\', Path.DirectorySeparatorChar)) .ToList(); //Validate project exists in solution if (projectPaths.Select(x => Path.GetFileName(x)).Where(x => x.Equals(projectFileName)).ToList().Count == 0) { return null; } return projectPaths; } /// /// Finds the project solution file (if one exists) and retrieves a list of projects that are part of one solution /// private List? GetProjectList() { var solutionDirectory = Directory.GetParent(_projectPath); // Climb upward from the csproj until we find a directory with one or more slns while (solutionDirectory != null) { var allSolutionFiles = solutionDirectory.GetFiles("*.sln"); if (allSolutionFiles.Length > 0) { foreach (var solutionFile in allSolutionFiles) { var projectList = GetProjectsFromSolutionFile(solutionFile.FullName); if (projectList != null) { // Validate that all referenced projects are at or below the solution, otherwise we'd have to use // a wider Docker execution directory that is potentially sending more than the project to the Docker daemon foreach (var project in projectList) { var absoluteProjectPath = _directoryManager.GetAbsolutePath(solutionDirectory.FullName, project); if (!_directoryManager.ExistsInsideDirectory(solutionDirectory.FullName, absoluteProjectPath)) { throw new DockerEngineException(DeployToolErrorCode.FailedToGenerateDockerFile, "Unable to generate a Dockerfile for this project becuase project reference(s) were detected above the solution (.sln) file. " + "Consider crafting your own Dockerfile or deploying to an AWS compute service that does not use Docker."); } } return projectList; } } } solutionDirectory = solutionDirectory.Parent; } return null; } /// /// Gets image mapping specific to this project /// private ImageMapping GetImageMapping() { var content = ProjectUtilities.ReadDockerFileConfig(); var definitions = JsonConvert.DeserializeObject>(content); var sdkType = _project.SdkType; // For the Microsoft.NET.Sdk.Workersdk type we want to use the same container as Microsoft.NET.Sdk since a project // using Microsoft.NET.Sdk.Worker is still just a regular console application. if (string.Equals(sdkType, "Microsoft.NET.Sdk.Worker", StringComparison.OrdinalIgnoreCase)) { sdkType = "Microsoft.NET.Sdk"; } var mappings = definitions?.FirstOrDefault(x => x.SdkType.Equals(sdkType)); if (mappings == null) throw new UnsupportedProjectException(DeployToolErrorCode.NoValidDockerMappingForSdkType, $"The project with SDK Type {_project.SdkType} is not supported."); return mappings.ImageMapping.FirstOrDefault(x => x.TargetFramework.Equals(_project.TargetFramework)) ?? throw new UnsupportedProjectException(DeployToolErrorCode.NoValidDockerMappingForTargetFramework, $"The project with Target Framework {_project.TargetFramework} is not supported."); } /// /// Inspects the Dockerfile associated with the recommendation /// and determines the appropriate Docker Execution Directory, /// if one is not set. /// /// public void DetermineDockerExecutionDirectory(Recommendation recommendation) { if (string.IsNullOrEmpty(recommendation.DeploymentBundle.DockerExecutionDirectory)) { var projectFilename = Path.GetFileName(recommendation.ProjectPath); if (DockerUtilities.TryGetAbsoluteDockerfile(recommendation, _fileManager, _directoryManager, out var dockerFilePath)) { using (var stream = File.OpenRead(dockerFilePath)) using (var reader = new StreamReader(stream)) { string? line; while ((line = reader.ReadLine()) != null) { var noSpaceLine = line.Replace(" ", ""); if (noSpaceLine.StartsWith("COPY") && (noSpaceLine.EndsWith(".sln./") || (projectFilename != null && noSpaceLine.Contains("/" + projectFilename)))) { recommendation.DeploymentBundle.DockerExecutionDirectory = Path.GetDirectoryName(recommendation.ProjectDefinition.ProjectSolutionPath) ?? ""; } } } } } } } }