Exemple #1
0
        private static async Task WriteLocalPublishDockerfileAsync(StreamWriter writer, string applicationEntryPoint, ContainerInfo container)
        {
            await writer.WriteLineAsync($"FROM {container.BaseImageName}:{container.BaseImageTag}");

            await writer.WriteLineAsync($"WORKDIR /app");

            await writer.WriteLineAsync($"COPY . /app");

            await writer.WriteLineAsync($"ENTRYPOINT [\"dotnet\", \"{applicationEntryPoint}.dll\"]");
        }
Exemple #2
0
        public static void ApplyContainerDefaults(Application application, ServiceEntry service, Project project, ContainerInfo container)
        {
            if (application is null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            if (service is null)
            {
                throw new ArgumentNullException(nameof(service));
            }

            if (project is null)
            {
                throw new ArgumentNullException(nameof(project));
            }

            if (container is null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            if (container.BaseImageName == null &&
                project.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App"))
            {
                container.BaseImageName = "mcr.microsoft.com/dotnet/core/aspnet";
            }
            else if (container.BaseImageName == null)
            {
                container.BaseImageName = "mcr.microsoft.com/dotnet/core/runtime";
            }

            if (container.BaseImageTag == null &&
                project.TargetFramework == "netcoreapp3.1")
            {
                container.BaseImageTag = "3.1";
            }
            else if (container.BaseImageTag == null &&
                     project.TargetFramework == "netcoreapp3.0")
            {
                container.BaseImageTag = "3.0";
            }

            if (container.BaseImageTag == null)
            {
                throw new CommandException($"Unsupported TFM {project.TargetFramework}.");
            }

            container.BuildImageName ??= "mcr.microsoft.com/dotnet/core/sdk";
            container.BuildImageTag ??= "3.1";

            if (container.ImageName == null && application.Globals.Registry?.Hostname == null)
            {
                container.ImageName ??= service.Service.Name.ToLowerInvariant();
            }
            else if (container.ImageName == null && application.Globals.Registry?.Hostname != null)
            {
                container.ImageName ??= $"{application.Globals.Registry?.Hostname}/{service.Service.Name.ToLowerInvariant()}";
            }

            container.ImageTag ??= project.Version.Replace("+", "-");
        }
Exemple #3
0
        public static async Task WriteDockerfileAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, string filePath)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            if (application is null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            if (service is null)
            {
                throw new ArgumentNullException(nameof(service));
            }

            if (project is null)
            {
                throw new ArgumentNullException(nameof(project));
            }

            if (container is null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            if (filePath is null)
            {
                throw new ArgumentNullException(nameof(filePath));
            }

            using var stream = File.OpenWrite(filePath);
            using var writer = new StreamWriter(stream, encoding: Encoding.UTF8, bufferSize: -1, leaveOpen: true);

            var entryPoint = Path.GetFileNameWithoutExtension(project.RelativeFilePath);

            output.WriteDebugLine($"Writing dockerfile to '{filePath}'.");
            if (container.UseMultiphaseDockerfile ?? true)
            {
                await WriteMultiphaseDockerfileAsync(writer, entryPoint, container);
            }
            else
            {
                await WriteLocalPublishDockerfileAsync(writer, entryPoint, container);
            }
            output.WriteDebugLine("Done writing dockerfile.");
        }
Exemple #4
0
        private static async Task WriteMultiphaseDockerfileAsync(StreamWriter writer, string applicationEntryPoint, ContainerInfo container)
        {
            await writer.WriteLineAsync($"FROM {container.BuildImageName}:{container.BuildImageTag} as SDK");

            await writer.WriteLineAsync($"WORKDIR /src");

            await writer.WriteLineAsync($"COPY . .");

            await writer.WriteLineAsync($"RUN dotnet publish -c Release -o /out");

            await writer.WriteLineAsync($"FROM {container.BaseImageName}:{container.BaseImageTag} as RUNTIME");

            await writer.WriteLineAsync($"WORKDIR /app");

            await writer.WriteLineAsync($"COPY --from=SDK /out .");

            await writer.WriteLineAsync($"ENTRYPOINT [\"dotnet\", \"{applicationEntryPoint}.dll\"]");
        }
Exemple #5
0
        public static async Task GenerateAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            if (application is null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            if (project is null)
            {
                throw new ArgumentNullException(nameof(project));
            }

            if (container is null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            if (chart is null)
            {
                throw new ArgumentNullException(nameof(chart));
            }

            if (outputDirectory is null)
            {
                throw new ArgumentNullException(nameof(outputDirectory));
            }

            ApplyHelmChartDefaults(application, project, container, chart);

            // The directory with the charts needs to be the same as the chart name
            var chartDirectoryPath = Path.Combine(outputDirectory.FullName, chart.ChartName);

            Directory.CreateDirectory(chartDirectoryPath);

            var templateDirectoryPath = Path.Combine(
                Path.GetDirectoryName(typeof(HelmChartGenerator).Assembly.Location) !,
                "Templates",
                "Helm");

            DirectoryCopy.Copy(templateDirectoryPath, chartDirectoryPath);

            // Write Chart.yaml
            //
            // apiVersion: v1
            // name: <appname>
            // version: <version>
            // appVersion: <version>
            await File.WriteAllLinesAsync(Path.Combine(chartDirectoryPath, "Chart.yaml"), new[]
            {
                $"apiVersion: v1",
                $"name: {chart.ChartName}",
                $"# helm requires the version and appVersion to specified in Chart.yaml",
                $"# tye will override these values when packaging the chart",
                $"version: {project.Version.Replace('+', '-')}",
                $"appVersion: {project.Version.Replace('+', '-')}"
            });

            // Write values.yaml
            //
            // image:
            //   repository: rynowak.azurecr.io/rochambot/gamemaster
            await File.WriteAllLinesAsync(Path.Combine(chartDirectoryPath, "values.yaml"), new[]
            {
                $"image:",
                $"  repository: {container.ImageName}",
            });
        }
Exemple #6
0
        public static void ApplyHelmChartDefaults(ApplicationBuilder application, ServiceBuilder service, ContainerInfo container, HelmChartStep chart)
        {
            if (application is null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            if (service is null)
            {
                throw new ArgumentNullException(nameof(service));
            }

            if (container is null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            if (chart is null)
            {
                throw new ArgumentNullException(nameof(chart));
            }

            chart.ChartName ??= service.Name.ToLowerInvariant();
        }
Exemple #7
0
        public static async Task WriteDockerfileAsync(OutputContext output, ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container, string filePath)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            if (application is null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            if (project is null)
            {
                throw new ArgumentNullException(nameof(project));
            }

            if (container is null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            if (filePath is null)
            {
                throw new ArgumentNullException(nameof(filePath));
            }

            await using var stream = File.OpenWrite(filePath);
            await using var writer = new StreamWriter(stream, encoding: new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: -1, leaveOpen: true);

            var entryPoint = project.AssemblyName;

            output.WriteDebugLine($"Writing Dockerfile to '{filePath}'.");
            if (container.UseMultiphaseDockerfile ?? true)
            {
                await WriteMultiphaseDockerfileAsync(writer, entryPoint, container);
            }
            else
            {
                await WriteLocalPublishDockerfileAsync(writer, entryPoint, container);
            }
            output.WriteDebugLine("Done writing Dockerfile.");
        }
Exemple #8
0
        public static void ApplyContainerDefaults(ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container)
        {
            if (application is null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            if (project is null)
            {
                throw new ArgumentNullException(nameof(project));
            }

            if (container is null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            if (string.IsNullOrEmpty(container.BaseImageName) && project.IsAspNet)
            {
                container.BaseImageName = "mcr.microsoft.com/dotnet/core/aspnet";
            }
            else if (string.IsNullOrEmpty(container.BaseImageName))
            {
                container.BaseImageName = "mcr.microsoft.com/dotnet/core/runtime";
            }

            if (string.IsNullOrEmpty(container.BaseImageTag) && project.TargetFrameworkName == "netcoreapp")
            {
                container.BaseImageTag = project.TargetFrameworkVersion;
            }

            if (string.IsNullOrEmpty(container.BaseImageTag))
            {
                throw new CommandException($"Unsupported TFM {project.TargetFramework}.");
            }

            container.BuildImageName ??= "mcr.microsoft.com/dotnet/core/sdk";
            container.BuildImageTag ??= "3.1";

            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 ??= project.Version?.Replace("+", "-") ?? "latest";
        }
        public static async Task BuildContainerImageAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            if (application is null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            if (service is null)
            {
                throw new ArgumentNullException(nameof(service));
            }

            if (project is null)
            {
                throw new ArgumentNullException(nameof(project));
            }

            if (container is null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            using var tempFile = TempFile.Create();

            var dockerFilePath = Path.Combine(application.GetProjectDirectory(project), Path.GetDirectoryName(project.RelativeFilePath) !, "Dockerfile");

            if (File.Exists(dockerFilePath))
            {
                output.WriteDebugLine($"Using existing dockerfile '{dockerFilePath}'.");
            }
            else
            {
                await DockerfileGenerator.WriteDockerfileAsync(output, application, service, project, container, tempFile.FilePath);

                dockerFilePath = tempFile.FilePath;
            }

            // We need to know if this is a single-phase or multi-phase dockerfile because the context directory will be
            // different depending on that choice.
            string contextDirectory;

            if (container.UseMultiphaseDockerfile ?? true)
            {
                contextDirectory = ".";
            }
            else
            {
                var publishOutput = service.Outputs.OfType <ProjectPublishOutput>().FirstOrDefault();
                if (publishOutput is null)
                {
                    throw new InvalidOperationException("We should have published the project for a single-phase dockerfile.");
                }

                contextDirectory = publishOutput.Directory.FullName;
            }

            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}\"",
                application.GetProjectDirectory(project),
                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}'");
            service.Outputs.Add(new DockerImageOutput(container.ImageName !, container.ImageTag !));
        }
Exemple #10
0
        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";
        }
Exemple #11
0
        internal static async Task<TemporaryApplicationAdapter> CreateApplicationAdapterAsync(OutputContext output, ConfigApplication application, bool interactive, bool requireRegistry)
        {
            var globals = new ApplicationGlobals()
            {
                Name = application.Name,
                Registry = application.Registry is null ? null : new ContainerRegistry(application.Registry),
            };

            var services = new List<Tye.ServiceEntry>();
            foreach (var configService in application.Services)
            {
                if (configService.Project is string projectFile)
                {
                    var fullPathProjectFile = Path.Combine(application.Source.DirectoryName, projectFile);
                    var project = new Project(fullPathProjectFile);
                    var service = new Service(configService.Name)
                    {
                        Source = project,
                    };

                    service.Replicas = configService.Replicas ?? 1;

                    foreach (var configBinding in configService.Bindings)
                    {
                        var binding = new ServiceBinding(configBinding.Name ?? service.Name)
                        {
                            ConnectionString = configBinding.ConnectionString,
                            Host = configBinding.Host,
                            Port = configBinding.Port,
                            Protocol = configBinding.Protocol,
                        };

                        binding.Protocol ??= "http";

                        if (binding.Port == null && configBinding.AutoAssignPort)
                        {
                            if (binding.Protocol == "http" || binding.Protocol == null)
                            {
                                binding.Port = 80;
                            }
                            else if (binding.Protocol == "https")
                            {
                                binding.Port = 443;
                            }
                        }

                        service.Bindings.Add(binding);
                    }

                    var serviceEntry = new ServiceEntry(service, configService.Name);

                    await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(fullPathProjectFile), project);

                    var container = new ContainerInfo()
                    {
                        UseMultiphaseDockerfile = false,
                    };
                    service.GeneratedAssets.Container = container;
                    services.Add(serviceEntry);
                }
                else
                {
                    // For a non-project, we don't really need much info about it, just the name and bindings
                    var service = new Service(configService.Name);
                    foreach (var configBinding in configService.Bindings)
                    {
                        service.Bindings.Add(new ServiceBinding(configBinding.Name ?? service.Name)
                        {
                            ConnectionString = configBinding.ConnectionString,
                            Host = configBinding.Host,
                            Port = configBinding.Port,
                            Protocol = configBinding.Protocol,
                        });
                    }

                    var serviceEntry = new ServiceEntry(service, configService.Name);
                    services.Add(serviceEntry);
                }
            }

            var temporaryApplication = new TemporaryApplicationAdapter(application, globals, services);
            if (temporaryApplication.Globals.Registry?.Hostname == null && interactive)
            {
                var registry = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)", allowEmpty: !requireRegistry);
                if (!string.IsNullOrWhiteSpace(registry))
                {
                    temporaryApplication.Globals.Registry = new ContainerRegistry(registry.Trim());
                }
            }
            else if (temporaryApplication.Globals.Registry?.Hostname == null && requireRegistry)
            {
                throw new CommandException("A registry is required for deploy operations. Add the registry to 'tye.yaml' or use '-i' for interactive mode.");
            }
            else
            {
                // No registry specified, and that's OK!
            }

            foreach (var service in temporaryApplication.Services)
            {
                if (service.Service.Source is Project project && service.Service.GeneratedAssets.Container is ContainerInfo container)
                {
                    DockerfileGenerator.ApplyContainerDefaults(temporaryApplication, service, project, container);
                }
            }

            return temporaryApplication;
        }
Exemple #12
0
        public static async Task BuildContainerImageAsync(OutputContext output, ApplicationBuilder application, DotnetProjectServiceBuilder project, ContainerInfo container)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            if (application is null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            if (project is null)
            {
                throw new ArgumentNullException(nameof(project));
            }

            if (container is null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            string contextDirectory;
            var    dockerFilePath = Path.Combine(project.ProjectFile.DirectoryName !, "Dockerfile");

            TempFile?     tempFile      = null;
            TempDirectory?tempDirectory = null;

            try
            {
                // We need to know if this is a single-phase or multi-phase Dockerfile because the context directory will be
                // different depending on that choice.
                //
                // For the cases where generate a Dockerfile, we have the constraint that we need
                // to place it on the same drive (Windows) as the docker context.
                if (container.UseMultiphaseDockerfile ?? true)
                {
                    // For a multi-phase Docker build, the context is always the project directory.
                    contextDirectory = ".";

                    if (File.Exists(dockerFilePath))
                    {
                        output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'.");
                    }
                    else
                    {
                        // We need to write the file, let's stick it under obj.
                        Directory.CreateDirectory(project.IntermediateOutputPath);
                        dockerFilePath = Path.Combine(project.IntermediateOutputPath, "Dockerfile");

                        // Clean up file when done building image
                        tempFile = new TempFile(dockerFilePath);

                        await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, tempFile.FilePath);
                    }
                }
                else
                {
                    // For a single-phase Docker build the context is always the directory containing the publish
                    // output. We need to put the Dockerfile in the context directory so it's on the same drive (Windows).
                    var publishOutput = project.Outputs.OfType <ProjectPublishOutput>().FirstOrDefault();
                    if (publishOutput is null)
                    {
                        throw new InvalidOperationException("We should have published the project for a single-phase Dockerfile.");
                    }

                    contextDirectory = publishOutput.Directory.FullName;

                    // Clean up directory when done building image
                    tempDirectory = new TempDirectory(publishOutput.Directory);

                    if (File.Exists(dockerFilePath))
                    {
                        output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'.");
                        File.Copy(dockerFilePath, Path.Combine(contextDirectory, "Dockerfile"));
                        dockerFilePath = Path.Combine(contextDirectory, "Dockerfile");
                    }
                    else
                    {
                        // No need to clean up, it's in a directory we're already cleaning up.
                        dockerFilePath = Path.Combine(contextDirectory, "Dockerfile");
                        await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, 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 application.ContainerEngine.ExecuteAsync(
                    $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"",
                    project.ProjectFile.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}'");
                project.Outputs.Add(new DockerImageOutput(container.ImageName !, container.ImageTag !));
            }
            finally
            {
                tempDirectory?.Dispose();
                tempFile?.Dispose();
            }
        }
Exemple #13
0
        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 application.ContainerEngine.ExecuteAsync(
                $"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 !));
        }
Exemple #14
0
        public static async Task BuildHelmChartAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, HelmChartStep chart)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            if (application is null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            if (service is null)
            {
                throw new ArgumentNullException(nameof(service));
            }

            if (project is null)
            {
                throw new ArgumentNullException(nameof(project));
            }

            if (container is null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            if (chart is null)
            {
                throw new ArgumentNullException(nameof(chart));
            }

            var projectDirectory    = Path.Combine(application.RootDirectory, Path.GetDirectoryName(project.RelativeFilePath) !);
            var outputDirectoryPath = Path.Combine(projectDirectory, "bin");

            using var tempDirectory = TempDirectory.Create();

            HelmChartGenerator.ApplyHelmChartDefaults(application, service, container, chart);

            var chartRoot = Path.Combine(projectDirectory, "charts");
            var chartPath = Path.Combine(chartRoot, chart.ChartName);

            output.WriteDebugLine($"Looking for existing chart in '{chartPath}'.");
            if (Directory.Exists(chartPath))
            {
                output.WriteDebugLine($"Found existing chart in '{chartPath}'.");
            }
            else
            {
                chartRoot = tempDirectory.DirectoryPath;
                chartPath = Path.Combine(chartRoot, chart.ChartName);
                output.WriteDebugLine($"Generating chart in '{chartPath}'.");
                await HelmChartGenerator.GenerateAsync(output, application, service, project, container, chart, new DirectoryInfo(tempDirectory.DirectoryPath));
            }

            output.WriteDebugLine("Running 'helm package'.");
            output.WriteCommandLine("helm", $"package -d \"{outputDirectoryPath}\" --version {project.Version.Replace('+', '-')} --app-version {project.Version.Replace('+', '-')}");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                "helm",
                $"package . -d \"{outputDirectoryPath}\" --version {project.Version.Replace('+', '-')} --app-version {project.Version.Replace('+', '-')}",
                workingDir : chartPath,
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Running 'helm package' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("Running 'helm package' failed.");
            }

            output.WriteInfoLine($"Created Helm Chart: {Path.Combine(outputDirectoryPath, chart.ChartName + "-" + project.Version.Replace('+', '-') + ".tgz")}");
        }
        public override async Task <object?> ExecuteAsync()
        {
            if (callbacks.Count == 0)
            {
                return(Task.FromResult <object?>(null));
            }

            if (callbacks.Count > 1)
            {
                throw new InvalidOperationException("More than one application type is not supported.");
            }

            var kvp = callbacks.Single();

            var type      = kvp.Key;
            var delegates = kvp.Value;

            output.WriteDebugLine($"Creating instance of application type '{type}'.");
            var application = Activator.CreateInstance(type);

            output.WriteDebugLine($"Done creating instance of application type '{type}'.");

            var wrapper = new ApplicationWrapper(application !, rootDirectory);

            wrapper.Globals.Name ??= name;

            foreach (var service in wrapper.Services)
            {
                output.WriteDebugLine($"Found service '{service.FriendlyName} {{ Name: {service.Service.Name} }}'.");

                string?projectRelativeFilePath = null;
                string?projectFilePath         = null;
                if (solution != null)
                {
                    var project = FindProjectInSolution(solution, service.FriendlyName);
                    if (project == null)
                    {
                        output.WriteDebugLine($"Could not find project for service '{service.FriendlyName}'.");
                        continue;
                    }

                    output.WriteDebugLine($"Found project '{project.RelativePath}' for service '{service.FriendlyName}'.");
                    projectRelativeFilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar);
                    projectFilePath         = project.AbsolutePath.Replace('\\', Path.DirectorySeparatorChar);
                }
                else if (projectFile != null)
                {
                    var normalized = Names.NormalizeToFriendly(Path.GetFileNameWithoutExtension(projectFile.Name));
                    if (!string.Equals(normalized, service.FriendlyName))
                    {
                        output.WriteDebugLine($"Skipping service '{service.FriendlyName}'.");
                        continue;
                    }

                    projectRelativeFilePath = projectFile.FullName;
                    projectFilePath         = projectFile.FullName;
                }

                if (projectFilePath != null)
                {
                    var project = new Project(projectRelativeFilePath !);
                    await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(projectFilePath), project);

                    service.Service.Source = project;

                    // Apply defaults to everything that has a project.
                    var container = new ContainerInfo();
                    service.Service.GeneratedAssets.Container = container;
                    DockerfileGenerator.ApplyContainerDefaults(wrapper, service, project, container);
                }
            }

            output.WriteDebugLine($"Running {delegates.Count} customization callbacks.");
            for (var i = 0; i < delegates.Count; i++)
            {
                delegates[i].DynamicInvoke(application);
            }
            output.WriteDebugLine($"Done running {delegates.Count} customization callbacks.");


            return(application);
        }