private static async Task DeployApplicationManifestAsync(OutputContext output, ApplicationBuilder application, string applicationName, string environment)
        {
            using var step = output.BeginStep("Deploying Application Manifests...");

            using var tempFile = TempFile.Create();
            output.WriteInfoLine($"Writing output to '{tempFile.FilePath}'.");

            {
                using var stream = File.OpenWrite(tempFile.FilePath);
                using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);

                await ApplicationYamlWriter.WriteAsync(output, writer, application);
            }

            output.WriteDebugLine("Running 'kubectl apply'.");
            output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"kubectl",
                $"apply -f \"{tempFile.FilePath}\"",
                System.Environment.CurrentDirectory,
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("'kubectl apply' failed.");
            }

            output.WriteInfoLine($"Deployed application '{applicationName}'.");

            step.MarkComplete();
        }
Example #2
0
        public static async Task ExecuteAsync(OutputContext output, string imageName, string imageTag)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

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

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

            output.WriteDebugLine("Running 'docker push'.");
            output.WriteCommandLine("docker", $"push {imageName}:{imageTag}");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"docker",
                $"push {imageName}:{imageTag}",
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'docker push' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("'docker push' failed.");
            }
        }
Example #3
0
        public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
        {
            var yaml = service.Outputs.OfType <IYamlManifestOutput>().ToArray();

            if (yaml.Length == 0)
            {
                output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping.");
                return;
            }

            if (!await KubectlDetector.Instance.IsKubectlInstalled.Value)
            {
                throw new CommandException($"Cannot apply manifests for '{service.Service.Name}' because kubectl is not installed.");
            }

            if (!await KubectlDetector.Instance.IsKubectlConnectedToCluster.Value)
            {
                throw new CommandException($"Cannot apply manifests for '{service.Service.Name}' because kubectl is not connected to a cluster.");
            }

            using var tempFile = TempFile.Create();
            output.WriteDebugLine($"Writing output to '{tempFile.FilePath}'.");

            {
                using var stream = File.OpenWrite(tempFile.FilePath);
                using var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: -1, leaveOpen: true);
                var yamlStream = new YamlStream(yaml.Select(y => y.Yaml));
                yamlStream.Save(writer, assignAnchors: false);
            }

            // kubectl apply logic is implemented in the client in older versions of k8s. The capability
            // to get the same behavior in the server isn't present in every version that's relevant.
            //
            // https://kubernetes.io/docs/reference/using-api/api-concepts/#server-side-apply
            //
            output.WriteDebugLine("Running 'kubectl apply'.");
            output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"kubectl",
                $"apply -f \"{tempFile.FilePath}\"",
                System.Environment.CurrentDirectory,
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("'kubectl apply' failed.");
            }

            output.WriteInfoLine($"Deployed service '{service.FriendlyName}'.");
        }
Example #4
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application)
        {
            using var step = output.BeginStep("");

            if (!await KubectlDetector.IsKubectlInstalledAsync(output))
            {
                throw new CommandException($"Cannot apply manifests because kubectl is not installed.");
            }

            if (!await KubectlDetector.IsKubectlConnectedToClusterAsync(output))
            {
                throw new CommandException($"Cannot apply manifests because kubectl is not connected to a cluster.");
            }

            using var tempFile = TempFile.Create();
            output.WriteInfoLine($"Writing output to '{tempFile.FilePath}'.");

            {
                await using var stream = File.OpenWrite(tempFile.FilePath);
                await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), leaveOpen: true);

                await ApplicationYamlWriter.WriteAsync(output, writer, application);
            }

            var ns = $"namespace ${application.Namespace}";

            if (string.IsNullOrEmpty(application.Namespace))
            {
                ns = "current namespace";
            }
            output.WriteDebugLine($"Running 'kubectl apply' in ${ns}");
            output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"kubectl",
                $"apply -f \"{tempFile.FilePath}\"",
                System.Environment.CurrentDirectory,
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("'kubectl apply' failed.");
            }

            output.WriteInfoLine($"Deployed application '{application.Name}'.");
        }
Example #5
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 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 !));
        }
