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(); }
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."); } }
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}'."); }
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}'."); }
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 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."); } }
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)); }
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))); }
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."); } }
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}'."); }
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")}"); }
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 !)); }
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(); } }
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++; } } } } }