Exemple #1
0
 public abstract Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service);
Exemple #2
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.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();

            // Use namespace of application, or current context, or 'default'
            config.Namespace = application.Namespace;
            config.Namespace ??= context?.ContextDetails?.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 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.");
                }

                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/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.");
            }
        }
Exemple #3
0
        public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source)
        {
            if (source is null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            var queue   = new Queue <(ConfigApplication, HashSet <string>)>();
            var visited = new HashSet <string>(StringComparer.OrdinalIgnoreCase);

            var rootConfig = ConfigFactory.FromFile(source);

            rootConfig.Validate();

            var root = new ApplicationBuilder(source, rootConfig.Name ?? source.Directory.Name.ToLowerInvariant());

            root.Namespace = rootConfig.Namespace;

            queue.Enqueue((rootConfig, new HashSet <string>()));

            while (queue.Count > 0)
            {
                var item   = queue.Dequeue();
                var config = item.Item1;

                // dependencies represents a set of all dependencies
                var dependencies = item.Item2;
                if (!visited.Add(config.Source.FullName))
                {
                    continue;
                }

                if (config == rootConfig && !string.IsNullOrEmpty(config.Registry))
                {
                    root.Registry = new ContainerRegistry(config.Registry);
                }

                if (config == rootConfig)
                {
                    root.Network = rootConfig.Network;
                }

                foreach (var configExtension in config.Extensions)
                {
                    var extension = new ExtensionConfiguration((string)configExtension["name"]);
                    foreach (var kvp in configExtension)
                    {
                        if (kvp.Key == "name")
                        {
                            continue;
                        }

                        extension.Data.Add(kvp.Key, kvp.Value);
                    }

                    root.Extensions.Add(extension);
                }

                foreach (var configService in config.Services)
                {
                    ServiceBuilder service;
                    if (root.Services.Any(s => s.Name == configService.Name))
                    {
                        // Even though this service has already created a service, we still need
                        // to update dependency information
                        AddToRootServices(root, dependencies, configService.Name);
                        continue;
                    }

                    if (!string.IsNullOrEmpty(configService.Project))
                    {
                        var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project);
                        var projectFile     = new FileInfo(Path.Combine(config.Source.DirectoryName, expandedProject));
                        var project         = new ProjectServiceBuilder(configService.Name !, projectFile);
                        service = project;

                        project.Build = configService.Build ?? true;
                        project.Args  = configService.Args;
                        foreach (var buildProperty in configService.BuildProperties)
                        {
                            project.BuildProperties.Add(buildProperty.Name, buildProperty.Value);
                        }
                        project.Replicas = configService.Replicas ?? 1;

                        await ProjectReader.ReadProjectDetailsAsync(output, project);

                        // We don't apply more container defaults here because we might need
                        // to prompt for the registry name.
                        project.ContainerInfo = new ContainerInfo()
                        {
                            UseMultiphaseDockerfile = false,
                        };

                        // Do k8s by default.
                        project.ManifestInfo = new KubernetesManifestInfo();
                    }
                    else if (!string.IsNullOrEmpty(configService.Image) || !string.IsNullOrEmpty(configService.DockerFile))
                    {
                        var container = new ContainerServiceBuilder(configService.Name !, configService.Image !)
                        {
                            Args       = configService.Args,
                            Replicas   = configService.Replicas ?? 1,
                            DockerFile = configService.DockerFile != null?Path.Combine(source.DirectoryName, configService.DockerFile) : null,
                                             DockerFileContext = configService.DockerFileContext != null?Path.Combine(source.DirectoryName, configService.DockerFileContext) : null
                        };
                        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(config.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(config.Source.Directory.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) :
                                                   workingDirectory,
                                                   Replicas = configService.Replicas ?? 1
                        };
                        service = executable;
                    }
                    else if (!string.IsNullOrEmpty(configService.Include))
                    {
                        var expandedYaml = Environment.ExpandEnvironmentVariables(configService.Include);

                        var nestedConfig = GetNestedConfig(rootConfig, Path.Combine(config.Source.DirectoryName, expandedYaml));
                        queue.Enqueue((nestedConfig, new HashSet <string>()));

                        AddToRootServices(root, dependencies, configService.Name);
                        continue;
                    }
                    else if (!string.IsNullOrEmpty(configService.Repository))
                    {
                        // clone to .tye folder
                        var path = Path.Join(rootConfig.Source.DirectoryName, ".tye", "deps");
                        if (!Directory.Exists(path))
                        {
                            Directory.CreateDirectory(path);
                        }

                        var clonePath = Path.Combine(path, configService.Name);

                        if (!Directory.Exists(clonePath))
                        {
                            if (!await GitDetector.Instance.IsGitInstalled.Value)
                            {
                                throw new CommandException($"Cannot clone repository {configService.Repository} because git is not installed. Please install git if you'd like to use \"repository\" in tye.yaml.");
                            }

                            var result = await ProcessUtil.RunAsync("git", $"clone {configService.Repository} {clonePath}", workingDirectory : path, throwOnError : false);

                            if (result.ExitCode != 0)
                            {
                                throw new CommandException($"Failed to clone repository {configService.Repository} with exit code {result.ExitCode}.{Environment.NewLine}{result.StandardError}{result.StandardOutput}.");
                            }
                        }

                        if (!ConfigFileFinder.TryFindSupportedFile(clonePath, out var file, out var errorMessage))
                        {
                            throw new CommandException(errorMessage !);
                        }

                        // pick different service type based on what is in the repo.
                        var nestedConfig = GetNestedConfig(rootConfig, file);

                        queue.Enqueue((nestedConfig, new HashSet <string>()));

                        AddToRootServices(root, dependencies, configService.Name);

                        continue;
                    }
                    else if (configService.External)
                    {
                        var external = new ExternalServiceBuilder(configService.Name !);
                        service = external;
                    }
                    else
                    {
                        throw new CommandException("Unable to determine service type.");
                    }

                    // Add dependencies to ourself before adding ourself to avoid self reference
                    service.Dependencies.UnionWith(dependencies);

                    AddToRootServices(root, dependencies, service.Name);

                    root.Services.Add(service);

                    // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding
                    if (configService.Bindings.Count == 0 &&
                        service is ProjectServiceBuilder project2 &&
                        project2.IsAspNet)
                    {
                        // HTTP is the default binding
                        service.Bindings.Add(new BindingBuilder()
                        {
                            Protocol = "http"
                        });
                        service.Bindings.Add(new BindingBuilder()
                        {
                            Name = "https", Protocol = "https"
                        });
                    }
Exemple #4
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 projectFile = new FileInfo(Path.Combine(builder.Source.DirectoryName, configService.Project));
                    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);
                    container.Args     = configService.Args;
                    container.Replicas = configService.Replicas ?? 1;
                    service            = container;
                }
                else if (!string.IsNullOrEmpty(configService.Executable))
                {
                    var executable = new ExecutableServiceBuilder(configService.Name, configService.Executable);
                    executable.Args             = configService.Args;
                    executable.WorkingDirectory = configService.WorkingDirectory;
                    executable.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,
                    };

                    if (binding.ConnectionString == 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);
        }