Example #6
0
        public static async Task ExecuteAsync(OutputContext output, string registryHostname, string chartFilePath)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

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

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

            // NOTE: this is currently hardcoded to use ACR and uses the azure CLI to do
            // the push operation. Helm 3 has support for pushing to OCI repositories,
            // but it's not documented that it works yet for azure.
            if (!registryHostname.EndsWith(".azurecr.io"))
            {
                throw new CommandException("Currently pushing of helm charts is limited to '*.azurecr.io'.");
            }

            var registryName = registryHostname.Substring(0, registryHostname.Length - ".azurecr.io".Length);

            output.WriteDebugLine("Running 'az acr helm push'.");
            output.WriteCommandLine("az", $"acr helm push --name {registryName} \"{chartFilePath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"az",
                $"acr helm push --name {registryName} \"{chartFilePath}\"",
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'az acr helm push' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("'az acr helm push' failed.");
            }
        }
Example #7
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
        {
            if (SkipWithoutProject(output, service, out var project))
            {
                return;
            }

            if (SkipWithoutContainerInfo(output, service, out var container))
            {
                return;
            }

            if (container.UseMultiphaseDockerfile != false)
            {
                return;
            }

            // NOTE: we're intentionally not cleaning up here. It's the responsibility of whomever consumes
            // the publish output to do cleanup.
            var outputDirectory = TempDirectory.Create();

            output.WriteDebugLine("Running 'dotnet publish'.");
            output.WriteCommandLine("dotnet", $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory.DirectoryPath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"dotnet",
                $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory.DirectoryPath}\"",
                project.ProjectFile.DirectoryName,
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'dotnet publish' exit code: {exitCode}");
            if (exitCode != 0)
            {
                outputDirectory.Dispose();
                throw new CommandException("'dotnet publish' failed.");
            }

            output.WriteDebugLine($"Created Publish Output: '{outputDirectory.DirectoryPath}'");
            service.Outputs.Add(new ProjectPublishOutput(outputDirectory.DirectoryInfo));
        }
Example #8
0
        public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
        {
            if (SkipWithoutProject(output, service, out var project))
            {
                return;
            }

            if (SkipWithoutContainerInfo(output, service, out var container))
            {
                return;
            }

            if (container.UseMultiphaseDockerfile != false)
            {
                return;
            }

            var projectFilePath = Path.Combine(application.RootDirectory, project.RelativeFilePath);
            var outputDirectory = Path.Combine(Path.GetDirectoryName(projectFilePath) !, "bin", "Release", project.TargetFramework, "publish");

            output.WriteDebugLine("Running 'dotnet publish'.");
            output.WriteCommandLine("dotnet", $"publish \"{projectFilePath}\" -c Release -o \"{outputDirectory}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"dotnet",
                $"publish \"{projectFilePath}\" -c Release -o \"{outputDirectory}\"",
                application.GetProjectDirectory(project),
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'dotnet publish' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("'dotnet publish' failed.");
            }

            output.WriteInfoLine($"Created Publish Output: '{outputDirectory}'");
            service.Outputs.Add(new ProjectPublishOutput(new DirectoryInfo(outputDirectory)));
        }
