internal static void ApplyRegistry(OutputContext output, ApplicationBuilder application, bool interactive, bool requireRegistry) { if (application.Registry is 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)) { application.Registry = new ContainerRegistry(registry.Trim()); } }
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 },
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}'."); }
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; }