Exemple #5
0
        private static Command CreateRunCommand()
        {
            var command = new Command("run", "run the application")
            {
                CommonArguments.Path_Required,

                new Option("--no-build")
                {
                    Description = "Do not build project files before running.",
                    Required    = false
                },
                new Option("--port")
                {
                    Description = "The port to run control plane on.",
                    Argument    = new Argument <int?>("port"),
                    Required    = false
                },
                new Option("--logs")
                {
                    Description = "Write structured application logs to the specified log provider. Supported providers are 'console', 'elastic' (Elasticsearch), 'ai' (ApplicationInsights), 'seq'.",
                    Argument    = new Argument <string>("logs"),
                    Required    = false
                },
                new Option("--dtrace")
                {
                    Description = "Write distributed traces to the specified tracing provider. Supported providers are 'zipkin'.",
                    Argument    = new Argument <string>("trace"),
                    Required    = false,
                },
                new Option("--metrics")
                {
                    Description = "Write metrics to the specified metrics provider.",
                    Argument    = new Argument <string>("metrics"),
                    Required    = false
                },
                new Option("--debug")
                {
                    Argument = new Argument <string[]>("service")
                    {
                        Arity = ArgumentArity.ZeroOrMore,
                    },
                    Description = "Wait for debugger attach to specific service. Specify \"*\" to wait for all services.",
                    Required    = false
                },
                new Option("--docker")
                {
                    Description = "Run projects as docker containers.",
                    Required    = false
                },
                new Option("--dashboard")
                {
                    Description = "Launch dashboard on run.",
                    Required    = false
                },

                StandardOptions.Verbosity,
            };

            command.Handler = CommandHandler.Create <RunCommandArguments>(async args =>
            {
                // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
                if (args.Path is null)
                {
                    throw new CommandException("No project or solution file was found.");
                }

                var output = new OutputContext(args.Console, Verbosity.Info);

                output.WriteInfoLine("Loading Application Details...");
                var application = await ApplicationFactory.CreateAsync(output, args.Path);
                if (application.Services.Count == 0)
                {
                    throw new CommandException($"No services found in \"{application.Source.Name}\"");
                }

                var options = new HostOptions()
                {
                    Dashboard = args.Dashboard,
                    Docker    = args.Docker,
                    NoBuild   = args.NoBuild,
                    Port      = args.Port,

                    // parsed later by the diagnostics code
                    DistributedTraceProvider = args.Dtrace,
                    LoggingProvider          = args.Logs,
                    MetricsProvider          = args.Metrics,
                };
                options.Debug.AddRange(args.Debug);

                await application.ProcessExtensionsAsync(options, output, ExtensionContext.OperationKind.LocalRun);

                InitializeThreadPoolSettings(application.Services.Count);

                output.WriteInfoLine("Launching Tye Host...");
                output.WriteInfoLine(string.Empty);

                await using var host = new TyeHost(application.ToHostingApplication(), options);
                await host.RunAsync();
            });

            return(command);
        }