Example #9
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingress)
        {
            // This code assumes that in the future we might support other ingress types besides nginx.
            //
            // Right now we only know some hardcoded details about ingress-nginx that we use for both
            // validation and generation of manifests.
            //
            // For instance we don't support k8s 1.18.X IngressClass resources because that version
            // isn't broadly available yet.
            var ingressClass = "nginx";

            if (!IngressClasses.Add(ingressClass))
            {
                output.WriteDebugLine($"Already validated ingress class '{ingressClass}'.");
                return;
            }

            if (await KubectlDetector.GetKubernetesServerVersion(output) == null)
            {
                throw new CommandException($"Cannot validate ingress because kubectl is not installed.");
            }

            if (!await KubectlDetector.IsKubectlConnectedToClusterAsync(output))
            {
                throw new CommandException($"Cannot validate ingress because kubectl is not connected to a cluster.");
            }

            output.WriteDebugLine($"Validating ingress class '{ingressClass}'.");
            var config = KubernetesClientConfiguration.BuildDefaultConfig();

            // If namespace is null, set it to default
            config.Namespace ??= "default";

            var kubernetes = new Kubernetes(config);

            // Looking for a deployment using a standard label.
            // Note: using a deployment instead of a service - minikube doesn't create a service for the controller.

            try
            {
                var result = await kubernetes.ListDeploymentForAllNamespacesWithHttpMessagesAsync(
                    labelSelector : "app.kubernetes.io/name in (ingress-nginx, nginx-ingress-controller)");

                if (result.Body.Items.Count > 0)
                {
                    foreach (var service in result.Body.Items)
                    {
                        output.WriteInfoLine($"Found existing ingress controller '{service.Metadata.Name}' in namespace '{service.Metadata.NamespaceProperty}'.");
                    }

                    return;
                }
            }
            catch (HttpOperationException ex) when(ex.Response.StatusCode == HttpStatusCode.NotFound)
            {
                // The kubernetes client uses exceptions for 404s.
            }
            catch (Exception ex)
            {
                output.WriteDebugLine("Failed to query secret.");
                output.WriteDebugLine(ex.ToString());
                throw new CommandException("Unable connect to kubernetes.", ex);
            }

            if (Force)
            {
                output.WriteDebugLine("Skipping because force was specified.");
                return;
            }

            if (!Interactive)
            {
                throw new CommandException(
                          $"An ingress was specified for the application, but the 'ingress-nginx' controller could not be found. " +
                          $"Rerun the command with --interactive to deploy a controller for development, or with --force to skip validation. Alternatively " +
                          $"see our documentation on ingress: https://aka.ms/tye/ingress");
            }

            output.WriteAlwaysLine(
                "Tye can deploy the ingress-nginx controller for you. This will be a basic deployment suitable for " +
                "experimentation and development. Your production needs, or requirements may differ depending on your Kubernetes distribution. " +
                "See: https://aka.ms/tye/ingress for documentation.");
            if (!output.Confirm($"Deploy ingress-nginx"))
            {
                // user skipped deployment of ingress, continue with deployment.
                return;
            }

            // We want to be able to detect minikube because the process for enabling nginx-ingress is different there,
            // it's shipped as an addon.
            //
            // see: https://github.com/telepresenceio/telepresence/blob/4364fd83d5926bef46babd704e7bd6c82a75dbd6/telepresence/startup.py#L220
            if (config.CurrentContext == "minikube")
            {
                output.WriteDebugLine($"Running 'minikube addons enable ingress'");
                output.WriteCommandLine("minikube", "addon enable ingress");
                var capture  = output.Capture();
                var exitCode = await ProcessUtil.ExecuteAsync(
                    $"minikube",
                    $"addons enable ingress",
                    System.Environment.CurrentDirectory,
                    stdOut : capture.StdOut,
                    stdErr : capture.StdErr);

                output.WriteDebugLine($"Done running 'minikube addons enable ingress' exit code: {exitCode}");
                if (exitCode != 0)
                {
                    throw new CommandException("'minikube addons enable ingress' failed.");
                }

                output.WriteInfoLine($"Deployed ingress-nginx.");
            }
            else
            {
                // If we get here then we should deploy the ingress controller.

                // The first time we apply the ingress controller, the validating webhook will not have started.
                // This causes an error to be returned from the process. As this always happens, we are going to
                // not check the error returned and assume the kubectl command worked. This is double checked in
                // the future as well when we try to create the ingress resource.

                output.WriteDebugLine($"Running 'kubectl apply'");
                output.WriteCommandLine("kubectl", $"apply -f \"https://aka.ms/tye/ingress/deploy\"");
                var capture  = output.Capture();
                var exitCode = await ProcessUtil.ExecuteAsync(
                    $"kubectl",
                    $"apply -f \"https://aka.ms/tye/ingress/deploy\"",
                    System.Environment.CurrentDirectory);

                output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}");

                output.WriteInfoLine($"Waiting for ingress-nginx controller to start.");

                // We need to then wait for the webhooks that are created by ingress-nginx to start. Deploying an ingress immediately
                // after creating the controller will fail if the webhook isn't ready.
                //
                // Internal error occurred: failed calling webhook "validate.nginx.ingress.kubernetes.io":
                // Post https://ingress-nginx-controller-admission.ingress-nginx.svc:443/networking.k8s.io/v1/ingresses?timeout=30s:
                // dial tcp 10.0.31.130:443: connect: connection refused
                //
                // Unfortunately this is the likely case for us.

                try
                {
                    output.WriteDebugLine("Watching for ingress-nginx controller readiness...");
                    var response = await kubernetes.ListNamespacedPodWithHttpMessagesAsync(
                        namespaceParameter : "ingress-nginx",
                        labelSelector : "app.kubernetes.io/component=controller,app.kubernetes.io/name=ingress-nginx",
                        watch : true);

                    var tcs = new TaskCompletionSource <object?>();
                    using var watcher = response.Watch <V1Pod, V1PodList>(
                              onEvent: (@event, pod) =>
                    {
                        // Wait for the readiness-check to pass.
                        if (pod.Status.Conditions.All(c => string.Equals(c.Status, bool.TrueString, StringComparison.OrdinalIgnoreCase)))
                        {
                            tcs.TrySetResult(null);     // Success!
                            output.WriteDebugLine($"Pod '{pod.Metadata.Name}' is ready.");
                        }
                    },
                              onError: ex =>
                    {
                        tcs.TrySetException(ex);
                        output.WriteDebugLine("Watch operation failed.");
                    },
                              onClosed: () =>
                    {
                        // YOLO?
                        tcs.TrySetResult(null);
                        output.WriteDebugLine("Watch operation completed.");
                    });

                    await tcs.Task;
                }
                catch (Exception ex)
                {
                    output.WriteDebugLine("Failed to ingress-nginx pods.");
                    output.WriteDebugLine(ex.ToString());
                    throw new CommandException("Failed to query ingress-nginx pods.", ex);
                }

                output.WriteInfoLine($"Deployed ingress-nginx.");
            }
        }
