// 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) ?? "";
}
}
}
}
}
}
}
}