Exemple #6
0
        public static async Task <KubernetesIngressOutput> CreateIngress(
            OutputContext output,
            ApplicationBuilder application,
            IngressBuilder ingress)
        {
            var root       = new YamlMappingNode();
            var k8sVersion = await KubectlDetector.GetKubernetesServerVersion(output);

            root.Add("kind", "Ingress");
            if (k8sVersion >= MinIngressVersion)
            {
                root.Add("apiVersion", "networking.k8s.io/v1");
            }
            else
            {
                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)
            {
                return(new KubernetesIngressOutput(ingress.Name, new YamlDocument(root)));
            }

            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);

                    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}'.");
                    }

                    if (k8sVersion >= MinIngressVersion)
                    {
                        var backendService = new YamlMappingNode();
                        backend.Add("service", backendService);

                        backendService.Add("name", ingressRule.Service);

                        var backendPort = new YamlMappingNode();

                        backendService.Add("port", backendPort);

                        backendPort.Add("number", (binding.Port ?? 80).ToString(CultureInfo.InvariantCulture));
                        path.Add("pathType", "Prefix");
                    }
                    else
                    {
                        backend.Add("serviceName", ingressRule.Service);
                        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
                    {
                        if (ingressRule.PreservePath)
                        {
                            path.Add("path", $"/()({ingressRule.Path.Trim('/')}.*)");
                        }
                        else
                        {
                            var regex = $"{ingressRule.Path.TrimEnd('/')}(/|$)(.*)";
                            path.Add("path", regex);
                        }
                    }

                    // Only support prefix matching for now
                }
            }

            return(new KubernetesIngressOutput(ingress.Name, new YamlDocument(root)));
        }
 public ApplicationExecutor(OutputContext output)
 {
     this.output = output;
 }
        public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source, string?framework = null, ApplicationFactoryFilter?filter = null)
        {
            if (source is null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            var queue   = new Queue <(ConfigApplication, HashSet <string>)>();
            var visited = new HashSet <string>(StringComparer.OrdinalIgnoreCase);

            var rootConfig = ConfigFactory.FromFile(source);

            rootConfig.Validate();

            var root = new ApplicationBuilder(source, rootConfig.Name !)
            {
                Namespace = rootConfig.Namespace
            };

            queue.Enqueue((rootConfig, new HashSet <string>()));

            while (queue.TryDequeue(out var item))
            {
                // dependencies represents a set of all dependencies
                var(config, dependencies) = item;

                if (!visited.Add(config.Source.FullName))
                {
                    continue;
                }

                if (config == rootConfig && !string.IsNullOrEmpty(config.Registry))
                {
                    root.Registry = new ContainerRegistry(config.Registry);
                }

                if (config == rootConfig)
                {
                    root.Network = rootConfig.Network;
                }

                foreach (var configExtension in config.Extensions)
                {
                    var extension = new ExtensionConfiguration((string)configExtension["name"]);
                    foreach (var kvp in configExtension)
                    {
                        if (kvp.Key == "name")
                        {
                            continue;
                        }

                        extension.Data.Add(kvp.Key, kvp.Value);
                    }

                    root.Extensions.Add(extension);
                }

                var services = filter?.ServicesFilter != null?
                               config.Services.Where(filter.ServicesFilter).ToList() :
                                   config.Services;

                var sw = Stopwatch.StartNew();
                // Project services will be restored and evaluated before resolving all other services.
                // This batching will mitigate the performance cost of running MSBuild out of process.
                var projectServices = services.Where(s => !string.IsNullOrEmpty(s.Project));
                var projectMetadata = new Dictionary <string, string>();

                var msbuildEvaluationResult = await EvaluateProjectsAsync(
                    projects : projectServices,
                    configRoot : config.Source.DirectoryName !,
                    output : output);

                var msbuildEvaluationOutput = msbuildEvaluationResult
                                              .StandardOutput
                                              .Split(Environment.NewLine);

                var multiTFMProjects = new List <ConfigService>();

                foreach (var line in msbuildEvaluationOutput)
                {
                    var trimmed = line.Trim();
                    if (trimmed.StartsWith("Microsoft.Tye metadata: "))
                    {
                        var values       = line.Split(':', 3);
                        var projectName  = values[1].Trim();
                        var metadataPath = values[2].Trim();
                        projectMetadata.Add(projectName, metadataPath);

                        output.WriteDebugLine($"Resolved metadata for service {projectName} at {metadataPath}");
                    }
                    else if (trimmed.StartsWith("Microsoft.Tye cross-targeting project: "))
                    {
                        var values      = line.Split(':', 2);
                        var projectName = values[1].Trim();

                        var multiTFMConfigService = projectServices.First(p => string.Equals(p.Name, projectName, StringComparison.OrdinalIgnoreCase));
                        multiTFMConfigService.BuildProperties.Add(new BuildProperty {
                            Name = "TargetFramework", Value = framework ?? string.Empty
                        });
                        multiTFMProjects.Add(multiTFMConfigService);
                    }
                }

                if (multiTFMProjects.Any())
                {
                    output.WriteDebugLine("Re-evaluating multi-targeted projects");

                    var multiTFMEvaluationResult = await EvaluateProjectsAsync(
                        projects : multiTFMProjects,
                        configRoot : config.Source.DirectoryName !,
                        output : output);

                    var multiTFMEvaluationOutput = multiTFMEvaluationResult
                                                   .StandardOutput
                                                   .Split(Environment.NewLine);

                    foreach (var line in multiTFMEvaluationOutput)
                    {
                        var trimmed = line.Trim();
                        if (trimmed.StartsWith("Microsoft.Tye metadata: "))
                        {
                            var values       = line.Split(':', 3);
                            var projectName  = values[1].Trim();
                            var metadataPath = values[2].Trim();
                            projectMetadata.Add(projectName, metadataPath);

                            output.WriteDebugLine($"Resolved metadata for service {projectName} at {metadataPath}");
                        }
                        else if (trimmed.StartsWith("Microsoft.Tye cross-targeting project: "))
                        {
                            var values      = line.Split(':', 2);
                            var projectName = values[1].Trim();
                            throw new CommandException($"Unable to run {projectName}. Your project targets multiple frameworks. Specify which framework to run using '--framework' or a build property in tye.yaml.");
                        }
                    }
                }

                output.WriteDebugLine($"Restore and project evaluation took: {sw.Elapsed.TotalMilliseconds}ms");

                foreach (var configService in services)
                {
                    ServiceBuilder service;
                    if (root.Services.Any(s => s.Name == configService.Name))
                    {
                        // Even though this service has already created a service, we still need
                        // to update dependency information
                        AddToRootServices(root, dependencies, configService.Name);
                        continue;
                    }

                    if (!string.IsNullOrEmpty(configService.Project))
                    {
                        var project = new DotnetProjectServiceBuilder(configService.Name !, new FileInfo(configService.ProjectFullPath));
                        service = project;

                        project.Build = configService.Build ?? true;
                        project.Args  = configService.Args;
                        foreach (var buildProperty in configService.BuildProperties)
                        {
                            project.BuildProperties.Add(buildProperty.Name, buildProperty.Value);
                        }

                        project.Replicas = configService.Replicas ?? 1;
                        project.Liveness = configService.Liveness != null?GetProbeBuilder(configService.Liveness) : null;

                        project.Readiness = configService.Readiness != null?GetProbeBuilder(configService.Readiness) : null;

                        // We don't apply more container defaults here because we might need
                        // to prompt for the registry name.
                        project.ContainerInfo = new ContainerInfo()
                        {
                            UseMultiphaseDockerfile = false,
                        };

                        // If project evaluation is successful this should not happen, therefore an exception will be thrown.
                        if (!projectMetadata.ContainsKey(configService.Name))
                        {
                            throw new CommandException($"Evaluated project metadata file could not be found for service {configService.Name}");
                        }

                        ProjectReader.ReadProjectDetails(output, project, projectMetadata[configService.Name]);

                        // Do k8s by default.
                        project.ManifestInfo = new KubernetesManifestInfo();
                    }
                    else if (!string.IsNullOrEmpty(configService.Image))
                    {
                        var container = new ContainerServiceBuilder(configService.Name !, configService.Image !)
                        {
                            Args     = configService.Args,
                            Replicas = configService.Replicas ?? 1
                        };
                        service = container;

                        container.Liveness = configService.Liveness != null?GetProbeBuilder(configService.Liveness) : null;

                        container.Readiness = configService.Readiness != null?GetProbeBuilder(configService.Readiness) : null;
                    }
                    else if (!string.IsNullOrEmpty(configService.DockerFile))
                    {
                        var dockerFile = new DockerFileServiceBuilder(configService.Name !, configService.Image !)
                        {
                            Args       = configService.Args,
                            Build      = configService.Build ?? true,
                            Replicas   = configService.Replicas ?? 1,
                            DockerFile = Path.Combine(source.DirectoryName !, configService.DockerFile),
                            // Supplying an absolute path with trailing slashes fails for DockerFileContext when calling docker build, so trim trailing slash.
                            DockerFileContext = GetDockerFileContext(source, configService),
                            BuildArgs         = configService.DockerFileArgs
                        };
                        service = dockerFile;

                        dockerFile.Liveness = configService.Liveness != null?GetProbeBuilder(configService.Liveness) : null;

                        dockerFile.Readiness = configService.Readiness != null?GetProbeBuilder(configService.Readiness) : null;

                        // We don't apply more container defaults here because we might need
                        // to prompt for the registry name.
                        dockerFile.ContainerInfo = new ContainerInfo()
                        {
                            UseMultiphaseDockerfile = false,
                        };

                        // Do k8s by default.
                        dockerFile.ManifestInfo = new KubernetesManifestInfo();
                    }
                    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(config.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(config.Source.Directory !.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) :
                                                   workingDirectory,
                                                   Replicas = configService.Replicas ?? 1
                        };
                        service = executable;

                        executable.Liveness = configService.Liveness != null?GetProbeBuilder(configService.Liveness) : null;

                        executable.Readiness = configService.Readiness != null?GetProbeBuilder(configService.Readiness) : null;
                    }
                    else if (!string.IsNullOrEmpty(configService.Include))
                    {
                        var expandedYaml = Environment.ExpandEnvironmentVariables(configService.Include);

                        var nestedConfig = GetNestedConfig(rootConfig, Path.Combine(config.Source.DirectoryName !, expandedYaml));
                        queue.Enqueue((nestedConfig, new HashSet <string>()));

                        AddToRootServices(root, dependencies, configService.Name);
                        continue;
                    }
                    else if (!string.IsNullOrEmpty(configService.Repository))
                    {
                        // clone to .tye folder
                        var path = configService.CloneDirectory ?? Path.Join(rootConfig.Source.DirectoryName, ".tye", "deps");
                        if (!Directory.Exists(path))
                        {
                            Directory.CreateDirectory(path);
                        }

                        var clonePath = Path.Combine(path, configService.Name);

                        if (!Directory.Exists(clonePath))
                        {
                            if (!await GitDetector.Instance.IsGitInstalled.Value)
                            {
                                throw new CommandException($"Cannot clone repository {configService.Repository} because git is not installed. Please install git if you'd like to use \"repository\" in tye.yaml.");
                            }

                            var result = await ProcessUtil.RunAsync("git", $"clone {configService.Repository} \"{clonePath}\"", workingDirectory : path, throwOnError : false);

                            if (result.ExitCode != 0)
                            {
                                throw new CommandException($"Failed to clone repository {configService.Repository} with exit code {result.ExitCode}.{Environment.NewLine}{result.StandardError}{result.StandardOutput}.");
                            }
                        }

                        if (!ConfigFileFinder.TryFindSupportedFile(clonePath, out var file, out var errorMessage))
                        {
                            throw new CommandException(errorMessage !);
                        }

                        // pick different service type based on what is in the repo.
                        var nestedConfig = GetNestedConfig(rootConfig, file);

                        queue.Enqueue((nestedConfig, new HashSet <string>()));

                        AddToRootServices(root, dependencies, configService.Name);

                        continue;
                    }
                    else if (!string.IsNullOrEmpty(configService.AzureFunction))
                    {
                        var azureFunctionDirectory = Path.Combine(config.Source.DirectoryName !, configService.AzureFunction);

                        var functionBuilder = new AzureFunctionServiceBuilder(
                            configService.Name,
                            azureFunctionDirectory)
                        {
                            Args               = configService.Args,
                            Replicas           = configService.Replicas ?? 1,
                            FuncExecutablePath = configService.FuncExecutable,
                        };

                        foreach (var proj in Directory.EnumerateFiles(azureFunctionDirectory))
                        {
                            var fileInfo = new FileInfo(proj);
                            if (fileInfo.Extension == ".csproj" || fileInfo.Extension == ".fsproj")
                            {
                                functionBuilder.ProjectFile = fileInfo.FullName;
                                break;
                            }
                        }

                        // TODO liveness?
                        service = functionBuilder;
                    }
                    else if (configService.External)
                    {
                        var external = new ExternalServiceBuilder(configService.Name);
                        service = external;
                    }
                    else
                    {
                        throw new CommandException("Unable to determine service type.");
                    }

                    // Add dependencies to ourself before adding ourself to avoid self reference
                    service.Dependencies.UnionWith(dependencies);

                    AddToRootServices(root, dependencies, service.Name);

                    root.Services.Add(service);

                    // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding
                    if (configService.Bindings.Count == 0 &&
                        service is ProjectServiceBuilder project2 &&
                        project2.IsAspNet)
                    {
                        // HTTP is the default binding
                        service.Bindings.Add(new BindingBuilder()
                        {
                            Protocol = "http"
                        });
                        service.Bindings.Add(new BindingBuilder()
                        {
                            Name = "https", Protocol = "https"
                        });
                    }
        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 configExtension in config.Extensions)
            {
                var extension = new ExtensionConfiguration((string)configExtension["name"]);
                foreach (var kvp in configExtension)
                {
                    if (kvp.Key == "name")
                    {
                        continue;
                    }

                    extension.Data.Add(kvp.Key, kvp.Value);
                }

                builder.Extensions.Add(extension);
            }

            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 prompt for the registry name.
                    project.ContainerInfo = new ContainerInfo()
                    {
                        UseMultiphaseDockerfile = false,
                    };

                    // Do k8s by default.
                    project.ManifestInfo = new KubernetesManifestInfo();
                }
                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);

                // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding
                if (configService.Bindings.Count == 0 &&
                    service is ProjectServiceBuilder project2 &&
                    project2.IsAspNet)
                {
                    // HTTP is the default binding
                    service.Bindings.Add(new BindingBuilder()
                    {
                        Protocol = "http"
                    });

                    service.Bindings.Add(new BindingBuilder()
                    {
                        Name     = "https",
                        Protocol = "https"
                    });
                }
