public static async Task UndeployAsync(OutputContext output, FileInfo path, string @namespace, bool interactive, bool whatIf, ApplicationFactoryFilter?filter = null) { var watch = System.Diagnostics.Stopwatch.StartNew(); var application = await ApplicationFactory.CreateAsync(output, path, null, filter); if (!string.IsNullOrEmpty(@namespace)) { application.Namespace = @namespace; } await ExecuteUndeployAsync(output, application, @namespace, interactive, whatIf); 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 UndeployAsync(IConsole console, FileInfo path, Verbosity verbosity, string @namespace, bool interactive, bool whatIf) { var watch = System.Diagnostics.Stopwatch.StartNew(); var output = new OutputContext(console, verbosity); output.WriteInfoLine("Loading Application Details..."); // We don't need to know anything about the services, just the application name. var application = ConfigFactory.FromFile(path); if (!string.IsNullOrEmpty(@namespace)) { application.Namespace = @namespace; } await ExecuteUndeployAsync(output, application, @namespace, interactive, whatIf); 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 UndeployAsync(IConsole console, FileInfo path, Verbosity verbosity, string @namespace, bool interactive, bool whatIf, string[] tags) { var watch = System.Diagnostics.Stopwatch.StartNew(); var output = new OutputContext(console, verbosity); output.WriteInfoLine("Loading Application Details..."); var filter = ApplicationFactoryFilter.GetApplicationFactoryFilter(tags); var application = await ApplicationFactory.CreateAsync(output, path, filter); if (!string.IsNullOrEmpty(@namespace)) { application.Namespace = @namespace; } await ExecuteUndeployAsync(output, application, @namespace, interactive, whatIf); 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 ExecuteUndeployAsync(OutputContext output, ConfigApplication application, string @namespace, bool interactive, bool whatIf) { var config = KubernetesClientConfiguration.BuildDefaultConfig(); var kubernetes = new Kubernetes(config); // If namespace is null, set it to default config.Namespace ??= "default"; // Due to some limitations in the k8s SDK we currently have a hardcoded list of resource // types that we handle deletes for. If we start adding extensibility for the *kinds* of // k8s resources we create, or the ability to deploy additional files along with the // resources we understand then we should revisit this. // // Basically the challenges are: // // - kubectl api-resources --all (and similar) are implemented client-side (n+1 problem) // - the C# k8s SDK doesn't have an untyped api for operations on arbitrary resources, the // closest thing is the custom resource APIs // - Legacy resources without an api group don't follow the same URL scheme as more modern // ones, and thus cannot be addressed using the custom resource APIs. // // So solving 'undeploy' generically would involve doing a bunch of work to query things // generically, including going outside of what's provided by the SDK. // // - querying api-resources // - querying api-groups // - handcrafing requests to list for each resource // - handcrafting requests to delete each resource var resources = new List <Resource>(); var applicationName = application.Name; try { output.WriteDebugLine("Querying services"); var response = await kubernetes.ListNamespacedServiceWithHttpMessagesAsync( config.Namespace, labelSelector : $"app.kubernetes.io/part-of={applicationName}"); foreach (var resource in response.Body.Items) { resource.Kind = V1Service.KubeKind; } resources.AddRange(response.Body.Items.Select(item => new Resource(item.Kind, item.Metadata, DeleteService))); output.WriteDebugLine($"Found {response.Body.Items.Count} matching services"); } catch (Exception ex) { output.WriteDebugLine("Failed to query services."); output.WriteDebugLine(ex.ToString()); throw new CommandException("Unable connect to kubernetes.", ex); } try { output.WriteDebugLine("Querying deployments"); var response = await kubernetes.ListNamespacedDeploymentWithHttpMessagesAsync( config.Namespace, labelSelector : $"app.kubernetes.io/part-of={applicationName}"); foreach (var resource in response.Body.Items) { resource.Kind = V1Deployment.KubeKind; } resources.AddRange(response.Body.Items.Select(item => new Resource(item.Kind, item.Metadata, DeleteDeployment))); output.WriteDebugLine($"Found {response.Body.Items.Count} matching deployments"); } catch (Exception ex) { output.WriteDebugLine("Failed to query deployments."); output.WriteDebugLine(ex.ToString()); throw new CommandException("Unable connect to kubernetes.", ex); } try { output.WriteDebugLine("Querying secrets"); var response = await kubernetes.ListNamespacedSecretWithHttpMessagesAsync( config.Namespace, labelSelector : $"app.kubernetes.io/part-of={applicationName}"); foreach (var resource in response.Body.Items) { resource.Kind = V1Secret.KubeKind; } resources.AddRange(response.Body.Items.Select(item => new Resource(item.Kind, item.Metadata, DeleteSecret))); output.WriteDebugLine($"Found {response.Body.Items.Count} matching secrets"); } catch (Exception ex) { output.WriteDebugLine("Failed to query secrets."); output.WriteDebugLine(ex.ToString()); throw new CommandException("Unable connect to kubernetes.", ex); } try { output.WriteDebugLine("Querying ingresses"); var response = await kubernetes.ListNamespacedIngressWithHttpMessagesAsync( config.Namespace, labelSelector : $"app.kubernetes.io/part-of={applicationName}"); foreach (var resource in response.Body.Items) { resource.Kind = "Ingress"; } resources.AddRange(response.Body.Items.Select(item => new Resource(item.Kind, item.Metadata, DeleteIngress))); output.WriteDebugLine($"Found {response.Body.Items.Count} matching ingress"); } catch (Exception ex) { output.WriteDebugLine("Failed to query ingress."); output.WriteDebugLine(ex.ToString()); throw new CommandException("Unable connect to kubernetes.", ex); } output.WriteInfoLine($"Found {resources.Count} resource(s)."); var exceptions = new List <(Resource resource, HttpOperationException exception)>(); foreach (var resource in resources) { var operation = Operations.Delete; if (interactive && !output.Confirm($"Delete {resource.Kind} '{resource.Metadata.Name}'?")) { operation = Operations.None; } if (whatIf && operation == Operations.Delete) { operation = Operations.Explain; } if (operation == Operations.None) { output.WriteAlwaysLine($"Skipping '{resource.Kind}' '{resource.Metadata.Name}' ..."); } else if (operation == Operations.Explain) { output.WriteAlwaysLine($"whatif: Deleting '{resource.Kind}' '{resource.Metadata.Name}' ..."); } else if (operation == Operations.Delete) { output.WriteAlwaysLine($"Deleting '{resource.Kind}' '{resource.Metadata.Name}' ..."); try { var response = await resource.Deleter(resource.Metadata.Name); output.WriteDebugLine($"Successfully deleted resource: '{resource.Kind}' '{resource.Metadata.Name}'."); } catch (HttpOperationException ex) { output.WriteDebugLine($"Failed to delete resource: '{resource.Kind}' '{resource.Metadata.Name}'."); output.WriteDebugLine(ex.ToString()); exceptions.Add((resource, ex)); } } } if (exceptions.Count > 0) { throw new CommandException( $"Failed to delete some resources: " + Environment.NewLine + Environment.NewLine + string.Join(Environment.NewLine, exceptions.Select(e => $"\t'{e.resource.Kind}' '{e.resource.Metadata.Name}': {e.exception.Body}."))); } Task <Rest.HttpOperationResponse <V1Status> > DeleteService(string name) { return(kubernetes !.DeleteNamespacedServiceWithHttpMessagesAsync(name, config !.Namespace)); } Task <Rest.HttpOperationResponse <V1Status> > DeleteDeployment(string name) { return(kubernetes !.DeleteNamespacedDeploymentWithHttpMessagesAsync(name, config !.Namespace)); } Task <Rest.HttpOperationResponse <V1Status> > DeleteSecret(string name) { return(kubernetes !.DeleteNamespacedSecretWithHttpMessagesAsync(name, config !.Namespace)); } Task <Rest.HttpOperationResponse <V1Status> > DeleteIngress(string name) { return(kubernetes !.DeleteNamespacedIngressWithHttpMessagesAsync(name, config !.Namespace)); } }
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 override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingress) { // This code assumes that in the future we might support other ingress types besides nginx. // // Right now we only know some hardcoded details about ingress-nginx that we use for both // validation and generation of manifests. // // For instance we don't support k8s 1.18.X IngressClass resources because that version // isn't broadly available yet. var ingressClass = "nginx"; if (!IngressClasses.Add(ingressClass)) { output.WriteDebugLine($"Already validated ingress class '{ingressClass}'."); return; } if (await KubectlDetector.GetKubernetesServerVersion(output) == null) { throw new CommandException($"Cannot validate ingress because kubectl is not installed."); } if (!await KubectlDetector.IsKubectlConnectedToClusterAsync(output)) { throw new CommandException($"Cannot validate ingress because kubectl is not connected to a cluster."); } output.WriteDebugLine($"Validating ingress class '{ingressClass}'."); var config = KubernetesClientConfiguration.BuildDefaultConfig(); // If namespace is null, set it to default config.Namespace ??= "default"; var kubernetes = new Kubernetes(config); // Looking for a deployment using a standard label. // Note: using a deployment instead of a service - minikube doesn't create a service for the controller. try { var result = await kubernetes.ListDeploymentForAllNamespacesWithHttpMessagesAsync( labelSelector : "app.kubernetes.io/name in (ingress-nginx, nginx-ingress-controller)"); if (result.Body.Items.Count > 0) { foreach (var service in result.Body.Items) { output.WriteInfoLine($"Found existing ingress controller '{service.Metadata.Name}' in namespace '{service.Metadata.NamespaceProperty}'."); } return; } } catch (HttpOperationException ex) when(ex.Response.StatusCode == HttpStatusCode.NotFound) { // The kubernetes client uses exceptions for 404s. } catch (Exception ex) { output.WriteDebugLine("Failed to query secret."); output.WriteDebugLine(ex.ToString()); throw new CommandException("Unable connect to kubernetes.", ex); } if (Force) { output.WriteDebugLine("Skipping because force was specified."); return; } if (!Interactive) { throw new CommandException( $"An ingress was specified for the application, but the 'ingress-nginx' controller could not be found. " + $"Rerun the command with --interactive to deploy a controller for development, or with --force to skip validation. Alternatively " + $"see our documentation on ingress: https://aka.ms/tye/ingress"); } output.WriteAlwaysLine( "Tye can deploy the ingress-nginx controller for you. This will be a basic deployment suitable for " + "experimentation and development. Your production needs, or requirements may differ depending on your Kubernetes distribution. " + "See: https://aka.ms/tye/ingress for documentation."); if (!output.Confirm($"Deploy ingress-nginx")) { // user skipped deployment of ingress, continue with deployment. return; } // We want to be able to detect minikube because the process for enabling nginx-ingress is different there, // it's shipped as an addon. // // see: https://github.com/telepresenceio/telepresence/blob/4364fd83d5926bef46babd704e7bd6c82a75dbd6/telepresence/startup.py#L220 if (config.CurrentContext == "minikube") { output.WriteDebugLine($"Running 'minikube addons enable ingress'"); output.WriteCommandLine("minikube", "addon enable ingress"); var capture = output.Capture(); var exitCode = await ProcessUtil.ExecuteAsync( $"minikube", $"addons enable ingress", System.Environment.CurrentDirectory, stdOut : capture.StdOut, stdErr : capture.StdErr); output.WriteDebugLine($"Done running 'minikube addons enable ingress' exit code: {exitCode}"); if (exitCode != 0) { throw new CommandException("'minikube addons enable ingress' failed."); } output.WriteInfoLine($"Deployed ingress-nginx."); } else { // If we get here then we should deploy the ingress controller. // The first time we apply the ingress controller, the validating webhook will not have started. // This causes an error to be returned from the process. As this always happens, we are going to // not check the error returned and assume the kubectl command worked. This is double checked in // the future as well when we try to create the ingress resource. output.WriteDebugLine($"Running 'kubectl apply'"); output.WriteCommandLine("kubectl", $"apply -f \"https://aka.ms/tye/ingress/deploy\""); var capture = output.Capture(); var exitCode = await ProcessUtil.ExecuteAsync( $"kubectl", $"apply -f \"https://aka.ms/tye/ingress/deploy\"", System.Environment.CurrentDirectory); output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}"); output.WriteInfoLine($"Waiting for ingress-nginx controller to start."); // We need to then wait for the webhooks that are created by ingress-nginx to start. Deploying an ingress immediately // after creating the controller will fail if the webhook isn't ready. // // Internal error occurred: failed calling webhook "validate.nginx.ingress.kubernetes.io": // Post https://ingress-nginx-controller-admission.ingress-nginx.svc:443/networking.k8s.io/v1/ingresses?timeout=30s: // dial tcp 10.0.31.130:443: connect: connection refused // // Unfortunately this is the likely case for us. try { output.WriteDebugLine("Watching for ingress-nginx controller readiness..."); var response = await kubernetes.ListNamespacedPodWithHttpMessagesAsync( namespaceParameter : "ingress-nginx", labelSelector : "app.kubernetes.io/component=controller,app.kubernetes.io/name=ingress-nginx", watch : true); var tcs = new TaskCompletionSource <object?>(); using var watcher = response.Watch <V1Pod, V1PodList>( onEvent: (@event, pod) => { // Wait for the readiness-check to pass. if (pod.Status.Conditions.All(c => string.Equals(c.Status, bool.TrueString, StringComparison.OrdinalIgnoreCase))) { tcs.TrySetResult(null); // Success! output.WriteDebugLine($"Pod '{pod.Metadata.Name}' is ready."); } }, onError: ex => { tcs.TrySetException(ex); output.WriteDebugLine("Watch operation failed."); }, onClosed: () => { // YOLO? tcs.TrySetResult(null); output.WriteDebugLine("Watch operation completed."); }); await tcs.Task; } catch (Exception ex) { output.WriteDebugLine("Failed to ingress-nginx pods."); output.WriteDebugLine(ex.ToString()); throw new CommandException("Failed to query ingress-nginx pods.", ex); } output.WriteInfoLine($"Deployed ingress-nginx."); } }
public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { var bindings = service.Outputs.OfType <ComputedBindings>().FirstOrDefault(); if (bindings is null) { return; } foreach (var binding in bindings.Bindings) { if (binding is SecretInputBinding secretInputBinding) { if (!Secrets.Add(secretInputBinding.Name)) { output.WriteDebugLine($"Already validated secret '{secretInputBinding.Name}'."); continue; } output.WriteDebugLine($"Validating secret '{secretInputBinding.Name}'."); var config = KubernetesClientConfiguration.BuildDefaultConfig(); // Workaround for https://github.com/kubernetes-client/csharp/issues/372 var store = await KubernetesClientConfiguration.LoadKubeConfigAsync(); var context = store.Contexts.Where(c => c.Name == config.CurrentContext).FirstOrDefault(); // 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 },