public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { if (SkipWithoutProject(output, service, out var project)) { return(Task.CompletedTask); } if (SkipWithoutContainerInfo(output, service, out var container)) { return(Task.CompletedTask); } if (project is DotnetProjectServiceBuilder dotnetProject) { DockerfileGenerator.ApplyContainerDefaults(application, dotnetProject, container); } else if (project is DockerFileServiceBuilder dockerFile) { DockerfileGenerator.ApplyContainerDefaults(application, dockerFile, container); } return(Task.CompletedTask); }
public async Task ExecuteAsync(ApplicationBuilder application) { foreach (var service in application.Services) { using var tracker = output.BeginStep($"Processing Service '{service.Name}'..."); foreach (var step in ServiceSteps) { using var stepTracker = output.BeginStep(step.DisplayText); await step.ExecuteAsync(output, application, service); stepTracker.MarkComplete(); } tracker.MarkComplete(); } foreach (var ingress in application.Ingress) { using var tracker = output.BeginStep($"Processing Ingress '{ingress.Name}'..."); foreach (var step in IngressSteps) { using var stepTracker = output.BeginStep(step.DisplayText); await step.ExecuteAsync(output, application, ingress); stepTracker.MarkComplete(); } tracker.MarkComplete(); } { foreach (var step in ApplicationSteps) { using var stepTracker = output.BeginStep(step.DisplayText); await step.ExecuteAsync(output, application); stepTracker.MarkComplete(); } } }
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; } var outputDirectory = Path.Combine(project.ProjectFile.DirectoryName, "bin", "Release", project.TargetFramework, "publish"); output.WriteDebugLine("Running 'dotnet publish'."); output.WriteCommandLine("dotnet", $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory}\""); var capture = output.Capture(); var exitCode = await Process.ExecuteAsync( $"dotnet", $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory}\"", project.ProjectFile.DirectoryName, 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 Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { if (SkipWithoutContainerOutput(output, service)) { return(Task.CompletedTask); } if (SkipWithoutProject(output, service, out var project)) { return(Task.CompletedTask); } var deployment = project.ManifestInfo?.Deployment; if (deployment is null) { return(Task.CompletedTask); } // Initialize defaults for deployment-related settings deployment.Labels.TryAdd("app.kubernetes.io/name", project.Name); deployment.Labels.TryAdd("app.kubernetes.io/part-of", application.Name); service.Outputs.Add(KubernetesManifestGenerator.CreateDeployment(output, application, project, deployment)); if (service.Bindings.Count > 0 && project.ManifestInfo?.Service is ServiceManifestInfo k8sService) { // Initialize defaults for service-related settings k8sService.Labels.TryAdd("app.kubernetes.io/name", project.Name); k8sService.Labels.TryAdd("app.kubernetes.io/part-of", application.Name); service.Outputs.Add(KubernetesManifestGenerator.CreateService(output, application, project, deployment, k8sService)); } return(Task.CompletedTask); }
public static async Task ExecuteBuildAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive) { await application.ProcessExtensionsAsync(output, ExtensionContext.OperationKind.Deploy); Program.ApplyRegistry(output, application, interactive, requireRegistry: false); var executor = new ApplicationExecutor(output) { ServiceSteps = { new ApplyContainerDefaultsStep(), new CombineStep() { Environment = environment, }, new PublishProjectStep(), new BuildDockerImageStep() { Environment = environment, }, }, }; await executor.ExecuteAsync(application); }
public static async Task ExecuteBuildAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive) { var steps = new List <ServiceExecutor.Step>() { new CombineStep() { Environment = environment, }, new PublishProjectStep(), new BuildDockerImageStep() { Environment = environment, }, }; Program.ApplyRegistryAndDefaults(output, application, interactive, requireRegistry: false); var executor = new ServiceExecutor(output, application, steps); foreach (var service in application.Services) { await executor.ExecuteAsync(service); } }
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 (!await DockerDetector.Instance.IsDockerInstalled.Value) { throw new CommandException($"Cannot generate a docker image for '{service.Name}' because docker is not installed."); } if (!await DockerDetector.Instance.IsDockerConnectedToDaemon.Value) { throw new CommandException($"Cannot generate a docker image for '{service.Name}' because docker is not running."); } await DockerContainerBuilder.BuildContainerImageAsync(output, application, project, container); }
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(); }
public static KubernetesIngressOutput CreateIngress( OutputContext output, ApplicationBuilder application, IngressBuilder ingress) { var root = new YamlMappingNode(); root.Add("kind", "Ingress"); root.Add("apiVersion", "extensions/v1beta1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", ingress.Name); if (!string.IsNullOrEmpty(application.Namespace)) { metadata.Add("namespace", application.Namespace); } var annotations = new YamlMappingNode(); metadata.Add("annotations", annotations); annotations.Add("kubernetes.io/ingress.class", new YamlScalarNode("nginx") { Style = ScalarStyle.SingleQuoted, }); annotations.Add("nginx.ingress.kubernetes.io/rewrite-target", new YamlScalarNode("/$2") { Style = ScalarStyle.SingleQuoted, }); var labels = new YamlMappingNode(); metadata.Add("labels", labels); labels.Add("app.kubernetes.io/part-of", new YamlScalarNode(application.Name) { Style = ScalarStyle.SingleQuoted, }); var spec = new YamlMappingNode(); root.Add("spec", spec); if (ingress.Rules.Count > 0) { var rules = new YamlSequenceNode(); spec.Add("rules", rules); // k8s ingress is grouped by host first, then grouped by path foreach (var hostgroup in ingress.Rules.GroupBy(r => r.Host)) { var rule = new YamlMappingNode(); rules.Add(rule); if (!string.IsNullOrEmpty(hostgroup.Key)) { rule.Add("host", hostgroup.Key); } var http = new YamlMappingNode(); rule.Add("http", http); var paths = new YamlSequenceNode(); http.Add("paths", paths); foreach (var ingressRule in hostgroup) { var path = new YamlMappingNode(); paths.Add(path); var backend = new YamlMappingNode(); path.Add("backend", backend); backend.Add("serviceName", ingressRule.Service); var service = application.Services.FirstOrDefault(s => s.Name == ingressRule.Service); if (service is null) { throw new InvalidOperationException($"Could not resolve service '{ingressRule.Service}'."); } var binding = service.Bindings.FirstOrDefault(b => b.Name is null || b.Name == "http"); if (binding is null) { throw new InvalidOperationException($"Could not resolve an http binding for service '{service.Name}'."); } backend.Add("servicePort", (binding.Port ?? 80).ToString(CultureInfo.InvariantCulture)); // Tye implements path matching similar to this example: // https://kubernetes.github.io/ingress-nginx/examples/rewrite/ // // Therefore our rewrite-target is set to $2 - we want to make sure we have // two capture groups. if (string.IsNullOrEmpty(ingressRule.Path) || ingressRule.Path == "/") { path.Add("path", "/()(.*)"); // () is an empty capture group. } else { var regex = $"{ingressRule.Path.TrimEnd('/')}(/|$)(.*)"; path.Add("path", regex); } } } } return(new KubernetesIngressOutput(ingress.Name, new YamlDocument(root))); }
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")}"); }
public static ServiceOutput CreateDeployment(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project) { 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)); } var bindings = project.Outputs.OfType <ComputedBindings>().FirstOrDefault(); var root = new YamlMappingNode(); root.Add("kind", "Deployment"); root.Add("apiVersion", "apps/v1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", project.Name); var labels = new YamlMappingNode(); metadata.Add("labels", labels); labels.Add("app.kubernetes.io/name", project.Name); labels.Add("app.kubernetes.io/part-of", application.Name); var spec = new YamlMappingNode(); root.Add("spec", spec); spec.Add("replicas", project.Replicas.ToString()); var selector = new YamlMappingNode(); spec.Add("selector", selector); var matchLabels = new YamlMappingNode(); selector.Add("matchLabels", matchLabels); matchLabels.Add("app.kubernetes.io/name", project.Name); var template = new YamlMappingNode(); spec.Add("template", template); metadata = new YamlMappingNode(); template.Add("metadata", metadata); labels = new YamlMappingNode(); metadata.Add("labels", labels); labels.Add("app.kubernetes.io/name", project.Name); labels.Add("app.kubernetes.io/part-of", application.Name); spec = new YamlMappingNode(); template.Add("spec", spec); var containers = new YamlSequenceNode(); spec.Add("containers", containers); var images = project.Outputs.OfType <DockerImageOutput>(); foreach (var image in images) { var container = new YamlMappingNode(); containers.Add(container); container.Add("name", project.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{image.ImageName}:{image.ImageTag}"); container.Add("imagePullPolicy", "Always"); // helps avoid problems with development + weak versioning if (project.EnvironmentVariables.Count > 0 || // We generate ASPNETCORE_URLS if there are bindings for http project.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) || // We generate environment variables for other services if there dependencies (bindings is object && bindings.Bindings.OfType <EnvironmentVariableInputBinding>().Any())) { var env = new YamlSequenceNode(); container.Add("env", env); foreach (var kvp in project.EnvironmentVariables) { env.Add(new YamlMappingNode() { { "name", kvp.Name }, { "value", new YamlScalarNode(kvp.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } if (bindings is object) { foreach (var binding in bindings.Bindings.OfType <EnvironmentVariableInputBinding>()) { env.Add(new YamlMappingNode() { { "name", binding.Name }, { "value", new YamlScalarNode(binding.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } } } if (bindings is object && bindings.Bindings.OfType <SecretInputBinding>().Any()) { var volumeMounts = new YamlSequenceNode(); container.Add("volumeMounts", volumeMounts); foreach (var binding in bindings.Bindings.OfType <SecretInputBinding>()) { var volumeMount = new YamlMappingNode(); volumeMounts.Add(volumeMount); volumeMount.Add("name", $"{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}"); volumeMount.Add("mountPath", $"/var/tye/bindings/{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}"); volumeMount.Add("readOnly", "true"); } } if (project.Bindings.Count > 0) { var ports = new YamlSequenceNode(); container.Add("ports", ports); foreach (var binding in project.Bindings) { if (binding.Protocol == "https") { // We skip https for now in deployment, because the E2E requires certificates // and we haven't done those features yet. continue; } if (binding.Port != null) { var containerPort = new YamlMappingNode(); ports.Add(containerPort); containerPort.Add("containerPort", binding.Port.Value.ToString()); } } } } if (bindings.Bindings.OfType <SecretInputBinding>().Any()) { var volumes = new YamlSequenceNode(); spec.Add("volumes", volumes); foreach (var binding in bindings.Bindings.OfType <SecretInputBinding>()) { var volume = new YamlMappingNode(); volumes.Add(volume); volume.Add("name", $"{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}"); var secret = new YamlMappingNode(); volume.Add("secret", secret); secret.Add("secretName", binding.Name !); var items = new YamlSequenceNode(); secret.Add("items", items); var item = new YamlMappingNode(); items.Add(item); item.Add("key", "connectionstring"); item.Add("path", binding.Filename); } } return(new KubernetesDeploymentOutput(project.Name, new YamlDocument(root))); }
public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service);
public static ServiceOutput CreateService(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { 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)); } var root = new YamlMappingNode(); root.Add("kind", "Service"); root.Add("apiVersion", "v1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", service.Name); var labels = new YamlMappingNode(); metadata.Add("labels", labels); labels.Add("app.kubernetes.io/name", service.Name); labels.Add("app.kubernetes.io/part-of", application.Name); var spec = new YamlMappingNode(); root.Add("spec", spec); var selector = new YamlMappingNode(); spec.Add("selector", selector); selector.Add("app.kubernetes.io/name", service.Name); spec.Add("type", "ClusterIP"); var ports = new YamlSequenceNode(); spec.Add("ports", ports); // We figure out the port based on bindings foreach (var binding in service.Bindings) { if (binding.Protocol == "https") { // We skip https for now in deployment, because the E2E requires certificates // and we haven't done those features yet. continue; } if (binding.Port != null) { var port = new YamlMappingNode(); ports.Add(port); port.Add("name", binding.Name ?? binding.Protocol ?? "http"); port.Add("protocol", "TCP"); // we use assume TCP. YOLO port.Add("port", binding.Port.Value.ToString()); port.Add("targetPort", binding.Port.Value.ToString()); } } return(new KubernetesServiceOutput(service.Name, new YamlDocument(root))); }
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."); } }
private static async Task GenerateApplicationManifestAsync(OutputContext output, ApplicationBuilder application, string environment) { using var step = output.BeginStep("Generating Application Manifests..."); var outputFilePath = Path.GetFullPath(Path.Combine(application.Source.DirectoryName, $"{application.Name}-generate-{environment}.yaml")); output.WriteInfoLine($"Writing output to '{outputFilePath}'."); { File.Delete(outputFilePath); await using var stream = File.OpenWrite(outputFilePath); await using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true); await ApplicationYamlWriter.WriteAsync(output, writer, application); } step.MarkComplete(); }
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 KubernetesServiceOutput CreateService( OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, DeploymentManifestInfo deployment, ServiceManifestInfo service) { var root = new YamlMappingNode(); root.Add("kind", "Service"); root.Add("apiVersion", "v1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", project.Name); if (!string.IsNullOrEmpty(application.Namespace)) { metadata.Add("namespace", application.Namespace); } if (service.Annotations.Count > 0) { var annotations = new YamlMappingNode(); metadata.Add("annotations", annotations); foreach (var annotation in service.Annotations) { annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value) { Style = ScalarStyle.SingleQuoted, }); } } var labels = new YamlMappingNode(); metadata.Add("labels", labels); foreach (var label in service.Labels) { labels.Add(label.Key, new YamlScalarNode(label.Value) { Style = ScalarStyle.SingleQuoted, }); } var spec = new YamlMappingNode(); root.Add("spec", spec); var selector = new YamlMappingNode(); spec.Add("selector", selector); // We need the name so we can use it with selector. if (!deployment.Labels.TryGetValue("app.kubernetes.io/name", out var selectorLabelValue)) { throw new InvalidOperationException("The label 'app.kubernetes.io/name` is required."); } selector.Add("app.kubernetes.io/name", selectorLabelValue); spec.Add("type", "ClusterIP"); var ports = new YamlSequenceNode(); spec.Add("ports", ports); // We figure out the port based on bindings foreach (var binding in project.Bindings) { if (binding.Protocol == "https") { // We skip https for now in deployment, because the E2E requires certificates // and we haven't done those features yet. continue; } if (binding.Port != null) { var port = new YamlMappingNode(); ports.Add(port); port.Add("name", binding.Name ?? binding.Protocol ?? "http"); port.Add("protocol", "TCP"); // we use assume TCP. YOLO port.Add("port", binding.Port.Value.ToString()); port.Add("targetPort", binding.Port.Value.ToString()); } } return(new KubernetesServiceOutput(project.Name, new YamlDocument(root))); }
public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source) { if (source is null) { throw new ArgumentNullException(nameof(source)); } var config = ConfigFactory.FromFile(source); config.Validate(); var builder = new ApplicationBuilder(source, config.Name ?? source.Directory.Name.ToLowerInvariant()); if (!string.IsNullOrEmpty(config.Registry)) { builder.Registry = new ContainerRegistry(config.Registry); } builder.Network = config.Network; foreach (var configExtension in config.Extensions) { var extension = new ExtensionConfiguration((string)configExtension["name"]); foreach (var kvp in configExtension) { if (kvp.Key == "name") { continue; } extension.Data.Add(kvp.Key, kvp.Value); } builder.Extensions.Add(extension); } foreach (var configService in config.Services) { ServiceBuilder service; if (!string.IsNullOrEmpty(configService.Project)) { var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project); var projectFile = new FileInfo(Path.Combine(builder.Source.DirectoryName, expandedProject)); var project = new ProjectServiceBuilder(configService.Name !, projectFile); service = project; project.Build = configService.Build ?? true; project.Args = configService.Args; foreach (var buildProperty in configService.BuildProperties) { project.BuildProperties.Add(buildProperty.Name, buildProperty.Value); } project.Replicas = configService.Replicas ?? 1; await ProjectReader.ReadProjectDetailsAsync(output, project); // We don't apply more container defaults here because we might need // to prompt for the registry name. project.ContainerInfo = new ContainerInfo() { UseMultiphaseDockerfile = false, }; // Do k8s by default. project.ManifestInfo = new KubernetesManifestInfo(); } else if (!string.IsNullOrEmpty(configService.Image)) { var container = new ContainerServiceBuilder(configService.Name !, configService.Image) { Args = configService.Args, Replicas = configService.Replicas ?? 1 }; service = container; } else if (!string.IsNullOrEmpty(configService.Executable)) { var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable); var workingDirectory = ""; // Special handling of .dlls as executables (it will be executed as dotnet {dll}) if (Path.GetExtension(expandedExecutable) == ".dll") { expandedExecutable = Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, expandedExecutable)); workingDirectory = Path.GetDirectoryName(expandedExecutable) !; } var executable = new ExecutableServiceBuilder(configService.Name !, expandedExecutable) { Args = configService.Args, WorkingDirectory = configService.WorkingDirectory != null? Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) : workingDirectory, Replicas = configService.Replicas ?? 1 }; service = executable; } else if (configService.External) { var external = new ExternalServiceBuilder(configService.Name !); service = external; } else { throw new CommandException("Unable to determine service type."); } builder.Services.Add(service); // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding if (configService.Bindings.Count == 0 && service is ProjectServiceBuilder project2 && project2.IsAspNet) { // HTTP is the default binding service.Bindings.Add(new BindingBuilder() { Protocol = "http" }); service.Bindings.Add(new BindingBuilder() { Name = "https", Protocol = "https" }); }
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}", }); }
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 KubernetesDeploymentOutput CreateDeployment( OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, DeploymentManifestInfo deployment) { var bindings = project.Outputs.OfType <ComputedBindings>().FirstOrDefault(); var root = new YamlMappingNode(); root.Add("kind", "Deployment"); root.Add("apiVersion", "apps/v1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", project.Name); if (!string.IsNullOrEmpty(application.Namespace)) { metadata.Add("namespace", application.Namespace); } if (deployment.Annotations.Count > 0) { var annotations = new YamlMappingNode(); metadata.Add("annotations", annotations); foreach (var annotation in deployment.Annotations) { annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value) { Style = ScalarStyle.SingleQuoted, }); } } var labels = new YamlMappingNode(); metadata.Add("labels", labels); foreach (var label in deployment.Labels) { labels.Add(label.Key, new YamlScalarNode(label.Value) { Style = ScalarStyle.SingleQuoted, }); } var spec = new YamlMappingNode(); root.Add("spec", spec); spec.Add("replicas", project.Replicas.ToString()); var selector = new YamlMappingNode(); spec.Add("selector", selector); var matchLabels = new YamlMappingNode(); selector.Add("matchLabels", matchLabels); // We need the name so we can use it with matchLabels. if (!deployment.Labels.TryGetValue("app.kubernetes.io/name", out var matchLabelsLabelValue)) { throw new InvalidOperationException("The label 'app.kubernetes.io/name` is required."); } matchLabels.Add("app.kubernetes.io/name", matchLabelsLabelValue); var template = new YamlMappingNode(); spec.Add("template", template); metadata = new YamlMappingNode(); template.Add("metadata", metadata); if (deployment.Annotations.Count > 0) { var annotations = new YamlMappingNode(); metadata.Add("annotations", annotations); foreach (var annotation in deployment.Annotations) { annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value) { Style = ScalarStyle.SingleQuoted, }); } } labels = new YamlMappingNode(); metadata.Add("labels", labels); foreach (var label in deployment.Labels) { labels.Add(label.Key, new YamlScalarNode(label.Value) { Style = ScalarStyle.SingleQuoted, }); } spec = new YamlMappingNode(); template.Add("spec", spec); var containers = new YamlSequenceNode(); spec.Add("containers", containers); var images = project.Outputs.OfType <DockerImageOutput>(); foreach (var image in images) { var container = new YamlMappingNode(); containers.Add(container); container.Add("name", project.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{image.ImageName}:{image.ImageTag}"); container.Add("imagePullPolicy", "Always"); // helps avoid problems with development + weak versioning if (project.EnvironmentVariables.Count > 0 || // We generate ASPNETCORE_URLS if there are bindings for http project.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) || // We generate environment variables for other services if there dependencies (bindings is object && bindings.Bindings.Any())) { var env = new YamlSequenceNode(); container.Add("env", env); foreach (var kvp in project.EnvironmentVariables) { env.Add(new YamlMappingNode() { { "name", kvp.Name }, { "value", new YamlScalarNode(kvp.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } if (bindings is object) { foreach (var binding in bindings.Bindings.OfType <EnvironmentVariableInputBinding>()) { env.Add(new YamlMappingNode() { { "name", binding.Name }, { "value", new YamlScalarNode(binding.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } foreach (var binding in bindings.Bindings.OfType <SecretInputBinding>()) { //- name: SECRET_USERNAME // valueFrom: // secretKeyRef: // name: mysecret // key: username if (binding is SecretConnectionStringInputBinding connectionStringBinding) { env.Add(new YamlMappingNode() { { "name", connectionStringBinding.KeyName }, { "valueFrom", new YamlMappingNode() { { "secretKeyRef", new YamlMappingNode() { { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, { "key", new YamlScalarNode("connectionstring") { Style = ScalarStyle.SingleQuoted } }, } }, } }, }); } else if (binding is SecretUrlInputBinding urlBinding) { env.Add(new YamlMappingNode() { { "name", $"{urlBinding.KeyNameBase}__PROTOCOL" }, { "valueFrom", new YamlMappingNode() { { "secretKeyRef", new YamlMappingNode() { { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, { "key", new YamlScalarNode("protocol") { Style = ScalarStyle.SingleQuoted } }, } }, } }, }); env.Add(new YamlMappingNode() { { "name", $"{urlBinding.KeyNameBase}__HOST" }, { "valueFrom", new YamlMappingNode() { { "secretKeyRef", new YamlMappingNode() { { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, { "key", new YamlScalarNode("host") { Style = ScalarStyle.SingleQuoted } }, } }, } }, }); env.Add(new YamlMappingNode() { { "name", $"{urlBinding.KeyNameBase}__PORT" }, { "valueFrom", new YamlMappingNode() { { "secretKeyRef", new YamlMappingNode() { { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, { "key", new YamlScalarNode("port") { Style = ScalarStyle.SingleQuoted } }, } }, } }, }); } } } } if (project.Bindings.Count > 0) { var ports = new YamlSequenceNode(); container.Add("ports", ports); foreach (var binding in project.Bindings) { if (binding.Protocol == "https") { // We skip https for now in deployment, because the E2E requires certificates // and we haven't done those features yet. continue; } if (binding.Port != null) { var containerPort = new YamlMappingNode(); ports.Add(containerPort); containerPort.Add("containerPort", binding.Port.Value.ToString()); } } } } return(new KubernetesDeploymentOutput(project.Name, new YamlDocument(root))); }
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.BaseImageTag) && (project.TargetFrameworkName == "netcoreapp" || project.TargetFrameworkName == "net")) { container.BaseImageTag = project.TargetFrameworkVersion; } if (string.IsNullOrEmpty(container.BaseImageTag)) { throw new CommandException($"Unsupported TFM {project.TargetFramework}."); } if (string.IsNullOrEmpty(container.BaseImageName)) { if (TagIs50OrNewer(container.BaseImageTag)) { container.BaseImageName = project.IsAspNet ? "mcr.microsoft.com/dotnet/aspnet" : "mcr.microsoft.com/dotnet/runtime"; } else { container.BaseImageName = project.IsAspNet ? "mcr.microsoft.com/dotnet/core/aspnet" : "mcr.microsoft.com/dotnet/core/runtime"; } } container.BuildImageTag ??= project.TargetFrameworkVersion; if (string.IsNullOrEmpty(container.BuildImageName)) { container.BuildImageName = TagIs50OrNewer(container.BuildImageTag) ? "mcr.microsoft.com/dotnet/sdk" : "mcr.microsoft.com/dotnet/core/sdk"; } 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"; // Disable color in the logs project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("DOTNET_LOGGING__CONSOLE__DISABLECOLORS") { Value = "true" }); }
public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source) { if (source is null) { throw new ArgumentNullException(nameof(source)); } var config = ConfigFactory.FromFile(source); ValidateConfigApplication(config); var builder = new ApplicationBuilder(source, config.Name ?? source.Directory.Name.ToLowerInvariant()); if (!string.IsNullOrEmpty(config.Registry)) { builder.Registry = new ContainerRegistry(config.Registry); } foreach (var configService in config.Services) { ServiceBuilder service; if (!string.IsNullOrEmpty(configService.Project)) { var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project); var projectFile = new FileInfo(Path.Combine(builder.Source.DirectoryName, expandedProject)); var project = new ProjectServiceBuilder(configService.Name, projectFile); service = project; project.Build = configService.Build ?? true; project.Args = configService.Args; project.Replicas = configService.Replicas ?? 1; await ProjectReader.ReadProjectDetailsAsync(output, project); // We don't apply more container defaults here because we might need // to prompty for the registry name. project.ContainerInfo = new ContainerInfo() { UseMultiphaseDockerfile = false, }; } else if (!string.IsNullOrEmpty(configService.Image)) { var container = new ContainerServiceBuilder(configService.Name, configService.Image) { Args = configService.Args, Replicas = configService.Replicas ?? 1 }; service = container; } else if (!string.IsNullOrEmpty(configService.Executable)) { var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable); var workingDirectory = ""; // Special handling of .dlls as executables (it will be executed as dotnet {dll}) if (Path.GetExtension(expandedExecutable) == ".dll") { expandedExecutable = Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, expandedExecutable)); workingDirectory = Path.GetDirectoryName(expandedExecutable) !; } var executable = new ExecutableServiceBuilder(configService.Name, expandedExecutable) { Args = configService.Args, WorkingDirectory = configService.WorkingDirectory != null? Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) : workingDirectory, Replicas = configService.Replicas ?? 1 }; service = executable; } else if (configService.External) { var external = new ExternalServiceBuilder(configService.Name); service = external; } else { throw new CommandException("Unable to determine service type."); } builder.Services.Add(service); // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding if (configService.Bindings.Count == 0 && service is ProjectServiceBuilder project2 && project2.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App")) { // HTTP is the default binding service.Bindings.Add(new BindingBuilder() { AutoAssignPort = true, Protocol = "http" }); service.Bindings.Add(new BindingBuilder() { Name = "https", AutoAssignPort = true, Protocol = "https" }); }
private static async Task ExecuteDeployAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive, bool force) { var watch = System.Diagnostics.Stopwatch.StartNew(); 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."); } await application.ProcessExtensionsAsync(options : null, output, ExtensionContext.OperationKind.Deploy); ApplyRegistry(output, application, interactive, requireRegistry: true); var executor = new ApplicationExecutor(output) { ServiceSteps = { new ApplyContainerDefaultsStep(), new CombineStep() { Environment = environment, }, new PublishProjectStep(), new BuildDockerImageStep() { Environment = environment, }, new PushDockerImageStep() { Environment = environment, }, new ValidateSecretStep() { Environment = environment, Interactive= interactive, Force = force, }, new GenerateServiceKubernetesManifestStep() { Environment = environment, }, }, IngressSteps = { new ValidateIngressStep() { Environment = environment, Interactive = interactive, Force = force, }, new GenerateIngressKubernetesManifestStep(), }, ApplicationSteps = { new DeployApplicationKubernetesManifestStep(), } }; await executor.ExecuteAsync(application); watch.Stop(); TimeSpan elapsedTime = watch.Elapsed; output.WriteAlwaysLine($"Time Elapsed: {elapsedTime.Hours:00}:{elapsedTime.Minutes:00}:{elapsedTime.Seconds:00}:{elapsedTime.Milliseconds / 10:00}"); }
public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source) { if (source is null) { throw new ArgumentNullException(nameof(source)); } var queue = new Queue <(ConfigApplication, HashSet <string>)>(); var visited = new HashSet <string>(StringComparer.OrdinalIgnoreCase); var rootConfig = ConfigFactory.FromFile(source); rootConfig.Validate(); var root = new ApplicationBuilder(source, rootConfig.Name ?? source.Directory.Name.ToLowerInvariant()); root.Namespace = rootConfig.Namespace; queue.Enqueue((rootConfig, new HashSet <string>())); while (queue.Count > 0) { var item = queue.Dequeue(); var config = item.Item1; // dependencies represents a set of all dependencies var dependencies = item.Item2; if (!visited.Add(config.Source.FullName)) { continue; } if (config == rootConfig && !string.IsNullOrEmpty(config.Registry)) { root.Registry = new ContainerRegistry(config.Registry); } if (config == rootConfig) { root.Network = rootConfig.Network; } foreach (var configExtension in config.Extensions) { var extension = new ExtensionConfiguration((string)configExtension["name"]); foreach (var kvp in configExtension) { if (kvp.Key == "name") { continue; } extension.Data.Add(kvp.Key, kvp.Value); } root.Extensions.Add(extension); } foreach (var configService in config.Services) { ServiceBuilder service; if (root.Services.Any(s => s.Name == configService.Name)) { // Even though this service has already created a service, we still need // to update dependency information AddToRootServices(root, dependencies, configService.Name); continue; } if (!string.IsNullOrEmpty(configService.Project)) { var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project); var projectFile = new FileInfo(Path.Combine(config.Source.DirectoryName, expandedProject)); var project = new ProjectServiceBuilder(configService.Name !, projectFile); service = project; project.Build = configService.Build ?? true; project.Args = configService.Args; foreach (var buildProperty in configService.BuildProperties) { project.BuildProperties.Add(buildProperty.Name, buildProperty.Value); } project.Replicas = configService.Replicas ?? 1; // We don't apply more container defaults here because we might need // to prompt for the registry name. project.ContainerInfo = new ContainerInfo() { UseMultiphaseDockerfile = false, }; await ProjectReader.ReadProjectDetailsAsync(output, project); // Do k8s by default. project.ManifestInfo = new KubernetesManifestInfo(); } else if (!string.IsNullOrEmpty(configService.Image) || !string.IsNullOrEmpty(configService.DockerFile)) { var container = new ContainerServiceBuilder(configService.Name !, configService.Image !) { Args = configService.Args, Replicas = configService.Replicas ?? 1, DockerFile = configService.DockerFile != null?Path.Combine(source.DirectoryName, configService.DockerFile) : null, DockerFileContext = configService.DockerFileContext != null?Path.Combine(source.DirectoryName, configService.DockerFileContext) : null }; service = container; } else if (!string.IsNullOrEmpty(configService.Executable)) { var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable); var workingDirectory = ""; // Special handling of .dlls as executables (it will be executed as dotnet {dll}) if (Path.GetExtension(expandedExecutable) == ".dll") { expandedExecutable = Path.GetFullPath(Path.Combine(config.Source.Directory.FullName, expandedExecutable)); workingDirectory = Path.GetDirectoryName(expandedExecutable) !; } var executable = new ExecutableServiceBuilder(configService.Name !, expandedExecutable) { Args = configService.Args, WorkingDirectory = configService.WorkingDirectory != null? Path.GetFullPath(Path.Combine(config.Source.Directory.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) : workingDirectory, Replicas = configService.Replicas ?? 1 }; service = executable; } else if (!string.IsNullOrEmpty(configService.Include)) { var expandedYaml = Environment.ExpandEnvironmentVariables(configService.Include); var nestedConfig = GetNestedConfig(rootConfig, Path.Combine(config.Source.DirectoryName, expandedYaml)); queue.Enqueue((nestedConfig, new HashSet <string>())); AddToRootServices(root, dependencies, configService.Name); continue; } else if (!string.IsNullOrEmpty(configService.Repository)) { // clone to .tye folder var path = Path.Join(rootConfig.Source.DirectoryName, ".tye", "deps"); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } var clonePath = Path.Combine(path, configService.Name); if (!Directory.Exists(clonePath)) { if (!await GitDetector.Instance.IsGitInstalled.Value) { throw new CommandException($"Cannot clone repository {configService.Repository} because git is not installed. Please install git if you'd like to use \"repository\" in tye.yaml."); } var result = await ProcessUtil.RunAsync("git", $"clone {configService.Repository} {clonePath}", workingDirectory : path, throwOnError : false); if (result.ExitCode != 0) { throw new CommandException($"Failed to clone repository {configService.Repository} with exit code {result.ExitCode}.{Environment.NewLine}{result.StandardError}{result.StandardOutput}."); } } if (!ConfigFileFinder.TryFindSupportedFile(clonePath, out var file, out var errorMessage)) { throw new CommandException(errorMessage !); } // pick different service type based on what is in the repo. var nestedConfig = GetNestedConfig(rootConfig, file); queue.Enqueue((nestedConfig, new HashSet <string>())); AddToRootServices(root, dependencies, configService.Name); continue; } else if (configService.External) { var external = new ExternalServiceBuilder(configService.Name !); service = external; } else { throw new CommandException("Unable to determine service type."); } // Add dependencies to ourself before adding ourself to avoid self reference service.Dependencies.UnionWith(dependencies); AddToRootServices(root, dependencies, service.Name); root.Services.Add(service); // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding if (configService.Bindings.Count == 0 && service is ProjectServiceBuilder project2 && project2.IsAspNet) { // HTTP is the default binding service.Bindings.Add(new BindingBuilder() { Protocol = "http" }); service.Bindings.Add(new BindingBuilder() { Name = "https", Protocol = "https" }); }
public static Task WriteOamApplicationAsync(TextWriter writer, OutputContext output, ApplicationBuilder application, string environment) { if (writer is null) { throw new ArgumentNullException(nameof(writer)); } if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (environment is null) { throw new ArgumentNullException(nameof(environment)); } var componentManifests = new List <OamComponentOutput>(); var documents = new List <YamlDocument>(); foreach (var service in application.Services) { componentManifests.AddRange(service.Outputs.OfType <OamComponentOutput>()); } var root = new YamlMappingNode(); root.Add("kind", "ApplicationConfiguration"); root.Add("apiVersion", "core.oam.dev/v1alpha1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", application.Name); var spec = new YamlMappingNode(); root.Add("spec", spec); var components = new YamlSequenceNode(); spec.Add("components", components); foreach (var manifest in componentManifests) { documents.Add(manifest.Yaml); var component = new YamlMappingNode(); components.Add(component); component.Add("componentName", manifest.Name); component.Add("instanceName", $"{environment.ToLowerInvariant()}-{manifest.Name}"); } documents.Add(new YamlDocument(root)); var stream = new YamlStream(documents.ToArray()); stream.Save(writer, assignAnchors: false); return(Task.CompletedTask); }
public static Application ToHostingApplication(this ApplicationBuilder application) { var services = new Dictionary <string, Service>(); foreach (var service in application.Services) { RunInfo?runInfo; Probe? liveness; Probe? readiness; int replicas; var env = new List <EnvironmentVariable>(); if (service is ExternalServiceBuilder) { runInfo = null; liveness = null; readiness = null; replicas = 1; } else if (service is DockerFileServiceBuilder dockerFile) { var dockerRunInfo = new DockerRunInfo(dockerFile.Image, dockerFile.Args) { IsAspNet = dockerFile.IsAspNet, BuildArgs = dockerFile.BuildArgs }; if (!string.IsNullOrEmpty(dockerFile.DockerFile)) { dockerRunInfo.DockerFile = new FileInfo(dockerFile.DockerFile); if (!string.IsNullOrEmpty(dockerFile.DockerFileContext)) { dockerRunInfo.DockerFileContext = new FileInfo(dockerFile.DockerFileContext); } else { dockerRunInfo.DockerFileContext = new FileInfo(dockerRunInfo.DockerFile.DirectoryName); } } foreach (var mapping in dockerFile.Volumes) { dockerRunInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target)); } runInfo = dockerRunInfo; replicas = dockerFile.Replicas; liveness = dockerFile.Liveness != null?GetProbeFromBuilder(dockerFile.Liveness) : null; readiness = dockerFile.Readiness != null?GetProbeFromBuilder(dockerFile.Readiness) : null; foreach (var entry in dockerFile.EnvironmentVariables) { env.Add(entry.ToHostingEnvironmentVariable()); } } else if (service is ContainerServiceBuilder container) { var dockerRunInfo = new DockerRunInfo(container.Image, container.Args) { IsAspNet = container.IsAspNet }; foreach (var mapping in container.Volumes) { dockerRunInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target)); } runInfo = dockerRunInfo; replicas = container.Replicas; liveness = container.Liveness != null?GetProbeFromBuilder(container.Liveness) : null; readiness = container.Readiness != null?GetProbeFromBuilder(container.Readiness) : null; foreach (var entry in container.EnvironmentVariables) { env.Add(entry.ToHostingEnvironmentVariable()); } } else if (service is ExecutableServiceBuilder executable) { runInfo = new ExecutableRunInfo(executable.Executable, executable.WorkingDirectory, executable.Args); replicas = executable.Replicas; liveness = executable.Liveness != null?GetProbeFromBuilder(executable.Liveness) : null; readiness = executable.Readiness != null?GetProbeFromBuilder(executable.Readiness) : null; foreach (var entry in executable.EnvironmentVariables) { env.Add(entry.ToHostingEnvironmentVariable()); } } else if (service is DotnetProjectServiceBuilder project) { if (project.TargetFrameworks.Length > 1) { throw new InvalidOperationException($"Unable to run {project.Name}. Multi-targeted projects are not supported."); } if (project.RunCommand == null) { throw new InvalidOperationException($"Unable to run {project.Name}. The project does not have a run command"); } var projectInfo = new ProjectRunInfo(project); foreach (var mapping in project.Volumes) { projectInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target)); } runInfo = projectInfo; replicas = project.Replicas; liveness = project.Liveness != null?GetProbeFromBuilder(project.Liveness) : null; readiness = project.Readiness != null?GetProbeFromBuilder(project.Readiness) : null; foreach (var entry in project.EnvironmentVariables) { env.Add(entry.ToHostingEnvironmentVariable()); } } else { throw new InvalidOperationException($"Cannot figure out how to run service '{service.Name}'."); } var description = new ServiceDescription(service.Name, runInfo) { Replicas = replicas, Liveness = liveness, Readiness = readiness }; description.Configuration.AddRange(env); description.Dependencies.AddRange(service.Dependencies); foreach (var binding in service.Bindings) { description.Bindings.Add(new ServiceBinding() { ConnectionString = binding.ConnectionString, Host = binding.Host, ContainerPort = binding.ContainerPort, Name = binding.Name, Port = binding.Port, Protocol = binding.Protocol, }); } services.Add(service.Name, new Service(description)); } // Ingress get turned into services for hosting foreach (var ingress in application.Ingress) { var rules = new List <IngressRule>(); foreach (var rule in ingress.Rules) { rules.Add(new IngressRule(rule.Host, rule.Path, rule.Service !)); } var runInfo = new IngressRunInfo(rules); var description = new ServiceDescription(ingress.Name, runInfo) { Replicas = ingress.Replicas, }; foreach (var binding in ingress.Bindings) { description.Bindings.Add(new ServiceBinding() { Name = binding.Name, Port = binding.Port, Protocol = binding.Protocol, }); } services.Add(ingress.Name, new Service(description)); } return(new Application(application.Source, services) { Network = application.Network }); }
public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { // No need to do this computation for a non-project since we're not deploying it. if (!(service is ProjectServiceBuilder project)) { return(Task.CompletedTask); } // Compute ASPNETCORE_URLS based on the bindings exposed by *this* project. foreach (var binding in service.Bindings) { if (binding.Protocol == null && binding.ConnectionString == null) { binding.Protocol = "http"; } if (binding.Port == null && binding.Protocol == "http") { binding.Port = 80; } if (binding.Protocol == "http") { var port = binding.Port ?? 80; var urls = $"http://*{(port == 80 ? "" : (":" + port.ToString()))}"; project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("ASPNETCORE_URLS") { Value = urls, }); project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("PORT") { Value = port.ToString(CultureInfo.InvariantCulture), }); break; } } // Process bindings and turn them into environment variables and secrets. There's // some duplication with the code in m8s (Application.cs) for populating environments. // // service.Service.Bindings is the bindings OUT - this step computes bindings IN. var bindings = new ComputedBindings(); service.Outputs.Add(bindings); foreach (var other in application.Services) { if (object.ReferenceEquals(service, other)) { continue; } foreach (var binding in other.Bindings) { if (other is ProjectServiceBuilder) { // The other thing is a project, and will be deployed along with this // service. var configName = (binding.Name is null || binding.Name == other.Name) ? other.Name.ToUpperInvariant() : $"{other.Name.ToUpperInvariant()}__{binding.Name.ToUpperInvariant()}"; if (!string.IsNullOrEmpty(binding.ConnectionString)) { // Special case for connection strings bindings.Bindings.Add(new EnvironmentVariableInputBinding($"CONNECTIONSTRINGS__{configName}", binding.ConnectionString)); continue; } if (binding.Protocol == "https") { // We skip https for now in deployment, because the E2E requires certificates // and we haven't done those features yet. continue; } if (binding.Protocol == null) { binding.Protocol = "http"; } if (binding.Port == null && binding.Protocol == "http") { binding.Port = 80; } if (!string.IsNullOrEmpty(binding.Protocol)) { bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PROTOCOL", binding.Protocol)); } if (binding.Port != null) { bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PORT", binding.Port.Value.ToString())); } bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__HOST", binding.Host ?? other.Name)); } else { // The other service is not a project, so we'll use secrets. if (!string.IsNullOrEmpty(binding.ConnectionString)) { bindings.Bindings.Add(new SecretConnectionStringInputBinding( name: $"binding-{Environment}-{(binding.Name == null ? other.Name.ToLowerInvariant() : (other.Name.ToLowerInvariant() + "-" + binding.Name.ToLowerInvariant()))}-secret", other, binding, keyname: $"CONNECTIONSTRINGS__{(binding.Name == null ? other.Name.ToUpperInvariant() : (other.Name.ToUpperInvariant() + "__" + binding.Name.ToUpperInvariant()))}")); } else { bindings.Bindings.Add(new SecretUrlInputBinding( name: $"binding-{Environment}-{(binding.Name == null ? other.Name.ToLowerInvariant() : (other.Name.ToLowerInvariant() + "-" + binding.Name.ToLowerInvariant()))}-secret", other, binding, keynamebase: $"SERVICE__{(binding.Name == null ? other.Name.ToUpperInvariant() : (other.Name.ToUpperInvariant() + "__" + binding.Name.ToUpperInvariant()))}")); } } } } return(Task.CompletedTask); }
private ComputedBindings ComputeBindings(ApplicationBuilder application, IEnumerable <string> dependencies) { var bindings = new ComputedBindings(); foreach (var dependency in dependencies) { var other = application.Services.Single(a => a.Name == dependency); foreach (var binding in other.Bindings) { if (other is ProjectServiceBuilder) { // The other thing is a project, and will be deployed along with this // service. var configName = (binding.Name is null || binding.Name == other.Name) ? other.Name.ToUpperInvariant() : $"{other.Name.ToUpperInvariant()}__{binding.Name.ToUpperInvariant()}"; if (!string.IsNullOrEmpty(binding.ConnectionString)) { // Special case for connection strings bindings.Bindings.Add(new EnvironmentVariableInputBinding($"CONNECTIONSTRINGS__{configName}", binding.ConnectionString)); continue; } if (binding.Protocol == "https") { // We skip https for now in deployment, because the E2E requires certificates // and we haven't done those features yet. continue; } if (binding.Protocol == null) { binding.Protocol = "http"; } if (binding.Port == null && binding.Protocol == "http") { binding.Port = 80; } if (!string.IsNullOrEmpty(binding.Protocol)) { bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PROTOCOL", binding.Protocol)); } if (binding.Port != null) { bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PORT", binding.Port.Value.ToString())); } bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__HOST", binding.Host ?? other.Name)); } else { // The other service is not a project, so we'll use secrets. if (!string.IsNullOrEmpty(binding.ConnectionString)) { bindings.Bindings.Add(new SecretConnectionStringInputBinding( name: $"binding-{Environment}-{(binding.Name == null ? other.Name.ToLowerInvariant() : (other.Name.ToLowerInvariant() + "-" + binding.Name.ToLowerInvariant()))}-secret", other, binding, keyname: $"CONNECTIONSTRINGS__{(binding.Name == null ? other.Name.ToUpperInvariant() : (other.Name.ToUpperInvariant() + "__" + binding.Name.ToUpperInvariant()))}")); } else { bindings.Bindings.Add(new SecretUrlInputBinding( name: $"binding-{Environment}-{(binding.Name == null ? other.Name.ToLowerInvariant() : (other.Name.ToLowerInvariant() + "-" + binding.Name.ToLowerInvariant()))}-secret", other, binding, keynamebase: $"SERVICE__{(binding.Name == null ? other.Name.ToUpperInvariant() : (other.Name.ToUpperInvariant() + "__" + binding.Name.ToUpperInvariant()))}")); } } } } return(bindings); }
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(); // Use namespace of application, or current context, or 'default' config.Namespace = application.Namespace; config.Namespace ??= context?.ContextDetails?.Namespace ?? "default"; var kubernetes = new Kubernetes(config); try { var result = await kubernetes.ReadNamespacedSecretWithHttpMessagesAsync(secretInputBinding.Name, config.Namespace); 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 && secretInputBinding is SecretConnectionStringInputBinding) { 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} --namespace {config.Namespace} --from-literal=connectionstring=<value>"); } if (!Interactive && secretInputBinding is SecretUrlInputBinding) { 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} --namespace {config.Namespace} --from-literal=protocol=<value> --from-literal=host=<value> --from-literal=port=<value>"); } V1Secret secret; if (secretInputBinding is SecretConnectionStringInputBinding) { // 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.WriteAlwaysLine($"Skipping creation of secret for '{secretInputBinding.Service.Name}'. This may prevent creation of pods until secrets are created."); output.WriteAlwaysLine($"Manually create a secret with:"); output.WriteAlwaysLine($"kubectl create secret generic {secretInputBinding.Name} --namespace {config.Namespace} --from-literal=connectionstring=<value>"); continue; } secret = new V1Secret(type: "Opaque", stringData: new Dictionary <string, string>() { { "connectionstring", text }, }); } else if (secretInputBinding is SecretUrlInputBinding) { // If we get here then we should create the secret. string text; Uri? uri = null; while (true) { text = output.Prompt($"Enter the URI to use for service '{secretInputBinding.Service.Name}'", allowEmpty: true); if (string.IsNullOrEmpty(text)) { break; // skip } else if (Uri.TryCreate(text, UriKind.Absolute, out uri)) { break; // success } output.WriteAlwaysLine($"Invalid URI: '{text}'"); } if (string.IsNullOrWhiteSpace(text)) { output.WriteAlwaysLine($"Skipping creation of secret for '{secretInputBinding.Service.Name}'. This may prevent creation of pods until secrets are created."); output.WriteAlwaysLine($"Manually create a secret with:"); output.WriteAlwaysLine($"kubectl create secret generic {secretInputBinding.Name} --namespace {config.Namespace} --from-literal=protocol=<value> --from-literal=host=<value> --from-literal=port=<value>"); continue; } secret = new V1Secret(type: "Opaque", stringData: new Dictionary <string, string>() { { "protocol", uri !.Scheme },