Exemple #10
0
 public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingress)
 {
     ingress.Outputs.Add(await KubernetesManifestGenerator.CreateIngress(output, application, ingress));
 }
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application)
        {
            using var step = output.BeginStep("Applying Kubernetes Manifests...");

            if (await KubectlDetector.GetKubernetesServerVersion(output) == null)
            {
                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.");
            }

            using var tempFile = TempFile.Create();
            output.WriteInfoLine($"Writing output to '{tempFile.FilePath}'.");

            {
                await using var stream = File.OpenWrite(tempFile.FilePath);
                await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), leaveOpen: true);

                await ApplicationYamlWriter.WriteAsync(output, writer, application);
            }

            var ns = $"namespace ${application.Namespace}";

            if (string.IsNullOrEmpty(application.Namespace))
            {
                ns = "current namespace";
            }
            output.WriteDebugLine($"Running 'kubectl apply' in ${ns}");
            output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
            var capture  = output.Capture();
            var exitCode = await ProcessUtil.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 application '{application.Name}'.");
            if (application.Ingress.Count > 0)
            {
                output.WriteInfoLine($"Waiting for ingress to be deployed. This may take a long time.");
                foreach (var ingress in application.Ingress)
                {
                    using var ingressStep = output.BeginStep($"Retrieving details for {ingress.Name}...");

                    var done = false;

                    Action <string> complete = line =>
                    {
                        done = line != "''";
                        if (done)
                        {
                            output.WriteInfoLine($"IngressIP: {line}");
                        }
                    };

                    var retries = 0;
                    while (!done && retries < 60)
                    {
                        var ingressExitCode = await ProcessUtil.ExecuteAsync(
                            "kubectl",
                            $"get ingress {ingress.Name} -o jsonpath='{{..ip}}'",
                            Environment.CurrentDirectory,
                            complete,
                            capture.StdErr);

                        if (ingressExitCode != 0)
                        {
                            throw new CommandException("'kubectl get ingress' failed");
                        }

                        if (!done)
                        {
                            await Task.Delay(2000);

                            retries++;
                        }
                    }
                }
            }
        }
Exemple #12
0
        public static async Task ExecuteUndeployAsync(OutputContext output, ApplicationBuilder 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));
            }
        }
        // For layering reasons this has to live in the `tye` project. We don't want to leak
        // the extensions themselves into Tye.Core.
        public static async Task ProcessExtensionsAsync(this ApplicationBuilder application, OutputContext output, ExtensionContext.OperationKind operation)
        {
            foreach (var extensionConfig in application.Extensions)
            {
                if (!WellKnownExtensions.Extensions.TryGetValue(extensionConfig.Name, out var extension))
                {
                    throw new CommandException($"Could not find the extension '{extensionConfig.Name}'.");
                }

                var context = new ExtensionContext(application, output, operation);
                await extension.ProcessAsync(context, extensionConfig);
            }
        }
Exemple #14
0
        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 abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application);
        public static async Task BuildContainerImageAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            if (application is null)
            {
                throw new ArgumentNullException(nameof(application));
            }

            if (project is null)
            {
                throw new ArgumentNullException(nameof(project));
            }

            if (container is null)
            {
                throw new ArgumentNullException(nameof(container));
            }

            string contextDirectory;
            var    dockerFilePath = Path.Combine(project.ProjectFile.DirectoryName, "Dockerfile");

            TempFile?     tempFile      = null;
            TempDirectory?tempDirectory = null;

            try
            {
                // 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.
                //
                // For the cases where generate a Dockerfile, we have the constraint that we need
                // to place it on the same drive (Windows) as the docker context.
                if (container.UseMultiphaseDockerfile ?? true)
                {
                    // For a multi-phase Docker build, the context is always the project directory.
                    contextDirectory = ".";

                    if (File.Exists(dockerFilePath))
                    {
                        output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'.");
                    }
                    else
                    {
                        // We need to write the file, let's stick it under obj.
                        Directory.CreateDirectory(project.IntermediateOutputPath);
                        dockerFilePath = Path.Combine(project.IntermediateOutputPath, "Dockerfile");

                        // Clean up file when done building image
                        tempFile = new TempFile(dockerFilePath);

                        await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, tempFile.FilePath);
                    }
                }
                else
                {
                    // For a single-phase Docker build the context is always the directory containing the publish
                    // output. We need to put the Dockerfile in the context directory so it's on the same drive (Windows).
                    var publishOutput = project.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;

                    // Clean up directory when done building image
                    tempDirectory = new TempDirectory(publishOutput.Directory);

                    if (File.Exists(dockerFilePath))
                    {
                        output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'.");
                        File.Copy(dockerFilePath, Path.Combine(contextDirectory, "Dockerfile"));
                        dockerFilePath = Path.Combine(contextDirectory, "Dockerfile");
                    }
                    else
                    {
                        // No need to clean up, it's in a directory we're already cleaning up.
                        dockerFilePath = Path.Combine(contextDirectory, "Dockerfile");
                        await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, dockerFilePath);
                    }
                }

                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}\"",
                    project.ProjectFile.DirectoryName,
                    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}'");
                project.Outputs.Add(new DockerImageOutput(container.ImageName !, container.ImageTag !));
            }
            finally
            {
                tempDirectory?.Dispose();
                tempFile?.Dispose();
            }
        }
 public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingres);
Exemple #18
0
        private static Command CreateRunCommand(string[] args)
        {
            var command = new Command("run", "run the application")
            {
                CommonArguments.Path_Required,
            };

            // TODO: We'll need to support a --build-args
            command.AddOption(new Option("--no-build")
            {
                Description = "Do not build project files before running.",
                Required    = false
            });

            command.AddOption(new Option("--port")
            {
                Description = "The port to run control plane on.",
                Argument    = new Argument <int>("port"),
                Required    = false
            });

            command.AddOption(new Option("--logs")
            {
                Description = "Write structured application logs to the specified log providers. Supported providers are console, elastic (Elasticsearch), ai (ApplicationInsights), seq.",
                Argument    = new Argument <string>("logs"),
                Required    = false
            });

            command.AddOption(new Option("--dtrace")
            {
                Description = "Write distributed traces to the specified providers. Supported providers are zipkin.",
                Argument    = new Argument <string>("logs"),
                Required    = false
            });

            command.AddOption(new Option("--debug")
            {
                Argument    = new Argument <string[]>("service"),
                Description = "Wait for debugger attach to specific service. Specify \"*\" to wait for all services.",
                Required    = false
            });

            command.AddOption(new Option("--docker")
            {
                Description = "Run projects as docker containers.",
                Required    = false
            });

            command.Handler = CommandHandler.Create <IConsole, FileInfo, string[]>(async(console, path, debug) =>
            {
                // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
                if (path is null)
                {
                    throw new CommandException("No project or solution file was found.");
                }

                var output      = new OutputContext(console, Verbosity.Quiet);
                var application = await ApplicationFactory.CreateAsync(output, path);

                await application.ProcessExtensionsAsync(ExtensionContext.OperationKind.LocalRun);

                InitializeThreadPoolSettings(application.Services.Count);

                if (application.Services.Count == 0)
                {
                    throw new CommandException($"No services found in \"{application.Source.Name}\"");
                }

                using var host = new TyeHost(application.ToHostingApplication(), args, debug);
                await host.RunAsync();
            });

            return(command);
        }
 public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service);
