Beispiel #1
0
        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.");
            }
        }
Beispiel #2
0
        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)));
        }
Beispiel #3
0
        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);

                foreach (var configBinding in configService.Bindings)
                {
                    var binding = new BindingBuilder()
                    {
                        Name             = configBinding.Name,
                        AutoAssignPort   = configBinding.AutoAssignPort,
                        ConnectionString = configBinding.ConnectionString,
                        Host             = configBinding.Host,
                        ContainerPort    = configBinding.ContainerPort,
                        Port             = configBinding.Port,
                        Protocol         = configBinding.Protocol,
                    };

                    // Assume HTTP for projects only (containers may be different)
                    if (binding.ConnectionString == null && configService.Project != null)
                    {
                        binding.Protocol ??= "http";
                    }

                    service.Bindings.Add(binding);
                }

                foreach (var configEnvVar in configService.Configuration)
                {
                    var envVar = new EnvironmentVariable(configEnvVar.Name, configEnvVar.Value);
                    if (service is ProjectServiceBuilder project)
                    {
                        project.EnvironmentVariables.Add(envVar);
                    }
                    else if (service is ContainerServiceBuilder container)
                    {
                        container.EnvironmentVariables.Add(envVar);
                    }
                    else if (service is ExecutableServiceBuilder executable)
                    {
                        executable.EnvironmentVariables.Add(envVar);
                    }
                    else if (service is ExternalServiceBuilder)
                    {
                        throw new CommandException("External services do not support environment variables.");
                    }
                    else
                    {
                        throw new CommandException("Unable to determine service type.");
                    }
                }

                foreach (var configVolume in configService.Volumes)
                {
                    var volume = new VolumeBuilder(configVolume.Source, configVolume.Target);
                    if (service is ProjectServiceBuilder project)
                    {
                        project.Volumes.Add(volume);
                    }
                    else if (service is ContainerServiceBuilder container)
                    {
                        container.Volumes.Add(volume);
                    }
                    else if (service is ExecutableServiceBuilder executable)
                    {
                        throw new CommandException("Executable services do not support volumes.");
                    }
                    else if (service is ExternalServiceBuilder)
                    {
                        throw new CommandException("External services do not support volumes.");
                    }
                    else
                    {
                        throw new CommandException("Unable to determine service type.");
                    }
                }
            }

            foreach (var configIngress in config.Ingress)
            {
                var ingress = new IngressBuilder(configIngress.Name);
                ingress.Replicas = configIngress.Replicas ?? 1;

                builder.Ingress.Add(ingress);

                foreach (var configBinding in configIngress.Bindings)
                {
                    var binding = new IngressBindingBuilder()
                    {
                        AutoAssignPort = configBinding.AutoAssignPort,
                        Name           = configBinding.Name,
                        Port           = configBinding.Port,
                        Protocol       = configBinding.Protocol ?? "http",
                    };
                    ingress.Bindings.Add(binding);
                }

                foreach (var configRule in configIngress.Rules)
                {
                    var rule = new IngressRuleBuilder()
                    {
                        Host    = configRule.Host,
                        Path    = configRule.Path,
                        Service = configRule.Service,
                    };
                    ingress.Rules.Add(rule);
                }
            }

            return(builder);
        }
 public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingress)
 {
     ingress.Outputs.Add(KubernetesManifestGenerator.CreateIngress(output, application, ingress));
     return(Task.CompletedTask);
 }
Beispiel #5
0
 public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingres);
Beispiel #6
0
 public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingress)
 {
     ingress.Outputs.Add(await KubernetesManifestGenerator.CreateIngress(output, application, ingress));
 }