From e368d04e914a3880a1a10e21cf5c271966241dbd Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Thu, 21 May 2020 17:21:44 -0700 Subject: [PATCH] Add docker file support for deploy (#489) --- src/Microsoft.Tye.Core/ApplicationExecutor.cs | 46 ++++++++ src/Microsoft.Tye.Core/ApplicationFactory.cs | 32 ++++- .../ApplyContainerDefaultsStep.cs | 9 +- .../BuildDockerImageStep.cs | 10 +- .../ConfigModel/ConfigApplication.cs | 2 +- .../ContainerServiceBuilder.cs | 4 - .../DockerContainerBuilder.cs | 50 +++++++- .../DockerFileServiceBuilder.cs | 20 ++++ src/Microsoft.Tye.Core/DockerfileGenerator.cs | 18 ++- .../DotnetProjectServiceBuilder.cs | 35 ++++++ .../GenerateDockerfileStep.cs | 2 +- .../GenerateHelmChartStep.cs | 2 +- .../GenerateOamComponentStep.cs | 2 +- src/Microsoft.Tye.Core/HelmChartGenerator.cs | 2 +- src/Microsoft.Tye.Core/ProjectReader.cs | 4 +- .../ProjectServiceBuilder.cs | 30 +---- src/Microsoft.Tye.Core/PublishProjectStep.cs | 2 +- .../DiagnosticAgent.cs | 2 +- .../Elastic/ElasticStackExtension.cs | 2 +- .../Zipkin/ZipkinExtension.cs | 2 +- .../Model/ProjectRunInfo.cs | 2 +- src/tye/ApplicationBuilderExtensions.cs | 39 +++++-- test/E2ETest/Microsoft.Tye.E2ETests.csproj | 4 + test/E2ETest/TyeGenerateTests.cs | 39 +++++++ test/E2ETest/TyeRunTests.cs | 6 +- .../testassets/generate/dockerfile.yaml | 109 ++++++++++++++++++ 26 files changed, 412 insertions(+), 63 deletions(-) create mode 100644 src/Microsoft.Tye.Core/DockerFileServiceBuilder.cs create mode 100644 src/Microsoft.Tye.Core/DotnetProjectServiceBuilder.cs create mode 100644 test/E2ETest/testassets/generate/dockerfile.yaml diff --git a/src/Microsoft.Tye.Core/ApplicationExecutor.cs b/src/Microsoft.Tye.Core/ApplicationExecutor.cs index c18ebb5d..ad6b3627 100644 --- a/src/Microsoft.Tye.Core/ApplicationExecutor.cs +++ b/src/Microsoft.Tye.Core/ApplicationExecutor.cs @@ -81,6 +81,29 @@ namespace Microsoft.Tye public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service); + protected bool SkipWithoutDotnetProject(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out DotnetProjectServiceBuilder project) + { + if (output is null) + { + throw new ArgumentNullException(nameof(output)); + } + + if (service is null) + { + throw new ArgumentNullException(nameof(service)); + } + + if (service is DotnetProjectServiceBuilder p) + { + project = p; + return false; + } + + output.WriteInfoLine($"Service '{service.Name}' does not have a project associated. Skipping."); + project = default!; + return true; + } + protected bool SkipWithoutProject(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out ProjectServiceBuilder project) { if (output is null) @@ -104,6 +127,29 @@ namespace Microsoft.Tye return true; } + protected bool SkipWithoutDockerFile(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out DockerFileServiceBuilder project) + { + if (output is null) + { + throw new ArgumentNullException(nameof(output)); + } + + if (service is null) + { + throw new ArgumentNullException(nameof(service)); + } + + if (service is DockerFileServiceBuilder p) + { + project = p; + return false; + } + + output.WriteInfoLine($"Service '{service.Name}' does not have a project associated. Skipping."); + project = default!; + return true; + } + protected bool SkipWithoutContainerInfo(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out ContainerInfo container) { if (output is null) diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs index 54e15f72..bd693c97 100644 --- a/src/Microsoft.Tye.Core/ApplicationFactory.cs +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -86,7 +86,7 @@ namespace Microsoft.Tye { var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project); var projectFile = new FileInfo(Path.Combine(config.Source.DirectoryName, expandedProject)); - var project = new ProjectServiceBuilder(configService.Name!, projectFile); + var project = new DotnetProjectServiceBuilder(configService.Name!, projectFile); service = project; project.Build = configService.Build ?? true; @@ -109,21 +109,41 @@ namespace Microsoft.Tye // Do k8s by default. project.ManifestInfo = new KubernetesManifestInfo(); } - else if (!string.IsNullOrEmpty(configService.Image) || !string.IsNullOrEmpty(configService.DockerFile)) + else if (!string.IsNullOrEmpty(configService.Image)) { var container = new ContainerServiceBuilder(configService.Name!, configService.Image!) { Args = configService.Args, - Replicas = configService.Replicas ?? 1, - DockerFile = configService.DockerFile != null ? Path.Combine(source.DirectoryName, configService.DockerFile) : null, - // Supplying an absolute path with trailing slashes fails for DockerFileContext when calling docker build, so trim trailing slash. - DockerFileContext = GetDockerFileContext(source, configService) + Replicas = configService.Replicas ?? 1 }; service = container; container.Liveness = configService.Liveness != null ? GetProbeBuilder(configService.Liveness) : null; container.Readiness = configService.Readiness != null ? GetProbeBuilder(configService.Readiness) : null; } + else if (!string.IsNullOrEmpty(configService.DockerFile)) + { + var dockerFile = new DockerFileServiceBuilder(configService.Name!, configService.Image!) + { + Args = configService.Args, + Build = configService.Build ?? true, + Replicas = configService.Replicas ?? 1, + DockerFile = Path.Combine(source.DirectoryName, configService.DockerFile), + // Supplying an absolute path with trailing slashes fails for DockerFileContext when calling docker build, so trim trailing slash. + DockerFileContext = GetDockerFileContext(source, configService) + }; + service = dockerFile; + + dockerFile.Liveness = configService.Liveness != null ? GetProbeBuilder(configService.Liveness) : null; + dockerFile.Readiness = configService.Readiness != null ? GetProbeBuilder(configService.Readiness) : null; + + // We don't apply more container defaults here because we might need + // to prompt for the registry name. + dockerFile.ContainerInfo = new ContainerInfo() { UseMultiphaseDockerfile = false, }; + + // Do k8s by default. + dockerFile.ManifestInfo = new KubernetesManifestInfo(); + } else if (!string.IsNullOrEmpty(configService.Executable)) { var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable); diff --git a/src/Microsoft.Tye.Core/ApplyContainerDefaultsStep.cs b/src/Microsoft.Tye.Core/ApplyContainerDefaultsStep.cs index 3f4a710c..f38f8d4d 100644 --- a/src/Microsoft.Tye.Core/ApplyContainerDefaultsStep.cs +++ b/src/Microsoft.Tye.Core/ApplyContainerDefaultsStep.cs @@ -22,7 +22,14 @@ namespace Microsoft.Tye return Task.CompletedTask; } - DockerfileGenerator.ApplyContainerDefaults(application, project, container); + if (project is DotnetProjectServiceBuilder dotnetProject) + { + DockerfileGenerator.ApplyContainerDefaults(application, dotnetProject, container); + } + else if (project is DockerFileServiceBuilder dockerFile) + { + DockerfileGenerator.ApplyContainerDefaults(application, dockerFile, container); + } return Task.CompletedTask; } } diff --git a/src/Microsoft.Tye.Core/BuildDockerImageStep.cs b/src/Microsoft.Tye.Core/BuildDockerImageStep.cs index f94b9e82..ceb0431c 100644 --- a/src/Microsoft.Tye.Core/BuildDockerImageStep.cs +++ b/src/Microsoft.Tye.Core/BuildDockerImageStep.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Threading.Tasks; namespace Microsoft.Tye @@ -34,7 +35,14 @@ namespace Microsoft.Tye throw new CommandException($"Cannot generate a docker image for '{service.Name}' because docker is not running."); } - await DockerContainerBuilder.BuildContainerImageAsync(output, application, project, container); + if (project is DotnetProjectServiceBuilder dotnetProject) + { + await DockerContainerBuilder.BuildContainerImageAsync(output, application, dotnetProject, container); + } + else if (project is DockerFileServiceBuilder dockerFile) + { + await DockerContainerBuilder.BuildContainerImageFromDockerFileAsync(output, application, dockerFile, container); + } } } } diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs index e9c26237..d795640c 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs @@ -139,7 +139,7 @@ namespace Microsoft.Tye.ConfigModel var probes = new[] { (Name: "liveness", Probe: service.Liveness), (Name: "readiness", Probe: service.Readiness) }.Where(p => p.Probe != null).ToArray(); foreach (var probe in probes) { - if (probe.Name == "liveness" && probe.Probe.SuccessThreshold != 1) + if (probe.Name == "liveness" && probe.Probe!.SuccessThreshold != 1) { throw new TyeYamlException(CoreStrings.FormatSuccessThresholdMustBeOne(probe.Name)); } diff --git a/src/Microsoft.Tye.Core/ContainerServiceBuilder.cs b/src/Microsoft.Tye.Core/ContainerServiceBuilder.cs index f9a0fee6..a28c0be8 100644 --- a/src/Microsoft.Tye.Core/ContainerServiceBuilder.cs +++ b/src/Microsoft.Tye.Core/ContainerServiceBuilder.cs @@ -20,10 +20,6 @@ namespace Microsoft.Tye public string? Args { get; set; } - public string? DockerFile { get; set; } - - public string? DockerFileContext { get; set; } - public int Replicas { get; set; } = 1; public List EnvironmentVariables { get; } = new List(); diff --git a/src/Microsoft.Tye.Core/DockerContainerBuilder.cs b/src/Microsoft.Tye.Core/DockerContainerBuilder.cs index 92f34b89..409a8694 100644 --- a/src/Microsoft.Tye.Core/DockerContainerBuilder.cs +++ b/src/Microsoft.Tye.Core/DockerContainerBuilder.cs @@ -12,7 +12,55 @@ namespace Microsoft.Tye { internal static class DockerContainerBuilder { - public static async Task BuildContainerImageAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container) + public static async Task BuildContainerImageFromDockerFileAsync(OutputContext output, ApplicationBuilder application, DockerFileServiceBuilder containerService, ContainerInfo container) + { + if (output is null) + { + throw new ArgumentNullException(nameof(output)); + } + + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (containerService is null) + { + throw new ArgumentNullException(nameof(containerService)); + } + + if (containerService.DockerFile is null) + { + throw new ArgumentNullException(nameof(containerService.DockerFile)); + } + + var dockerFileInfo = new FileInfo(containerService.DockerFile); + var contextDirectory = containerService.DockerFileContext ?? dockerFileInfo.DirectoryName; + var dockerFilePath = Path.Combine(dockerFileInfo.DirectoryName, "Dockerfile"); + + output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'."); + + output.WriteDebugLine("Running 'docker build'."); + output.WriteCommandLine("docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\""); + var capture = output.Capture(); + var exitCode = await Process.ExecuteAsync( + $"docker", + $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"", + new FileInfo(containerService.DockerFile).DirectoryName, + stdOut: capture.StdOut, + stdErr: capture.StdErr); + + output.WriteDebugLine($"Done running 'docker build' exit code: {exitCode}"); + if (exitCode != 0) + { + throw new CommandException("'docker build' failed."); + } + + output.WriteInfoLine($"Created Docker Image: '{container.ImageName}:{container.ImageTag}'"); + containerService.Outputs.Add(new DockerImageOutput(container.ImageName!, container.ImageTag!)); + } + + public static async Task BuildContainerImageAsync(OutputContext output, ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container) { if (output is null) { diff --git a/src/Microsoft.Tye.Core/DockerFileServiceBuilder.cs b/src/Microsoft.Tye.Core/DockerFileServiceBuilder.cs new file mode 100644 index 00000000..73a796f9 --- /dev/null +++ b/src/Microsoft.Tye.Core/DockerFileServiceBuilder.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Tye +{ + public class DockerFileServiceBuilder : ProjectServiceBuilder + { + public DockerFileServiceBuilder(string name, string image) + : base(name) + { + Image = image; + } + public string Image { get; set; } + + public string? DockerFile { get; set; } + + public string? DockerFileContext { get; set; } + } +} diff --git a/src/Microsoft.Tye.Core/DockerfileGenerator.cs b/src/Microsoft.Tye.Core/DockerfileGenerator.cs index e08eac3b..b3e3fb7e 100644 --- a/src/Microsoft.Tye.Core/DockerfileGenerator.cs +++ b/src/Microsoft.Tye.Core/DockerfileGenerator.cs @@ -12,7 +12,7 @@ namespace Microsoft.Tye { public static class DockerfileGenerator { - public static async Task WriteDockerfileAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, string filePath) + public static async Task WriteDockerfileAsync(OutputContext output, ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container, string filePath) { if (output is null) { @@ -75,7 +75,7 @@ namespace Microsoft.Tye await writer.WriteLineAsync($"ENTRYPOINT [\"dotnet\", \"{applicationEntryPoint}.dll\"]"); } - public static void ApplyContainerDefaults(ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container) + public static void ApplyContainerDefaults(ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container) { if (application is null) { @@ -125,5 +125,19 @@ namespace Microsoft.Tye container.ImageTag ??= project.Version?.Replace("+", "-") ?? "latest"; } + + public static void ApplyContainerDefaults(ApplicationBuilder application, DockerFileServiceBuilder project, ContainerInfo container) + { + if (container.ImageName == null && application.Registry?.Hostname == null) + { + container.ImageName ??= project.Name.ToLowerInvariant(); + } + else if (container.ImageName == null && application.Registry?.Hostname != null) + { + container.ImageName ??= $"{application.Registry?.Hostname}/{project.Name.ToLowerInvariant()}"; + } + + container.ImageTag ??= "latest"; + } } } diff --git a/src/Microsoft.Tye.Core/DotnetProjectServiceBuilder.cs b/src/Microsoft.Tye.Core/DotnetProjectServiceBuilder.cs new file mode 100644 index 00000000..6f43cd5c --- /dev/null +++ b/src/Microsoft.Tye.Core/DotnetProjectServiceBuilder.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.Tye +{ + public class DotnetProjectServiceBuilder : ProjectServiceBuilder + { + public DotnetProjectServiceBuilder(string name, FileInfo projectFile) + : base(name) + { + ProjectFile = projectFile; + } + + public FileInfo ProjectFile { get; } + public FrameworkCollection Frameworks { get; } = new FrameworkCollection(); + + // These is always set on the ApplicationFactory codepath. + public string TargetFrameworkName { get; set; } = default!; + public string TargetFrameworkVersion { get; set; } = default!; + public string TargetFramework { get; set; } = default!; + public string[] TargetFrameworks { get; set; } = default!; + public string Version { get; set; } = default!; + public string TargetPath { get; set; } = default!; + public string RunCommand { get; set; } = default!; + public string RunArguments { get; set; } = default!; + public string AssemblyName { get; set; } = default!; + public string PublishDir { get; set; } = default!; + public string IntermediateOutputPath { get; set; } = default!; + public Dictionary BuildProperties { get; } = new Dictionary(); + } +} diff --git a/src/Microsoft.Tye.Core/GenerateDockerfileStep.cs b/src/Microsoft.Tye.Core/GenerateDockerfileStep.cs index bbb307b6..f0fc9733 100644 --- a/src/Microsoft.Tye.Core/GenerateDockerfileStep.cs +++ b/src/Microsoft.Tye.Core/GenerateDockerfileStep.cs @@ -15,7 +15,7 @@ namespace Microsoft.Tye public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { - if (SkipWithoutProject(output, service, out var project)) + if (SkipWithoutDotnetProject(output, service, out var project)) { return; } diff --git a/src/Microsoft.Tye.Core/GenerateHelmChartStep.cs b/src/Microsoft.Tye.Core/GenerateHelmChartStep.cs index f2236c26..d2ebeddb 100644 --- a/src/Microsoft.Tye.Core/GenerateHelmChartStep.cs +++ b/src/Microsoft.Tye.Core/GenerateHelmChartStep.cs @@ -15,7 +15,7 @@ namespace Microsoft.Tye public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { - if (SkipWithoutProject(output, service, out var project)) + if (SkipWithoutDotnetProject(output, service, out var project)) { return Task.CompletedTask; } diff --git a/src/Microsoft.Tye.Core/GenerateOamComponentStep.cs b/src/Microsoft.Tye.Core/GenerateOamComponentStep.cs index e5bb0bed..86867925 100644 --- a/src/Microsoft.Tye.Core/GenerateOamComponentStep.cs +++ b/src/Microsoft.Tye.Core/GenerateOamComponentStep.cs @@ -20,7 +20,7 @@ namespace Microsoft.Tye return Task.CompletedTask; } - if (SkipWithoutProject(output, service, out var project)) + if (SkipWithoutDotnetProject(output, service, out var project)) { return Task.CompletedTask; } diff --git a/src/Microsoft.Tye.Core/HelmChartGenerator.cs b/src/Microsoft.Tye.Core/HelmChartGenerator.cs index 4d9d6f14..ecff19d1 100644 --- a/src/Microsoft.Tye.Core/HelmChartGenerator.cs +++ b/src/Microsoft.Tye.Core/HelmChartGenerator.cs @@ -10,7 +10,7 @@ namespace Microsoft.Tye { internal static class HelmChartGenerator { - public static async Task GenerateAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory) + public static async Task GenerateAsync(OutputContext output, ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory) { if (output is null) { diff --git a/src/Microsoft.Tye.Core/ProjectReader.cs b/src/Microsoft.Tye.Core/ProjectReader.cs index 7fd0b94d..295bfab9 100644 --- a/src/Microsoft.Tye.Core/ProjectReader.cs +++ b/src/Microsoft.Tye.Core/ProjectReader.cs @@ -61,7 +61,7 @@ namespace Microsoft.Tye } } - public static Task ReadProjectDetailsAsync(OutputContext output, ProjectServiceBuilder project) + public static Task ReadProjectDetailsAsync(OutputContext output, DotnetProjectServiceBuilder project) { if (output is null) { @@ -142,7 +142,7 @@ namespace Microsoft.Tye // Do not load MSBuild types before using EnsureMSBuildRegistered. [MethodImpl(MethodImplOptions.NoInlining)] - private static void EvaluateProject(OutputContext output, ProjectServiceBuilder project) + private static void EvaluateProject(OutputContext output, DotnetProjectServiceBuilder project) { var sw = Stopwatch.StartNew(); diff --git a/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs b/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs index c4a55144..b9040178 100644 --- a/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs +++ b/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs @@ -3,19 +3,16 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.IO; namespace Microsoft.Tye { - public sealed class ProjectServiceBuilder : ServiceBuilder + public class ProjectServiceBuilder : ServiceBuilder { - public ProjectServiceBuilder(string name, FileInfo projectFile) + public ProjectServiceBuilder(string name) : base(name) { - ProjectFile = projectFile; } - - public FileInfo ProjectFile { get; } + public bool IsAspNet { get; set; } public int Replicas { get; set; } = 1; @@ -23,23 +20,6 @@ namespace Microsoft.Tye public string? Args { get; set; } - public FrameworkCollection Frameworks { get; } = new FrameworkCollection(); - - // These is always set on the ApplicationFactory codepath. - public string TargetFrameworkName { get; set; } = default!; - public string TargetFrameworkVersion { get; set; } = default!; - public string TargetFramework { get; set; } = default!; - public string[] TargetFrameworks { get; set; } = default!; - public string Version { get; set; } = default!; - public string TargetPath { get; set; } = default!; - public string RunCommand { get; set; } = default!; - public string RunArguments { get; set; } = default!; - public string AssemblyName { get; set; } = default!; - public string PublishDir { get; set; } = default!; - public string IntermediateOutputPath { get; set; } = default!; - public bool IsAspNet { get; set; } - public bool RelocateDiagnosticsDomainSockets { get; set; } - // Data used for building containers public ContainerInfo? ContainerInfo { get; set; } @@ -51,12 +31,12 @@ namespace Microsoft.Tye // Used when running in a container locally. public List Volumes { get; } = new List(); - public Dictionary BuildProperties { get; } = new Dictionary(); - public List Sidecars { get; } = new List(); public ProbeBuilder? Liveness { get; set; } public ProbeBuilder? Readiness { get; set; } + + public bool RelocateDiagnosticsDomainSockets { get; set; } } } diff --git a/src/Microsoft.Tye.Core/PublishProjectStep.cs b/src/Microsoft.Tye.Core/PublishProjectStep.cs index c99d83a5..9dbbd7c1 100644 --- a/src/Microsoft.Tye.Core/PublishProjectStep.cs +++ b/src/Microsoft.Tye.Core/PublishProjectStep.cs @@ -16,7 +16,7 @@ namespace Microsoft.Tye public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { - if (SkipWithoutProject(output, service, out var project)) + if (SkipWithoutDotnetProject(output, service, out var project)) { return; } diff --git a/src/Microsoft.Tye.Extensions/DiagnosticAgent.cs b/src/Microsoft.Tye.Extensions/DiagnosticAgent.cs index fa189499..63ea4462 100644 --- a/src/Microsoft.Tye.Extensions/DiagnosticAgent.cs +++ b/src/Microsoft.Tye.Extensions/DiagnosticAgent.cs @@ -8,7 +8,7 @@ namespace Microsoft.Tye { public static class DiagnosticAgent { - public static SidecarBuilder GetOrAddSidecar(ProjectServiceBuilder project) + public static SidecarBuilder GetOrAddSidecar(DotnetProjectServiceBuilder project) { // Bring your rain boots. project.RelocateDiagnosticsDomainSockets = true; diff --git a/src/Microsoft.Tye.Extensions/Elastic/ElasticStackExtension.cs b/src/Microsoft.Tye.Extensions/Elastic/ElasticStackExtension.cs index bf4fcdec..78229660 100644 --- a/src/Microsoft.Tye.Extensions/Elastic/ElasticStackExtension.cs +++ b/src/Microsoft.Tye.Extensions/Elastic/ElasticStackExtension.cs @@ -83,7 +83,7 @@ namespace Microsoft.Tye.Extensions.Elastic var kibana = elastic.Bindings.Single(b => b.Name == "kibana"); elastic.Bindings.Remove(kibana); - foreach (var project in context.Application.Services.OfType()) + foreach (var project in context.Application.Services.OfType()) { var sidecar = DiagnosticAgent.GetOrAddSidecar(project); diff --git a/src/Microsoft.Tye.Extensions/Zipkin/ZipkinExtension.cs b/src/Microsoft.Tye.Extensions/Zipkin/ZipkinExtension.cs index e93816f6..c00b76f8 100644 --- a/src/Microsoft.Tye.Extensions/Zipkin/ZipkinExtension.cs +++ b/src/Microsoft.Tye.Extensions/Zipkin/ZipkinExtension.cs @@ -60,7 +60,7 @@ namespace Microsoft.Tye.Extensions.Zipkin } else if (context.Operation == ExtensionContext.OperationKind.Deploy) { - foreach (var project in context.Application.Services.OfType()) + foreach (var project in context.Application.Services.OfType()) { var sidecar = DiagnosticAgent.GetOrAddSidecar(project); diff --git a/src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs b/src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs index c7ce4c97..b95e7405 100644 --- a/src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs +++ b/src/Microsoft.Tye.Hosting/Model/ProjectRunInfo.cs @@ -9,7 +9,7 @@ namespace Microsoft.Tye.Hosting.Model { public class ProjectRunInfo : RunInfo { - public ProjectRunInfo(ProjectServiceBuilder project) + public ProjectRunInfo(DotnetProjectServiceBuilder project) { ProjectFile = project.ProjectFile; Args = project.Args; diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index 6e5786e6..80c2065f 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -47,19 +47,19 @@ namespace Microsoft.Tye readiness = null; replicas = 1; } - else if (service is ContainerServiceBuilder container) + else if (service is DockerFileServiceBuilder dockerFile) { - var dockerRunInfo = new DockerRunInfo(container.Image, container.Args) + var dockerRunInfo = new DockerRunInfo(dockerFile.Image, dockerFile.Args) { - IsAspNet = container.IsAspNet + IsAspNet = dockerFile.IsAspNet }; - if (!string.IsNullOrEmpty(container.DockerFile)) + if (!string.IsNullOrEmpty(dockerFile.DockerFile)) { - dockerRunInfo.DockerFile = new FileInfo(container.DockerFile); - if (!string.IsNullOrEmpty(container.DockerFileContext)) + dockerRunInfo.DockerFile = new FileInfo(dockerFile.DockerFile); + if (!string.IsNullOrEmpty(dockerFile.DockerFileContext)) { - dockerRunInfo.DockerFileContext = new FileInfo(container.DockerFileContext); + dockerRunInfo.DockerFileContext = new FileInfo(dockerFile.DockerFileContext); } else { @@ -67,6 +67,29 @@ namespace Microsoft.Tye } } + foreach (var mapping in dockerFile.Volumes) + { + dockerRunInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target)); + } + + runInfo = dockerRunInfo; + replicas = dockerFile.Replicas; + liveness = dockerFile.Liveness != null ? GetProbeFromBuilder(dockerFile.Liveness) : null; + readiness = dockerFile.Readiness != null ? GetProbeFromBuilder(dockerFile.Readiness) : null; + + foreach (var entry in dockerFile.EnvironmentVariables) + { + env.Add(entry.ToHostingEnvironmentVariable()); + } + } + + else if (service is ContainerServiceBuilder container) + { + var dockerRunInfo = new DockerRunInfo(container.Image, container.Args) + { + IsAspNet = container.IsAspNet + }; + foreach (var mapping in container.Volumes) { dockerRunInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target)); @@ -94,7 +117,7 @@ namespace Microsoft.Tye env.Add(entry.ToHostingEnvironmentVariable()); } } - else if (service is ProjectServiceBuilder project) + else if (service is DotnetProjectServiceBuilder project) { if (project.TargetFrameworks.Length > 1) { diff --git a/test/E2ETest/Microsoft.Tye.E2ETests.csproj b/test/E2ETest/Microsoft.Tye.E2ETests.csproj index 8eb61443..cf36fe6f 100644 --- a/test/E2ETest/Microsoft.Tye.E2ETests.csproj +++ b/test/E2ETest/Microsoft.Tye.E2ETests.csproj @@ -31,4 +31,8 @@ + + + + \ No newline at end of file diff --git a/test/E2ETest/TyeGenerateTests.cs b/test/E2ETest/TyeGenerateTests.cs index 4c4efb46..2d1c1deb 100644 --- a/test/E2ETest/TyeGenerateTests.cs +++ b/test/E2ETest/TyeGenerateTests.cs @@ -427,5 +427,44 @@ namespace E2ETest await DockerAssert.DeleteDockerImagesAsync(output, projectName); } } + + [ConditionalFact] + [SkipIfDockerNotRunning] + public async Task Generate_DockerFile() + { + var applicationName = "dockerfile"; + var environment = "production"; + var projectName = "frontend-backend"; + + await DockerAssert.DeleteDockerImagesAsync(output, "frontend"); + await DockerAssert.DeleteDockerImagesAsync(output, "backend"); + + using var projectDirectory = TestHelpers.CopyTestProjectDirectory(applicationName); + + var projectFile = new FileInfo(Path.Combine(projectDirectory.DirectoryPath, "tye.yaml")); + + var outputContext = new OutputContext(sink, Verbosity.Debug); + var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); + + try + { + await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false); + + // name of application is the folder + var content = await File.ReadAllTextAsync(Path.Combine(projectDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml")); + var expectedContent = await File.ReadAllTextAsync($"testassets/generate/{applicationName}.yaml"); + + YamlAssert.Equals(expectedContent, content, output); + + await DockerAssert.AssertImageExistsAsync(output, "frontend"); + await DockerAssert.AssertImageExistsAsync(output, "backend"); + } + finally + { + + await DockerAssert.DeleteDockerImagesAsync(output, "frontend"); + await DockerAssert.DeleteDockerImagesAsync(output, "backend"); + } + } } } diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index a7dd8a8d..1f70d793 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -161,7 +161,7 @@ namespace E2ETest var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); // Transform the backend into a docker image for testing - var project = (ProjectServiceBuilder)application.Services.First(s => s.Name == "backend"); + var project = (DotnetProjectServiceBuilder)application.Services.First(s => s.Name == "backend"); application.Services.Remove(project); var outputFileName = project.AssemblyName + ".dll"; @@ -208,7 +208,7 @@ namespace E2ETest var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); // Transform the backend into a docker image for testing - var project = (ProjectServiceBuilder)application.Services.First(s => s.Name == "frontend"); + var project = (DotnetProjectServiceBuilder)application.Services.First(s => s.Name == "frontend"); application.Services.Remove(project); var outputFileName = project.AssemblyName + ".dll"; @@ -257,7 +257,7 @@ namespace E2ETest var application = await ApplicationFactory.CreateAsync(outputContext, projectFile); // Transform the backend into a docker image for testing - var project = (ProjectServiceBuilder)application.Services.First(s => s.Name == "backend-baseimage"); + var project = (DotnetProjectServiceBuilder)application.Services.First(s => s.Name == "backend-baseimage"); // check ContainerInfo values Assert.True(string.Equals(project.ContainerInfo!.BaseImageName, "mcr.microsoft.com/dotnet/core/sdk")); diff --git a/test/E2ETest/testassets/generate/dockerfile.yaml b/test/E2ETest/testassets/generate/dockerfile.yaml new file mode 100644 index 00000000..a8f197ad --- /dev/null +++ b/test/E2ETest/testassets/generate/dockerfile.yaml @@ -0,0 +1,109 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: backend + labels: + app.kubernetes.io/name: 'backend' + app.kubernetes.io/part-of: 'frontend-backend' +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: backend + template: + metadata: + labels: + app.kubernetes.io/name: 'backend' + app.kubernetes.io/part-of: 'frontend-backend' + spec: + containers: + - name: backend + image: backend:latest + imagePullPolicy: Always + env: + - name: ASPNETCORE_URLS + value: 'http://*' + - name: PORT + value: '80' + - name: SERVICE__FRONTEND__PROTOCOL + value: 'http' + - name: SERVICE__FRONTEND__PORT + value: '80' + - name: SERVICE__FRONTEND__HOST + value: 'frontend' + ports: + - containerPort: 80 +... +--- +kind: Service +apiVersion: v1 +metadata: + name: backend + labels: + app.kubernetes.io/name: 'backend' + app.kubernetes.io/part-of: 'frontend-backend' +spec: + selector: + app.kubernetes.io/name: backend + type: ClusterIP + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 +... +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: frontend + labels: + app.kubernetes.io/name: 'frontend' + app.kubernetes.io/part-of: 'frontend-backend' +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: frontend + template: + metadata: + labels: + app.kubernetes.io/name: 'frontend' + app.kubernetes.io/part-of: 'frontend-backend' + spec: + containers: + - name: frontend + image: frontend:1.0.0 + imagePullPolicy: Always + env: + - name: ASPNETCORE_URLS + value: 'http://*' + - name: PORT + value: '80' + - name: SERVICE__BACKEND__PROTOCOL + value: 'http' + - name: SERVICE__BACKEND__PORT + value: '80' + - name: SERVICE__BACKEND__HOST + value: 'backend' + ports: + - containerPort: 80 +... +--- +kind: Service +apiVersion: v1 +metadata: + name: frontend + labels: + app.kubernetes.io/name: 'frontend' + app.kubernetes.io/part-of: 'frontend-backend' +spec: + selector: + app.kubernetes.io/name: frontend + type: ClusterIP + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 +...