Exemple #20
0
        public static KubernetesServiceOutput CreateService(
            OutputContext output,
            ApplicationBuilder application,
            ProjectServiceBuilder project,
            DeploymentManifestInfo deployment,
            ServiceManifestInfo service)
        {
            var root = new YamlMappingNode();

            root.Add("kind", "Service");
            root.Add("apiVersion", "v1");

            var metadata = new YamlMappingNode();

            root.Add("metadata", metadata);
            metadata.Add("name", project.Name);

            if (!string.IsNullOrEmpty(application.Namespace))
            {
                metadata.Add("namespace", application.Namespace);
            }

            if (service.Annotations.Count > 0)
            {
                var annotations = new YamlMappingNode();
                metadata.Add("annotations", annotations);

                foreach (var annotation in service.Annotations)
                {
                    annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value)
                    {
                        Style = ScalarStyle.SingleQuoted,
                    });
                }
            }

            var labels = new YamlMappingNode();

            metadata.Add("labels", labels);
            foreach (var label in service.Labels)
            {
                labels.Add(label.Key, new YamlScalarNode(label.Value)
                {
                    Style = ScalarStyle.SingleQuoted,
                });
            }

            var spec = new YamlMappingNode();

            root.Add("spec", spec);

            var selector = new YamlMappingNode();

            spec.Add("selector", selector);

            // We need the name so we can use it with selector.
            if (!deployment.Labels.TryGetValue("app.kubernetes.io/name", out var selectorLabelValue))
            {
                throw new InvalidOperationException("The label 'app.kubernetes.io/name` is required.");
            }
            selector.Add("app.kubernetes.io/name", selectorLabelValue);

            spec.Add("type", "ClusterIP");

            var ports = new YamlSequenceNode();

            spec.Add("ports", ports);

            // We figure out the port based on bindings
            foreach (var binding in project.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)
                {
                    continue;
                }

                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.ContainerPort ?? binding.Port.Value).ToString());
            }

            return(new KubernetesServiceOutput(project.Name, new YamlDocument(root)));
        }
Exemple #21
0
        public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
        {
            // No need to do this computation for a non-project since we're not deploying it.
            if (!(service is ProjectServiceBuilder project))
            {
                return(Task.CompletedTask);
            }

            // Compute ASPNETCORE_URLS based on the bindings exposed by *this* project.
            foreach (var binding in service.Bindings)
            {
                if (binding.Protocol == null && binding.ConnectionString == null)
                {
                    binding.Protocol = "http";
                }

                if (binding.Port == null && binding.Protocol == "http")
                {
                    binding.Port = 80;
                }

                if (binding.Protocol == "http")
                {
                    var port = binding.Port ?? 80;
                    var urls = $"http://*{(port == 80 ? "" : (":" + port.ToString()))}";
                    project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("ASPNETCORE_URLS")
                    {
                        Value = urls,
                    });
                    project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("PORT")
                    {
                        Value = port.ToString(CultureInfo.InvariantCulture),
                    });
                    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.Bindings)
                {
                    // The other thing is a project, and will be deployed along with this
                    // service.
                    var configName =
                        (binding.Name is null || binding.Name == other.Name) ?
                        other.Name.ToUpperInvariant() :
                        $"{other.Name.ToUpperInvariant()}__{binding.Name.ToUpperInvariant()}";
                    if (other is ProjectServiceBuilder)
                    {
                        if (!string.IsNullOrEmpty(binding.ConnectionString))
                        {
                            // Special case for connection strings
                            bindings.Bindings.Add(new EnvironmentVariableInputBinding($"CONNECTIONSTRING__{configName}", binding.ConnectionString));
                            continue;
                        }

                        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.Protocol == null)
                        {
                            binding.Protocol = "http";
                        }

                        if (binding.Port == null && binding.Protocol == "http")
                        {
                            binding.Port = 80;
                        }

                        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.Name));
                    }
                    else
                    {
                        // The other service is not a project, so we'll use secrets.
                        bindings.Bindings.Add(new SecretInputBinding(
                                                  name: $"binding-{Environment}-{other.Name}-{binding.Name ?? other.Name}-secret",
                                                  filename: $"CONNECTIONSTRING__{configName}",
                                                  other,
                                                  binding));
                    }
                }
            }

            return(Task.CompletedTask);
        }