Example #10
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
        {
            var bindings = service.Outputs.OfType <ComputedBindings>().FirstOrDefault();

            if (bindings is null)
            {
                return;
            }

            foreach (var binding in bindings.Bindings)
            {
                if (binding is SecretInputBinding secretInputBinding)
                {
                    if (!Secrets.Add(secretInputBinding.Name))
                    {
                        output.WriteDebugLine($"Already validated secret '{secretInputBinding.Name}'.");
                        continue;
                    }

                    output.WriteDebugLine($"Validating secret '{secretInputBinding.Name}'.");

                    var config = KubernetesClientConfiguration.BuildDefaultConfig();

                    // Workaround for https://github.com/kubernetes-client/csharp/issues/372
                    var store = await KubernetesClientConfiguration.LoadKubeConfigAsync();

                    var context = store.Contexts.Where(c => c.Name == config.CurrentContext).FirstOrDefault();
                    config.Namespace ??= context?.ContextDetails?.Namespace;

                    var kubernetes = new Kubernetes(config);

                    try
                    {
                        var result = await kubernetes.ReadNamespacedSecretWithHttpMessagesAsync(secretInputBinding.Name, config.Namespace ?? "default");

                        output.WriteInfoLine($"Found existing secret '{secretInputBinding.Name}'.");
                        continue;
                    }
                    catch (HttpOperationException ex) when(ex.Response.StatusCode == HttpStatusCode.NotFound)
                    {
                        // The kubernetes client uses exceptions for 404s.
                    }
                    catch (Exception ex)
                    {
                        output.WriteDebugLine("Failed to query secret.");
                        output.WriteDebugLine(ex.ToString());
                        throw new CommandException("Unable connect to kubernetes.", ex);
                    }

                    if (Force)
                    {
                        output.WriteDebugLine("Skipping because force was specified.");
                        continue;
                    }

                    if (!Interactive)
                    {
                        throw new CommandException(
                                  $"The secret '{secretInputBinding.Name}' used for service '{secretInputBinding.Service.Name}' is missing from the deployment environment. " +
                                  $"Rerun the command with --interactive to specify the value interactively, or with --force to skip validation. Alternatively " +
                                  $"use the following command to manually create the secret." + System.Environment.NewLine +
                                  $"kubectl create secret generic {secretInputBinding.Name} --from-literal=connectionstring=<value>");
                    }

                    // If we get here then we should create the secret.
                    var text = output.Prompt($"Enter the connection string to use for service '{secretInputBinding.Service.Name}'", allowEmpty: true);
                    if (string.IsNullOrWhiteSpace(text))
                    {
                        output.WriteAlways($"Skipping creation of secret for '{secretInputBinding.Service.Name}'. This may prevent creation of pods until secrets are created.");
                        output.WriteAlways($"Manually create a secret with:");
                        output.WriteAlways($"kubectl create secret generic {secretInputBinding.Name} --from-literal=connectionstring=<value>");
                        continue;
                    }

                    var secret = new V1Secret(type: "Opaque", stringData: new Dictionary <string, string>()
                    {
                        { "connectionstring", text },
                    });
                    secret.Metadata      = new V1ObjectMeta();
                    secret.Metadata.Name = secretInputBinding.Name;

                    output.WriteDebugLine($"Creating secret '{secret.Metadata.Name}'.");

                    try
                    {
                        await kubernetes.CreateNamespacedSecretWithHttpMessagesAsync(secret, config.Namespace ?? "default");

                        output.WriteInfoLine($"Created secret '{secret.Metadata.Name}'.");
                    }
                    catch (Exception ex)
                    {
                        output.WriteDebugLine("Failed to create secret.");
                        output.WriteDebugLine(ex.ToString());
                        throw new CommandException("Failed to create secret.", ex);
                    }
                }
            }

            var yaml = service.Outputs.OfType <IYamlManifestOutput>().ToArray();

            if (yaml.Length == 0)
            {
                output.WriteDebugLine($"No yaml manifests found for service '{service.Name}'. Skipping.");
                return;
            }

            using var tempFile = TempFile.Create();
            output.WriteDebugLine($"Writing output to '{tempFile.FilePath}'.");

            {
                await using var stream = File.OpenWrite(tempFile.FilePath);
                await using var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: -1, leaveOpen: true);
                var yamlStream = new YamlStream(yaml.Select(y => y.Yaml));
                yamlStream.Save(writer, assignAnchors: false);
            }

            // kubectl apply logic is implemented in the client in older versions of k8s. The capability
            // to get the same behavior in the server isn't present in every version that's relevant.
            //
            // https://kubernetes.io/docs/reference/using-api/api-concepts/#server-side-apply
            //
            output.WriteDebugLine("Running 'kubectl apply'.");
            output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"kubectl",
                $"apply -f \"{tempFile.FilePath}\"",
                System.Environment.CurrentDirectory,
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("'kubectl apply' failed.");
            }

            output.WriteInfoLine($"Deployed service '{service.Name}'.");
        }
