3 changed files with 219 additions and 1 deletions
@ -0,0 +1,217 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.CommandLine.Invocation; |
||||
|
using System.Linq; |
||||
|
using System.Net; |
||||
|
using System.Threading.Tasks; |
||||
|
using k8s; |
||||
|
using k8s.Models; |
||||
|
using Microsoft.Rest; |
||||
|
|
||||
|
namespace Microsoft.Tye |
||||
|
{ |
||||
|
public sealed class ValidateIngressStep : ApplicationExecutor.IngressStep |
||||
|
{ |
||||
|
public override string DisplayText => "Validating Ingress..."; |
||||
|
|
||||
|
public string Environment { get; set; } = "production"; |
||||
|
|
||||
|
public bool Interactive { get; set; } |
||||
|
|
||||
|
public bool Force { get; set; } |
||||
|
|
||||
|
// Keep track of ingress classes we've seen so we don't validate them twice.
|
||||
|
public HashSet<string> IngressClasses { get; } = new HashSet<string>(); |
||||
|
|
||||
|
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.Instance.IsKubectlInstalled.Value) |
||||
|
{ |
||||
|
throw new CommandException($"Cannot validate ingress because kubectl is not installed."); |
||||
|
} |
||||
|
|
||||
|
if (!await KubectlDetector.Instance.IsKubectlConnectedToCluster.Value) |
||||
|
{ |
||||
|
throw new CommandException($"Cannot validate ingress because kubectl is not connected to a cluster."); |
||||
|
} |
||||
|
|
||||
|
output.WriteDebugLine($"Validating ingress class '{ingressClass}'."); |
||||
|
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); |
||||
|
|
||||
|
// 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 requirments 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 Process.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.
|
||||
|
output.WriteDebugLine($"Running 'kubectl apply'"); |
||||
|
output.WriteCommandLine("kubectl", $"apply -f \"https://aka.ms/tye/ingress/deploy\"");
|
||||
|
var capture = output.Capture(); |
||||
|
var exitCode = await Process.ExecuteAsync( |
||||
|
$"kubectl", |
||||
|
$"apply -f \"https://aka.ms/tye/ingress/deploy\"",
|
||||
|
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."); |
||||
|
} |
||||
|
|
||||
|
// 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/extensions/v1beta1/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."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
Loading…
Reference in new issue