Exemple #22
0
        public static KubernetesDeploymentOutput CreateDeployment(
            OutputContext output,
            ApplicationBuilder application,
            ProjectServiceBuilder project,
            DeploymentManifestInfo deployment)
        {
            var bindings = project.Outputs.OfType <ComputedBindings>().FirstOrDefault();

            var root = new YamlMappingNode
            {
                { "kind", "Deployment" },
                { "apiVersion", "apps/v1" }
            };

            var metadata = new YamlMappingNode();

            root.Add("metadata", metadata);
            metadata.Add("name", project.Name);
            if (!string.IsNullOrEmpty(application.Namespace))
            {
                metadata.Add("namespace", application.Namespace);
            }

            if (deployment.Annotations.Count > 0)
            {
                var annotations = new YamlMappingNode();
                metadata.Add("annotations", annotations);

                foreach (var annotation in deployment.Annotations)
                {
                    annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value)
                    {
                        Style = ScalarStyle.SingleQuoted,
                    });
                }
            }

            var labels = new YamlMappingNode();

            metadata.Add("labels", labels);
            foreach (var label in deployment.Labels)
            {
                labels.Add(label.Key, new YamlScalarNode(label.Value)
                {
                    Style = ScalarStyle.SingleQuoted,
                });
            }

            var spec = new YamlMappingNode();

            root.Add("spec", spec);

            spec.Add("replicas", project.Replicas.ToString());

            var selector = new YamlMappingNode();

            spec.Add("selector", selector);

            var matchLabels = new YamlMappingNode();

            selector.Add("matchLabels", matchLabels);

            // We need the name so we can use it with matchLabels.
            if (!deployment.Labels.TryGetValue("app.kubernetes.io/name", out var matchLabelsLabelValue))
            {
                throw new InvalidOperationException("The label 'app.kubernetes.io/name` is required.");
            }
            matchLabels.Add("app.kubernetes.io/name", matchLabelsLabelValue);

            var template = new YamlMappingNode();

            spec.Add("template", template);

            metadata = new YamlMappingNode();
            template.Add("metadata", metadata);

            if (deployment.Annotations.Count > 0)
            {
                var annotations = new YamlMappingNode();
                metadata.Add("annotations", annotations);

                foreach (var annotation in deployment.Annotations)
                {
                    annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value)
                    {
                        Style = ScalarStyle.SingleQuoted,
                    });
                }
            }

            labels = new YamlMappingNode();
            metadata.Add("labels", labels);
            foreach (var label in deployment.Labels)
            {
                labels.Add(label.Key, new YamlScalarNode(label.Value)
                {
                    Style = ScalarStyle.SingleQuoted,
                });
            }

            spec = new YamlMappingNode();
            template.Add("spec", spec);

            if (project.Sidecars.Count > 0)
            {
                // Share process namespace when we have sidecars. So we can list other processes.
                // see: https://kubernetes.io/docs/tasks/configure-pod-container/share-process-namespace/#understanding-process-namespace-sharing
                spec.Add("shareProcessNamespace", new YamlScalarNode("true")
                {
                    Style = ScalarStyle.Plain
                });
            }

            if (project.RelocateDiagnosticsDomainSockets)
            {
                // Our diagnostics functionality uses $TMPDIR to locate other dotnet processes through
                // eventpipe. see: https://github.com/dotnet/diagnostics/blob/master/documentation/design-docs/ipc-protocol.md#transport
                //
                // In order for diagnostics features to 'find' each other, we need to make $TMPDIR into
                // something shared.
                //
                // see: https://kubernetes.io/docs/tasks/access-application-cluster/communicate-containers-same-pod-shared-volume/
                project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("TMPDIR")
                {
                    Value = "/var/tye/diagnostics",
                });

                foreach (var sidecar in project.Sidecars)
                {
                    sidecar.EnvironmentVariables.Add(new EnvironmentVariableBuilder("TMPDIR")
                    {
                        Value = "/var/tye/diagnostics",
                    });
                }
            }

            var containers = new YamlSequenceNode();

            spec.Add("containers", containers);

            var images = project.Outputs.OfType <DockerImageOutput>();

            foreach (var image in images)
            {
                var container = new YamlMappingNode();
                containers.Add(container);
                container.Add("name", project.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 (project.EnvironmentVariables.Count > 0 ||

                    // We generate ASPNETCORE_URLS if there are bindings for http
                    project.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) ||

                    // We generate environment variables for other services if there dependencies
                    bindings?.Bindings.Count > 0)
                {
                    var env = new YamlSequenceNode();
                    container.Add("env", env);

                    foreach (var kvp in project.EnvironmentVariables)
                    {
                        env.Add(new YamlMappingNode()
                        {
                            { "name", kvp.Name },
                            { "value", new YamlScalarNode(kvp.Value)
                              {
                                  Style = ScalarStyle.SingleQuoted,
                              } },
                        });
                    }

                    if (bindings != null)
                    {
                        AddEnvironmentVariablesForComputedBindings(env, bindings);
                    }

                    if (project.RelocateDiagnosticsDomainSockets)
                    {
                        // volumeMounts:
                        // - name: shared-data
                        //   mountPath: /usr/share/nginx/html
                        var volumeMounts = new YamlSequenceNode();
                        container.Add("volumeMounts", volumeMounts);

                        var volumeMount = new YamlMappingNode();
                        volumeMounts.Add(volumeMount);
                        volumeMount.Add("name", "tye-diagnostics");
                        volumeMount.Add("mountPath", "/var/tye/diagnostics");
                    }
                }

                if (project.Bindings.Count > 0)
                {
                    var ports = new YamlSequenceNode();
                    container.Add("ports", ports);

                    foreach (var binding in project.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)
                        {
                            continue;
                        }

                        var containerPort = new YamlMappingNode();
                        ports.Add(containerPort);
                        containerPort.Add("containerPort", (binding.ContainerPort ?? binding.Port.Value).ToString());
                    }
                }

                if (project.Liveness != null)
                {
                    AddProbe(project, container, project.Liveness !, "livenessProbe");
                }

                if (project.Readiness != null)
                {
                    AddProbe(project, container, project.Readiness !, "readinessProbe");
                }
            }

            foreach (var sidecar in project.Sidecars)
            {
                var container = new YamlMappingNode();
                containers.Add(container);
                container.Add("name", sidecar.Name);        // NOTE: to really support multiple images we'd need to generate unique names.
                container.Add("image", $"{sidecar.ImageName}:{sidecar.ImageTag}");
                container.Add("imagePullPolicy", "Always"); // helps avoid problems with development + weak versioning

                if (sidecar.Args.Count > 0)
                {
                    var args = new YamlSequenceNode();
                    container.Add("args", args);

                    foreach (var arg in sidecar.Args)
                    {
                        args.Add(new YamlScalarNode(arg)
                        {
                            Style = ScalarStyle.SingleQuoted,
                        });
                    }
                }

                var sidecarBindings = sidecar.Outputs.OfType <ComputedBindings>().FirstOrDefault();
                if (sidecar.EnvironmentVariables.Count > 0 || sidecarBindings?.Bindings.Count > 0)
                {
                    var env = new YamlSequenceNode();
                    container.Add("env", env);

                    foreach (var kvp in sidecar.EnvironmentVariables)
                    {
                        env.Add(new YamlMappingNode()
                        {
                            { "name", kvp.Name },
                            { "value", new YamlScalarNode(kvp.Value)
                              {
                                  Style = ScalarStyle.SingleQuoted,
                              } },
                        });
                    }

                    if (sidecarBindings != null)
                    {
                        AddEnvironmentVariablesForComputedBindings(env, sidecarBindings);
                    }
                }

                if (!project.RelocateDiagnosticsDomainSockets)
                {
                    continue;
                }

                // volumeMounts:
                // - name: shared-data
                //   mountPath: /usr/share/nginx/html
                var volumeMounts = new YamlSequenceNode();
                container.Add("volumeMounts", volumeMounts);

                var volumeMount = new YamlMappingNode();
                volumeMounts.Add(volumeMount);
                volumeMount.Add("name", "tye-diagnostics");
                volumeMount.Add("mountPath", "/var/tye/diagnostics");
            }

            if (!project.RelocateDiagnosticsDomainSockets)
            {
                return(new KubernetesDeploymentOutput(project.Name, new YamlDocument(root)));
            }
            // volumes:
            // - name: shared-data
            //   emptyDir: {}
            var volumes = new YamlSequenceNode();

            spec.Add("volumes", volumes);

            var volume = new YamlMappingNode();

            volumes.Add(volume);
            volume.Add("name", "tye-diagnostics");
            volume.Add("emptyDir", new YamlMappingNode());

            return(new KubernetesDeploymentOutput(project.Name, new YamlDocument(root)));
        }
Exemple #23
0
 private static void LogIt(OutputContext output)
 {
     output.WriteDebugLine("Loaded: " + typeof(ProjectInstance).Assembly.FullName);
     output.WriteDebugLine("Loaded From: " + typeof(ProjectInstance).Assembly.Location);
 }
