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 static ServiceOutput CreateService(OutputContext output, Application application, ServiceEntry 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.Service.Name); var labels = new YamlMappingNode(); metadata.Add("labels", labels); labels.Add("app.kubernetes.io/name", service.Service.Name); if (application.Globals.Name is object) { labels.Add("app.kubernetes.io/part-of", application.Globals.Name); } var spec = new YamlMappingNode(); root.Add("spec", spec); var selector = new YamlMappingNode(); spec.Add("selector", selector); selector.Add("app.kubernetes.io/name", service.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.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.Service.Name, new YamlDocument(root))); }
public static ServiceOutput CreateDeployment(OutputContext output, Application application, ServiceEntry 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 bindings = service.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", service.Service.Name); var labels = new YamlMappingNode(); metadata.Add("labels", labels); labels.Add("app.kubernetes.io/name", service.Service.Name); if (application.Globals.Name is object) { labels.Add("app.kubernetes.io/part-of", application.Globals.Name); } var spec = new YamlMappingNode(); root.Add("spec", spec); spec.Add("replicas", service.Service.Replicas.ToString()); var selector = new YamlMappingNode(); spec.Add("selector", selector); var matchLabels = new YamlMappingNode(); selector.Add("matchLabels", matchLabels); matchLabels.Add("app.kubernetes.io/name", service.Service.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", service.Service.Name); if (application.Globals.Name is object) { labels.Add("app.kubernetes.io/part-of", application.Globals.Name); } spec = new YamlMappingNode(); template.Add("spec", spec); var containers = new YamlSequenceNode(); spec.Add("containers", containers); var images = service.Outputs.OfType <DockerImageOutput>(); foreach (var image in images) { var container = new YamlMappingNode(); containers.Add(container); container.Add("name", service.Service.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 (service.Service.Environment.Count > 0 || // We generate ASPNETCORE_URLS if there are bindings for http service.Service.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 service.Service.Environment) { env.Add(new YamlMappingNode() { { "name", kvp.Key }, { "value", new YamlScalarNode(kvp.Value.ToString()) { 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.Service.Name}-{binding.Binding.Name}"); volumeMount.Add("mountPath", $"/var/tye/bindings/{binding.Service.Service.Name}-{binding.Binding.Name}"); volumeMount.Add("readOnly", "true"); } } if (service.Service.Bindings.Count > 0) { var ports = new YamlSequenceNode(); container.Add("ports", ports); foreach (var binding in service.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 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.Service.Name}-{binding.Binding.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(service.Service.Name, new YamlDocument(root))); }
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) { // No need to do this computation for a non-project since we're not deploying it. if (!(service.Service.Source is Project)) { return(Task.CompletedTask); } // Compute ASPNETCORE_URLS based on the bindings exposed by *this* project. foreach (var binding in service.Service.Bindings) { if (binding.Protocol == "http" || binding.Protocol == null) { var port = binding.Port ?? 80; var urls = $"http://*{(port == 80 ? "" : (":" + port.ToString()))}"; service.Service.Environment.Add("ASPNETCORE_URLS", urls); 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.Service.Bindings) { // The other thing is a project, and will be deployed along with this // service. var configName = binding.Name == other.Service.Name ? other.Service.Name.ToUpperInvariant() : $"{other.Service.Name.ToUpperInvariant()}__{binding.Name.ToUpperInvariant()}"; if (other.Service.Source is Project) { if (!string.IsNullOrEmpty(binding.ConnectionString)) { // Special case for connection strings bindings.Bindings.Add(new EnvironmentVariableInputBinding($"CONNECTIONSTRING__{configName}", binding.ConnectionString)); } 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 (!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.Service.Name)); } else { // The other service is not a project, so we'll use secrets. bindings.Bindings.Add(new SecretInputBinding( name: $"binding-{Environment}-{other.Service.Name}-{binding.Name}-secret", filename: $"CONNECTIONSTRING__{configName}", other, binding)); } } } return(Task.CompletedTask); }
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry 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 = KubernetesClientConfiguration.LoadKubeConfig(); 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.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 sceret. var text = output.Prompt($"Enter the connection string to use for service '{secretInputBinding.Service.Service.Name}'", allowEmpty: true); if (string.IsNullOrWhiteSpace(text)) { output.WriteAlways($"Skipping creation of secret for '{secretInputBinding.Service.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.FriendlyName}'. Skipping."); return; } 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 Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) { if (SkipWithoutContainerOutput(output, service)) { return(Task.CompletedTask); } if (SkipForEnvironment(output, service, Environment)) { return(Task.CompletedTask); } var component = OamComponentGenerator.CreateOamComponent(output, application, service); service.Outputs.Add(component); return(Task.CompletedTask); }
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; } var chartDirectory = Path.Combine(application.GetProjectDirectory(project), "charts"); if (Directory.Exists(chartDirectory) && !Force) { throw new CommandException("'charts' directory already exists for project. use '--force' to overwrite."); } else if (Directory.Exists(chartDirectory)) { Directory.Delete(chartDirectory, recursive: true); } var chart = new HelmChartStep(); await HelmChartGenerator.GenerateAsync( output, application, service, project, container, chart, new DirectoryInfo(chartDirectory)); output.WriteInfoLine($"Generated Helm Chart at '{Path.Combine(chartDirectory, chart.ChartName)}'."); }
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 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 (SkipForEnvironment(output, service, Environment)) { return; } if (!await DockerDetector.Instance.IsDockerInstalled.Value) { throw new CommandException($"Cannot generate a docker image for '{service.Service.Name}' because docker is not installed."); } if (!await DockerDetector.Instance.IsDockerConnectedToDaemon.Value) { throw new CommandException($"Cannot generate a docker image for '{service.Service.Name}' because docker is not running."); } await DockerContainerBuilder.BuildContainerImageAsync(output, application, service, project, container); }
public static OamComponentOutput CreateOamComponent(OutputContext output, Application application, ServiceEntry 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", "ComponentSchematic"); root.Add("apiVersion", "core.oam.dev/v1alpha1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", service.Service.Name); var spec = new YamlMappingNode(); root.Add("spec", spec); spec.Add("workloadType", "core.oam.dev/v1alpha1.Server"); var containers = new YamlSequenceNode(); spec.Add("containers", containers); var images = service.Outputs.OfType <DockerImageOutput>(); foreach (var image in images) { var container = new YamlMappingNode(); containers.Add(container); container.Add("name", service.Service.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{image.ImageName}:{image.ImageTag}"); } return(new OamComponentOutput(service.Service.Name, new YamlDocument(root))); }
public static async Task GenerateAsync(OutputContext output, Application application, ServiceEntry service, Project 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 (service is null) { throw new ArgumentNullException(nameof(service)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (chart is null) { throw new ArgumentNullException(nameof(chart)); } if (outputDirectory is null) { throw new ArgumentNullException(nameof(outputDirectory)); } ApplyHelmChartDefaults(application, service, 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}", }); }
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) { if (SkipWithoutProject(output, service, out var _)) { return; } if (SkipWithoutContainerInfo(output, service, out var _)) { return; } if (SkipForEnvironment(output, service, Environment)) { return; } foreach (var image in service.Outputs.OfType <DockerImageOutput>()) { await DockerPush.ExecuteAsync(output, image.ImageName, image.ImageTag); output.WriteInfoLine($"Pushed docker image: '{image.ImageName}:{image.ImageTag}'"); } }
internal static async Task<TemporaryApplicationAdapter> CreateApplicationAdapterAsync(OutputContext output, ConfigApplication application, bool interactive, bool requireRegistry) { var globals = new ApplicationGlobals() { Name = application.Name, Registry = application.Registry is null ? null : new ContainerRegistry(application.Registry), }; var services = new List<Tye.ServiceEntry>(); foreach (var configService in application.Services) { if (configService.Project is string projectFile) { var fullPathProjectFile = Path.Combine(application.Source.DirectoryName, projectFile); var project = new Project(fullPathProjectFile); var service = new Service(configService.Name) { Source = project, }; service.Replicas = configService.Replicas ?? 1; foreach (var configBinding in configService.Bindings) { var binding = new ServiceBinding(configBinding.Name ?? service.Name) { ConnectionString = configBinding.ConnectionString, Host = configBinding.Host, Port = configBinding.Port, Protocol = configBinding.Protocol, }; binding.Protocol ??= "http"; if (binding.Port == null && configBinding.AutoAssignPort) { if (binding.Protocol == "http" || binding.Protocol == null) { binding.Port = 80; } else if (binding.Protocol == "https") { binding.Port = 443; } } service.Bindings.Add(binding); } var serviceEntry = new ServiceEntry(service, configService.Name); await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(fullPathProjectFile), project); var container = new ContainerInfo() { UseMultiphaseDockerfile = false, }; service.GeneratedAssets.Container = container; services.Add(serviceEntry); } else { // For a non-project, we don't really need much info about it, just the name and bindings var service = new Service(configService.Name); foreach (var configBinding in configService.Bindings) { service.Bindings.Add(new ServiceBinding(configBinding.Name ?? service.Name) { ConnectionString = configBinding.ConnectionString, Host = configBinding.Host, Port = configBinding.Port, Protocol = configBinding.Protocol, }); } var serviceEntry = new ServiceEntry(service, configService.Name); services.Add(serviceEntry); } } var temporaryApplication = new TemporaryApplicationAdapter(application, globals, services); if (temporaryApplication.Globals.Registry?.Hostname == null && interactive) { var registry = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)", allowEmpty: !requireRegistry); if (!string.IsNullOrWhiteSpace(registry)) { temporaryApplication.Globals.Registry = new ContainerRegistry(registry.Trim()); } } else if (temporaryApplication.Globals.Registry?.Hostname == null && requireRegistry) { throw new CommandException("A registry is required for deploy operations. Add the registry to 'tye.yaml' or use '-i' for interactive mode."); } else { // No registry specified, and that's OK! } foreach (var service in temporaryApplication.Services) { if (service.Service.Source is Project project && service.Service.GeneratedAssets.Container is ContainerInfo container) { DockerfileGenerator.ApplyContainerDefaults(temporaryApplication, service, project, container); } } return temporaryApplication; }
public static async Task BuildHelmChartAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, HelmChartStep chart) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (service is null) { throw new ArgumentNullException(nameof(service)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (chart is null) { throw new ArgumentNullException(nameof(chart)); } var projectDirectory = Path.Combine(application.RootDirectory, Path.GetDirectoryName(project.RelativeFilePath) !); var outputDirectoryPath = Path.Combine(projectDirectory, "bin"); using var tempDirectory = TempDirectory.Create(); HelmChartGenerator.ApplyHelmChartDefaults(application, service, container, chart); var chartRoot = Path.Combine(projectDirectory, "charts"); var chartPath = Path.Combine(chartRoot, chart.ChartName); output.WriteDebugLine($"Looking for existing chart in '{chartPath}'."); if (Directory.Exists(chartPath)) { output.WriteDebugLine($"Found existing chart in '{chartPath}'."); } else { chartRoot = tempDirectory.DirectoryPath; chartPath = Path.Combine(chartRoot, chart.ChartName); output.WriteDebugLine($"Generating chart in '{chartPath}'."); await HelmChartGenerator.GenerateAsync(output, application, service, project, container, chart, new DirectoryInfo(tempDirectory.DirectoryPath)); } output.WriteDebugLine("Running 'helm package'."); output.WriteCommandLine("helm", $"package -d \"{outputDirectoryPath}\" --version {project.Version.Replace('+', '-')} --app-version {project.Version.Replace('+', '-')}"); var capture = output.Capture(); var exitCode = await Process.ExecuteAsync( "helm", $"package . -d \"{outputDirectoryPath}\" --version {project.Version.Replace('+', '-')} --app-version {project.Version.Replace('+', '-')}", workingDir : chartPath, stdOut : capture.StdOut, stdErr : capture.StdErr); output.WriteDebugLine($"Running 'helm package' exit code: {exitCode}"); if (exitCode != 0) { throw new CommandException("Running 'helm package' failed."); } output.WriteInfoLine($"Created Helm Chart: {Path.Combine(outputDirectoryPath, chart.ChartName + "-" + project.Version.Replace('+', '-') + ".tgz")}"); }
public static async Task WriteDockerfileAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, string filePath) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (service is null) { throw new ArgumentNullException(nameof(service)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (filePath is null) { throw new ArgumentNullException(nameof(filePath)); } using var stream = File.OpenWrite(filePath); using var writer = new StreamWriter(stream, encoding: Encoding.UTF8, bufferSize: -1, leaveOpen: true); var entryPoint = Path.GetFileNameWithoutExtension(project.RelativeFilePath); output.WriteDebugLine($"Writing dockerfile to '{filePath}'."); if (container.UseMultiphaseDockerfile ?? true) { await WriteMultiphaseDockerfileAsync(writer, entryPoint, container); } else { await WriteLocalPublishDockerfileAsync(writer, entryPoint, container); } output.WriteDebugLine("Done writing dockerfile."); }
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service) { if (SkipWithoutContainerOutput(output, service)) { return(Task.CompletedTask); } if (SkipForEnvironment(output, service, Environment)) { return(Task.CompletedTask); } service.Outputs.Add(KubernetesManifestGenerator.CreateDeployment(output, application, service)); if (service.Service.Bindings.Count > 0) { service.Outputs.Add(KubernetesManifestGenerator.CreateService(output, application, service)); } return(Task.CompletedTask); }
public static void ApplyContainerDefaults(Application application, ServiceEntry service, Project project, ContainerInfo container) { if (application is null) { throw new ArgumentNullException(nameof(application)); } if (service is null) { throw new ArgumentNullException(nameof(service)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (container.BaseImageName == null && project.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App")) { container.BaseImageName = "mcr.microsoft.com/dotnet/core/aspnet"; } else if (container.BaseImageName == null) { container.BaseImageName = "mcr.microsoft.com/dotnet/core/runtime"; } if (container.BaseImageTag == null && project.TargetFramework == "netcoreapp3.1") { container.BaseImageTag = "3.1"; } else if (container.BaseImageTag == null && project.TargetFramework == "netcoreapp3.0") { container.BaseImageTag = "3.0"; } if (container.BaseImageTag == null) { throw new CommandException($"Unsupported TFM {project.TargetFramework}."); } container.BuildImageName ??= "mcr.microsoft.com/dotnet/core/sdk"; container.BuildImageTag ??= "3.1"; if (container.ImageName == null && application.Globals.Registry?.Hostname == null) { container.ImageName ??= service.Service.Name.ToLowerInvariant(); } else if (container.ImageName == null && application.Globals.Registry?.Hostname != null) { container.ImageName ??= $"{application.Globals.Registry?.Hostname}/{service.Service.Name.ToLowerInvariant()}"; } container.ImageTag ??= project.Version.Replace("+", "-"); }
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) { throw new CommandException("Generated Dockerfile workflow does not support single-phase Dockerfiles."); } container.UseMultiphaseDockerfile ??= true; var dockerFilePath = Path.Combine(application.GetProjectDirectory(project), "Dockerfile"); if (File.Exists(dockerFilePath) && !Force) { throw new CommandException("'Dockerfile' already exists for project. use '--force' to overwrite."); } File.Delete(dockerFilePath); await DockerfileGenerator.WriteDockerfileAsync(output, application, service, project, container, dockerFilePath); output.WriteInfoLine($"Generated Dockerfile at '{dockerFilePath}'."); }
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 abstract Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service);