Example #11
0
        public static async Task BuildHelmChartAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart)
        {
            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));
            }

            var projectDirectory    = project.ProjectFile.DirectoryName;
            var outputDirectoryPath = Path.Combine(projectDirectory, "bin");

            using var tempDirectory = TempDirectory.Create();

            HelmChartGenerator.ApplyHelmChartDefaults(application, project, 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, 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")}");
        }
Example #12
0
        private static async Task EvaluateMSBuildAsync(OutputContext output, FileInfo projectFile, Project project)
        {
            try
            {
                output.WriteDebugLine("Installing msbuild targets.");
                TargetInstaller.Install(projectFile.FullName);
                output.WriteDebugLine("Installed msbuild targets.");
            }
            catch (Exception ex)
            {
                throw new CommandException("Failed to install targets.", ex);
            }

            var outputFilePath = Path.GetTempFileName();

            try
            {
                var capture     = output.Capture();
                var programRoot = Path.GetDirectoryName(typeof(Program).Assembly.Location);

                var restore = string.Empty;
                if (!File.Exists(Path.Combine(projectFile.DirectoryName, "obj", "project.assets.json")))
                {
                    restore = "/restore";
                }

                output.WriteDebugLine("Running 'dotnet msbuild'.");
                var msbuildCommand = $"msbuild {restore} /t:EvaluateTyeProjectInfo \"/p:TyeTargetLocation={programRoot}\" \"/p:TyeOutputFilePath={outputFilePath}\"";

                output.WriteCommandLine("dotnet", msbuildCommand);
                var exitCode = await Process.ExecuteAsync(
                    $"dotnet",
                    msbuildCommand,
                    workingDir : projectFile.DirectoryName,
                    stdOut : capture.StdOut,
                    stdErr : capture.StdErr);

                output.WriteDebugLine($"Done running 'dotnet msbuild' exit code: {exitCode}");
                if (exitCode != 0)
                {
                    throw new CommandException("'dotnet msbuild' failed.");
                }

                var lines = await File.ReadAllLinesAsync(outputFilePath);

                for (var i = 0; i < lines.Length; i++)
                {
                    var line = lines[i];
                    if (line.StartsWith("version="))
                    {
                        project.Version = line.Substring("version=".Length).Trim();
                        output.WriteDebugLine($"Found application version: {line}");
                        continue;
                    }

                    if (line.StartsWith("tfm"))
                    {
                        project.TargetFramework = line.Substring("tfm=".Length).Trim();
                        output.WriteDebugLine($"Found target framework: {line}");
                        continue;
                    }

                    if (line.StartsWith("frameworks="))
                    {
                        var right = line.Substring("frameworks=".Length).Trim();
                        project.Frameworks.AddRange(right.Split(",").Select(s => new Framework(s)));
                        output.WriteDebugLine($"Found shared frameworks: {line}");
                        continue;
                    }
                }
            }
            finally
            {
                File.Delete(outputFilePath);
            }
        }
        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 !));
        }