Exemple #24
0
        private static void EvaluateProject(OutputContext output, DotnetProjectServiceBuilder project, string metadataFile)
        {
            var sw = Stopwatch.StartNew();

            var metadata     = new Dictionary <string, string>();
            var metadataKVPs = File.ReadLines(metadataFile).Select(l => l.Split(new[] { ':' }, 2));

            foreach (var metadataKVP in metadataKVPs)
            {
                if (!string.IsNullOrEmpty(metadataKVP[1]))
                {
                    metadata.Add(metadataKVP[0], metadataKVP[1].Trim());
                }
            }

            // Reading a few different version properties to be more resilient.
            var version = GetMetadataValueOrNull("AssemblyInformationalVersion") ??
                          GetMetadataValueOrNull("InformationalVersion") ??
                          GetMetadataValueOrEmpty("Version");

            project.Version = version;
            output.WriteDebugLine($"Found application version: {version}");

            project.TargetFrameworks = GetMetadataValueOrNull("TargetFrameworks")?.Split(';', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty <string>();

            // Figure out if functions app.
            // If so, run app with function host.
            project.RunCommand             = GetMetadataValueOrEmpty("RunCommand");
            project.RunArguments           = GetMetadataValueOrEmpty("RunArguments");
            project.TargetPath             = GetMetadataValueOrEmpty("TargetPath");
            project.PublishDir             = GetMetadataValueOrEmpty("PublishDir");
            project.AssemblyName           = GetMetadataValueOrEmpty("AssemblyName");
            project.IntermediateOutputPath = GetMetadataValueOrEmpty("IntermediateOutputPath");

            output.WriteDebugLine($"RunCommand={project.RunCommand}");
            output.WriteDebugLine($"RunArguments={project.RunArguments}");
            output.WriteDebugLine($"TargetPath={project.TargetPath}");
            output.WriteDebugLine($"PublishDir={project.PublishDir}");
            output.WriteDebugLine($"AssemblyName={project.AssemblyName}");
            output.WriteDebugLine($"IntermediateOutputPath={project.IntermediateOutputPath}");

            // Normalize directories to their absolute paths
            project.IntermediateOutputPath = Path.Combine(project.ProjectFile.DirectoryName !, NormalizePath(project.IntermediateOutputPath));
            project.TargetPath             = Path.Combine(project.ProjectFile.DirectoryName !, NormalizePath(project.TargetPath));
            project.PublishDir             = Path.Combine(project.ProjectFile.DirectoryName !, NormalizePath(project.PublishDir));

            var targetFramework = GetMetadataValueOrEmpty("TargetFramework");

            project.TargetFramework = targetFramework;
            output.WriteDebugLine($"Found target framework: {targetFramework}");

            // TODO: Parse the name and version manually out of the TargetFramework field if it's non-null
            project.TargetFrameworkName    = GetMetadataValueOrNull("_ShortFrameworkIdentifier") ?? project.TargetFramework.TrimEnd(".0123456789".ToCharArray());
            project.TargetFrameworkVersion = GetMetadataValueOrNull("_ShortFrameworkVersion") ?? GetMetadataValueOrEmpty("_TargetFrameworkVersionWithoutV");
            output.WriteDebugLine($"Parsed target framework name: {project.TargetFrameworkName}");
            output.WriteDebugLine($"Parsed target framework version: {project.TargetFrameworkVersion}");

            var sharedFrameworks = GetMetadataValueOrNull("FrameworkReference")?.Split(';') ?? Enumerable.Empty <string>();

            project.Frameworks.AddRange(sharedFrameworks.Select(s => new Framework(s)));
            output.WriteDebugLine($"Found shared frameworks: {string.Join(", ", sharedFrameworks)}");

            // determine container base image
            if (project.ContainerInfo != null)
            {
                project.ContainerInfo.BaseImageName = GetMetadataValueOrEmpty("ContainerBaseImage");
                project.ContainerInfo.BaseImageTag  = GetMetadataValueOrEmpty("ContainerBaseTag");
            }

            project.IsAspNet = project.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App") ||
                               GetMetadataValueOrEmpty("MicrosoftNETPlatformLibrary") == "Microsoft.AspNetCore.App" ||
                               MetadataIsTrue("_AspNetCoreAppSharedFxIsEnabled") ||
                               MetadataIsTrue("UsingMicrosoftNETSdkWeb");

            output.WriteDebugLine($"IsAspNet={project.IsAspNet}");

            output.WriteDebugLine($"Evaluation Took: {sw.Elapsed.TotalMilliseconds}ms");

            string?GetMetadataValueOrNull(string key) => metadata !.TryGetValue(key, out var value) ? value : null;
            string GetMetadataValueOrEmpty(string key) => metadata !.TryGetValue(key, out var value) ? value : string.Empty;
            bool MetadataIsTrue(string key) => metadata !.TryGetValue(key, out var value) && bool.Parse(value);
        }
Exemple #25
0
        private static void EvaluateProject(OutputContext output, DotnetProjectServiceBuilder project)
        {
            var sw = Stopwatch.StartNew();

            // Currently we only log at debug level.
            var logger = new ConsoleLogger(
                verbosity: LoggerVerbosity.Normal,
                write: message => output.WriteDebug(message),
                colorSet: null,
                colorReset: null);

            // We need to isolate projects from each other for testing. MSBuild does not support
            // loading the same project twice in the same collection.
            var projectCollection = new ProjectCollection();

            ProjectInstance projectInstance;

            Microsoft.Build.Evaluation.Project msbuildProject;

            try
            {
                output.WriteDebugLine($"Loading project '{project.ProjectFile.FullName}'.");
                msbuildProject = Microsoft.Build.Evaluation.Project.FromFile(project.ProjectFile.FullName, new ProjectOptions()
                {
                    ProjectCollection = projectCollection,
                    GlobalProperties  = project.BuildProperties
                });
                projectInstance = msbuildProject.CreateProjectInstance();
                output.WriteDebugLine($"Loaded project '{project.ProjectFile.FullName}'.");
            }
            catch (Exception ex)
            {
                throw new CommandException($"Failed to load project: '{project.ProjectFile.FullName}'.", ex);
            }

            try
            {
                AssemblyLoadContext.Default.Resolving += ResolveAssembly;

                output.WriteDebugLine($"Restoring project '{project.ProjectFile.FullName}'.");

                // Similar to what MSBuild does for restore:
                // https://github.com/microsoft/msbuild/blob/3453beee039fb6f5ccc54ac783ebeced31fec472/src/MSBuild/XMake.cs#L1417
                //
                // We need to do restore as a separate operation
                var restoreRequest = new BuildRequestData(
                    projectInstance,
                    targetsToBuild: new[] { "Restore" },
                    hostServices: null,
                    flags: BuildRequestDataFlags.ClearCachesAfterBuild | BuildRequestDataFlags.SkipNonexistentTargets | BuildRequestDataFlags.IgnoreMissingEmptyAndInvalidImports);

                var parameters = new BuildParameters(projectCollection)
                {
                    Loggers = new[] { logger, },
                };

                // We don't really look at the result, because it's not clear we should halt totally
                // if restore fails.
                var restoreResult = BuildManager.DefaultBuildManager.Build(parameters, restoreRequest);
                output.WriteDebugLine($"Restored project '{project.ProjectFile.FullName}'.");

                msbuildProject.MarkDirty();
                projectInstance = msbuildProject.CreateProjectInstance();

                var targets = new List <string>()
                {
                    "ResolveReferences",
                    "ResolvePackageDependenciesDesignTime",
                    "PrepareResources",
                    "GetAssemblyAttributes",
                };

                var result = projectInstance.Build(
                    targets: targets.ToArray(),
                    loggers: new[] { logger, });

                // If the build fails, we're not really blocked from doing our work.
                // For now we just log the output to debug. There are errors that occur during
                // running these targets we don't really care as long as we get the data.
            }
            finally
            {
                AssemblyLoadContext.Default.Resolving -= ResolveAssembly;
            }

            // Reading a few different version properties to be more resilient.
            var version =
                projectInstance.GetProperty("AssemblyInformationalVersion")?.EvaluatedValue ??
                projectInstance.GetProperty("InformationalVersion")?.EvaluatedValue ??
                projectInstance.GetProperty("Version").EvaluatedValue;

            project.Version = version;
            output.WriteDebugLine($"Found application version: {version}");

            var targetFrameworks = projectInstance.GetPropertyValue("TargetFrameworks");

            project.TargetFrameworks = targetFrameworks.Split(';', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty <string>();

            // Figure out if functions app.
            // If so, run app with function host.
            project.RunCommand             = projectInstance.GetPropertyValue("RunCommand");
            project.RunArguments           = projectInstance.GetPropertyValue("RunArguments");
            project.TargetPath             = projectInstance.GetPropertyValue("TargetPath");
            project.PublishDir             = projectInstance.GetPropertyValue("PublishDir");
            project.AssemblyName           = projectInstance.GetPropertyValue("AssemblyName");
            project.IntermediateOutputPath = projectInstance.GetPropertyValue("IntermediateOutputPath");

            output.WriteDebugLine($"RunCommand={project.RunCommand}");
            output.WriteDebugLine($"RunArguments={project.RunArguments}");
            output.WriteDebugLine($"TargetPath={project.TargetPath}");
            output.WriteDebugLine($"PublishDir={project.PublishDir}");
            output.WriteDebugLine($"AssemblyName={project.AssemblyName}");
            output.WriteDebugLine($"IntermediateOutputPath={project.IntermediateOutputPath}");

            // Normalize directories to their absolute paths
            project.IntermediateOutputPath = Path.Combine(project.ProjectFile.DirectoryName, NormalizePath(project.IntermediateOutputPath));
            project.TargetPath             = Path.Combine(project.ProjectFile.DirectoryName, NormalizePath(project.TargetPath));
            project.PublishDir             = Path.Combine(project.ProjectFile.DirectoryName, NormalizePath(project.PublishDir));

            var targetFramework = projectInstance.GetPropertyValue("TargetFramework");

            project.TargetFramework = targetFramework;
            output.WriteDebugLine($"Found target framework: {targetFramework}");

            // TODO: Parse the name and version manually out of the TargetFramework field if it's non-null
            project.TargetFrameworkName    = projectInstance.GetPropertyValue("_ShortFrameworkIdentifier");
            project.TargetFrameworkVersion = projectInstance.GetPropertyValue("_ShortFrameworkVersion") ?? projectInstance.GetPropertyValue("_TargetFrameworkVersionWithoutV");

            var sharedFrameworks = projectInstance.GetItems("FrameworkReference").Select(i => i.EvaluatedInclude).ToList();

            project.Frameworks.AddRange(sharedFrameworks.Select(s => new Framework(s)));
            output.WriteDebugLine($"Found shared frameworks: {string.Join(", ", sharedFrameworks)}");

            // determine container base image
            if (project.ContainerInfo != null)
            {
                project.ContainerInfo.BaseImageName = projectInstance.GetPropertyValue("ContainerBaseImage");
                project.ContainerInfo.BaseImageTag  = projectInstance.GetPropertyValue("ContainerBaseTag");
            }

            bool PropertyIsTrue(string property)
            {
                return(projectInstance.GetPropertyValue(property) is string s && !string.IsNullOrEmpty(s) && bool.Parse(s));
            }

            project.IsAspNet = project.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App") ||
                               projectInstance.GetPropertyValue("MicrosoftNETPlatformLibrary") == "Microsoft.AspNetCore.App" ||
                               PropertyIsTrue("_AspNetCoreAppSharedFxIsEnabled") ||
                               PropertyIsTrue("UsingMicrosoftNETSdkWeb");

            output.WriteDebugLine($"IsAspNet={project.IsAspNet}");

            output.WriteDebugLine($"Evaluation Took: {sw.Elapsed.TotalMilliseconds}ms");

            // The Microsoft.Build.Locator doesn't handle the loading of other assemblies
            // that are shipped with MSBuild (ex NuGet).
            //
            // This means that the set of assemblies that need special handling depends on the targets
            // that we run :(
            //
            // This is workaround for this limitation based on the targets we need to run
            // to resolve references and versions.
            //
            // See: https://github.com/microsoft/MSBuildLocator/issues/86
            Assembly?ResolveAssembly(AssemblyLoadContext context, AssemblyName assemblyName)
            {
                if (assemblyName.Name is object)
                {
                    var msbuildDirectory = Environment.GetEnvironmentVariable("MSBuildExtensionsPath") !;
                    var assemblyFilePath = Path.Combine(msbuildDirectory, assemblyName.Name + ".dll");
                    if (File.Exists(assemblyFilePath))
                    {
                        return(context.LoadFromAssemblyPath(assemblyFilePath));
                    }
                }

                return(default);
Exemple #26
0
        public static ServiceOutput CreateDeployment(
            OutputContext output,
            ApplicationBuilder application,
            ProjectServiceBuilder project,
            DeploymentManifestInfo deployment)
        {
            var bindings = project.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", project.Name);

            if (deployment.Annotations.Count > 0)
            {
                var annotations = new YamlMappingNode();
                metadata.Add("annotations", annotations);

                foreach (var annotation in deployment.Annotations)
                {
                    annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value)
                    {
                        Style = ScalarStyle.SingleQuoted,
                    });
                }
            }

            var labels = new YamlMappingNode();

            metadata.Add("labels", labels);
            foreach (var label in deployment.Labels)
            {
                labels.Add(label.Key, new YamlScalarNode(label.Value)
                {
                    Style = ScalarStyle.SingleQuoted,
                });
            }

            var spec = new YamlMappingNode();

            root.Add("spec", spec);

            spec.Add("replicas", project.Replicas.ToString());

            var selector = new YamlMappingNode();

            spec.Add("selector", selector);

            var matchLabels = new YamlMappingNode();

            selector.Add("matchLabels", matchLabels);

            // We need the name so we can use it with matchLabels.
            if (!deployment.Labels.TryGetValue("app.kubernetes.io/name", out var matchLabelsLabelValue))
            {
                throw new InvalidOperationException("The label 'app.kubernetes.io/name` is required.");
            }
            matchLabels.Add("app.kubernetes.io/name", matchLabelsLabelValue);

            var template = new YamlMappingNode();

            spec.Add("template", template);

            metadata = new YamlMappingNode();
            template.Add("metadata", metadata);

            if (deployment.Annotations.Count > 0)
            {
                var annotations = new YamlMappingNode();
                metadata.Add("annotations", annotations);

                foreach (var annotation in deployment.Annotations)
                {
                    annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value)
                    {
                        Style = ScalarStyle.SingleQuoted,
                    });
                }
            }

            labels = new YamlMappingNode();
            metadata.Add("labels", labels);
            foreach (var label in deployment.Labels)
            {
                labels.Add(label.Key, new YamlScalarNode(label.Value)
                {
                    Style = ScalarStyle.SingleQuoted,
                });
            }

            spec = new YamlMappingNode();
            template.Add("spec", spec);

            var containers = new YamlSequenceNode();

            spec.Add("containers", containers);

            var images = project.Outputs.OfType <DockerImageOutput>();

            foreach (var image in images)
            {
                var container = new YamlMappingNode();
                containers.Add(container);
                container.Add("name", project.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 (project.EnvironmentVariables.Count > 0 ||

                    // We generate ASPNETCORE_URLS if there are bindings for http
                    project.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.Any()))
                {
                    var env = new YamlSequenceNode();
                    container.Add("env", env);

                    foreach (var kvp in project.EnvironmentVariables)
                    {
                        env.Add(new YamlMappingNode()
                        {
                            { "name", kvp.Name },
                            { "value", new YamlScalarNode(kvp.Value)
                              {
                                  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,
                                  } },
                            });
                        }

                        foreach (var binding in bindings.Bindings.OfType <SecretInputBinding>())
                        {
                            //- name: SECRET_USERNAME
                            //  valueFrom:
                            //    secretKeyRef:
                            //      name: mysecret
                            //      key: username

                            if (binding is SecretConnectionStringInputBinding connectionStringBinding)
                            {
                                env.Add(new YamlMappingNode()
                                {
                                    { "name", connectionStringBinding.KeyName },
                                    { "valueFrom", new YamlMappingNode()
                                      {
                                          { "secretKeyRef", new YamlMappingNode()
                                            {
                                                { "name", new YamlScalarNode(binding.Name)
                                                    {
                                                        Style = ScalarStyle.SingleQuoted
                                                    } },
                                                { "key", new YamlScalarNode("connectionString")
                                                    {
                                                        Style = ScalarStyle.SingleQuoted
                                                    } },
                                            } },
                                      } },
                                });
                            }
                            else if (binding is SecretUrlInputBinding urlBinding)
                            {
                                env.Add(new YamlMappingNode()
                                {
                                    { "name", $"{urlBinding.KeyNameBase}__PROTOCOL" },
                                    { "valueFrom", new YamlMappingNode()
                                      {
                                          { "secretKeyRef", new YamlMappingNode()
                                            {
                                                { "name", new YamlScalarNode(binding.Name)
                                                    {
                                                        Style = ScalarStyle.SingleQuoted
                                                    } },
                                                { "key", new YamlScalarNode("protocol")
                                                    {
                                                        Style = ScalarStyle.SingleQuoted
                                                    } },
                                            } },
                                      } },
                                });

                                env.Add(new YamlMappingNode()
                                {
                                    { "name", $"{urlBinding.KeyNameBase}__HOST" },
                                    { "valueFrom", new YamlMappingNode()
                                      {
                                          { "secretKeyRef", new YamlMappingNode()
                                            {
                                                { "name", new YamlScalarNode(binding.Name)
                                                    {
                                                        Style = ScalarStyle.SingleQuoted
                                                    } },
                                                { "key", new YamlScalarNode("host")
                                                    {
                                                        Style = ScalarStyle.SingleQuoted
                                                    } },
                                            } },
                                      } },
                                });

                                env.Add(new YamlMappingNode()
                                {
                                    { "name", $"{urlBinding.KeyNameBase}__PORT" },
                                    { "valueFrom", new YamlMappingNode()
                                      {
                                          { "secretKeyRef", new YamlMappingNode()
                                            {
                                                { "name", new YamlScalarNode(binding.Name)
                                                    {
                                                        Style = ScalarStyle.SingleQuoted
                                                    } },
                                                { "key", new YamlScalarNode("port")
                                                    {
                                                        Style = ScalarStyle.SingleQuoted
                                                    } },
                                            } },
                                      } },
                                });
                            }
                        }
                    }
                }

                if (project.Bindings.Count > 0)
                {
                    var ports = new YamlSequenceNode();
                    container.Add("ports", ports);

                    foreach (var binding in project.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());
                        }
                    }
                }
            }

            return(new KubernetesDeploymentOutput(project.Name, new YamlDocument(root)));
        }
Exemple #27
0
        private static async Task ExecuteDeployAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive, bool force)
        {
            if (!await KubectlDetector.Instance.IsKubectlInstalled.Value)
            {
                throw new CommandException($"Cannot apply manifests because kubectl is not installed.");
            }

            if (!await KubectlDetector.Instance.IsKubectlConnectedToCluster.Value)
            {
                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);
        }