Example #14
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();
            }
        }
Example #15
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application)
        {
            using var step = output.BeginStep("Applying Kubernetes Manifests...");

            if (await KubectlDetector.GetKubernetesServerVersion(output) == null)
            {
                throw new CommandException($"Cannot apply manifests because kubectl is not installed.");
            }

            if (!await KubectlDetector.IsKubectlConnectedToClusterAsync(output))
            {
                throw new CommandException($"Cannot apply manifests because kubectl is not connected to a cluster.");
            }

            using var tempFile = TempFile.Create();
            output.WriteInfoLine($"Writing output to '{tempFile.FilePath}'.");

            {
                await using var stream = File.OpenWrite(tempFile.FilePath);
                await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), leaveOpen: true);

                await ApplicationYamlWriter.WriteAsync(output, writer, application);
            }

            var ns = $"namespace ${application.Namespace}";

            if (string.IsNullOrEmpty(application.Namespace))
            {
                ns = "current namespace";
            }
            output.WriteDebugLine($"Running 'kubectl apply' in ${ns}");
            output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"kubectl",
                $"apply -f \"{tempFile.FilePath}\"",
                System.Environment.CurrentDirectory,
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("'kubectl apply' failed.");
            }

            output.WriteInfoLine($"Deployed application '{application.Name}'.");
            if (application.Ingress.Count > 0)
            {
                output.WriteInfoLine($"Waiting for ingress to be deployed. This may take a long time.");
                foreach (var ingress in application.Ingress)
                {
                    using var ingressStep = output.BeginStep($"Retrieving details for {ingress.Name}...");

                    var done = false;

                    Action <string> complete = line =>
                    {
                        done = line != "''";
                        if (done)
                        {
                            output.WriteInfoLine($"IngressIP: {line}");
                        }
                    };

                    var retries = 0;
                    while (!done && retries < 60)
                    {
                        var ingressExitCode = await Process.ExecuteAsync(
                            "kubectl",
                            $"get ingress {ingress.Name} -o jsonpath='{{..ip}}'",
                            Environment.CurrentDirectory,
                            complete,
                            capture.StdErr);

                        if (ingressExitCode != 0)
                        {
                            throw new CommandException("'kubectl get ingress' failed");
                        }

                        if (!done)
                        {
                            await Task.Delay(2000);

                            retries++;
                        }
                    }
                }
            }
        }