예제 #1
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingress)
        {
            // This code assumes that in the future we might support other ingress types besides nginx.
            //
            // Right now we only know some hardcoded details about ingress-nginx that we use for both
            // validation and generation of manifests.
            //
            // For instance we don't support k8s 1.18.X IngressClass resources because that version
            // isn't broadly available yet.
            var ingressClass = "nginx";

            if (!IngressClasses.Add(ingressClass))
            {
                output.WriteDebugLine($"Already validated ingress class '{ingressClass}'.");
                return;
            }

            if (await KubectlDetector.GetKubernetesServerVersion(output) == null)
            {
                throw new CommandException($"Cannot validate ingress because kubectl is not installed.");
            }

            if (!await KubectlDetector.IsKubectlConnectedToClusterAsync(output))
            {
                throw new CommandException($"Cannot validate ingress because kubectl is not connected to a cluster.");
            }

            output.WriteDebugLine($"Validating ingress class '{ingressClass}'.");
            var config = KubernetesClientConfiguration.BuildDefaultConfig();

            // If namespace is null, set it to default
            config.Namespace ??= "default";

            var kubernetes = new Kubernetes(config);

            // Looking for a deployment using a standard label.
            // Note: using a deployment instead of a service - minikube doesn't create a service for the controller.

            try
            {
                var result = await kubernetes.ListDeploymentForAllNamespacesWithHttpMessagesAsync(
                    labelSelector : "app.kubernetes.io/name in (ingress-nginx, nginx-ingress-controller)");

                if (result.Body.Items.Count > 0)
                {
                    foreach (var service in result.Body.Items)
                    {
                        output.WriteInfoLine($"Found existing ingress controller '{service.Metadata.Name}' in namespace '{service.Metadata.NamespaceProperty}'.");
                    }

                    return;
                }
            }
            catch (HttpOperationException ex) when(ex.Response.StatusCode == HttpStatusCode.NotFound)
            {
                // The kubernetes client uses exceptions for 404s.
            }
            catch (Exception ex)
            {
                output.WriteDebugLine("Failed to query secret.");
                output.WriteDebugLine(ex.ToString());
                throw new CommandException("Unable connect to kubernetes.", ex);
            }

            if (Force)
            {
                output.WriteDebugLine("Skipping because force was specified.");
                return;
            }

            if (!Interactive)
            {
                throw new CommandException(
                          $"An ingress was specified for the application, but the 'ingress-nginx' controller could not be found. " +
                          $"Rerun the command with --interactive to deploy a controller for development, or with --force to skip validation. Alternatively " +
                          $"see our documentation on ingress: https://aka.ms/tye/ingress");
            }

            output.WriteAlwaysLine(
                "Tye can deploy the ingress-nginx controller for you. This will be a basic deployment suitable for " +
                "experimentation and development. Your production needs, or requirements may differ depending on your Kubernetes distribution. " +
                "See: https://aka.ms/tye/ingress for documentation.");
            if (!output.Confirm($"Deploy ingress-nginx"))
            {
                // user skipped deployment of ingress, continue with deployment.
                return;
            }

            // We want to be able to detect minikube because the process for enabling nginx-ingress is different there,
            // it's shipped as an addon.
            //
            // see: https://github.com/telepresenceio/telepresence/blob/4364fd83d5926bef46babd704e7bd6c82a75dbd6/telepresence/startup.py#L220
            if (config.CurrentContext == "minikube")
            {
                output.WriteDebugLine($"Running 'minikube addons enable ingress'");
                output.WriteCommandLine("minikube", "addon enable ingress");
                var capture  = output.Capture();
                var exitCode = await ProcessUtil.ExecuteAsync(
                    $"minikube",
                    $"addons enable ingress",
                    System.Environment.CurrentDirectory,
                    stdOut : capture.StdOut,
                    stdErr : capture.StdErr);

                output.WriteDebugLine($"Done running 'minikube addons enable ingress' exit code: {exitCode}");
                if (exitCode != 0)
                {
                    throw new CommandException("'minikube addons enable ingress' failed.");
                }

                output.WriteInfoLine($"Deployed ingress-nginx.");
            }
            else
            {
                // If we get here then we should deploy the ingress controller.

                // The first time we apply the ingress controller, the validating webhook will not have started.
                // This causes an error to be returned from the process. As this always happens, we are going to
                // not check the error returned and assume the kubectl command worked. This is double checked in
                // the future as well when we try to create the ingress resource.

                output.WriteDebugLine($"Running 'kubectl apply'");
                output.WriteCommandLine("kubectl", $"apply -f \"https://aka.ms/tye/ingress/deploy\"");
                var capture  = output.Capture();
                var exitCode = await ProcessUtil.ExecuteAsync(
                    $"kubectl",
                    $"apply -f \"https://aka.ms/tye/ingress/deploy\"",
                    System.Environment.CurrentDirectory);

                output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}");

                output.WriteInfoLine($"Waiting for ingress-nginx controller to start.");

                // We need to then wait for the webhooks that are created by ingress-nginx to start. Deploying an ingress immediately
                // after creating the controller will fail if the webhook isn't ready.
                //
                // Internal error occurred: failed calling webhook "validate.nginx.ingress.kubernetes.io":
                // Post https://ingress-nginx-controller-admission.ingress-nginx.svc:443/networking.k8s.io/v1/ingresses?timeout=30s:
                // dial tcp 10.0.31.130:443: connect: connection refused
                //
                // Unfortunately this is the likely case for us.

                try
                {
                    output.WriteDebugLine("Watching for ingress-nginx controller readiness...");
                    var response = await kubernetes.ListNamespacedPodWithHttpMessagesAsync(
                        namespaceParameter : "ingress-nginx",
                        labelSelector : "app.kubernetes.io/component=controller,app.kubernetes.io/name=ingress-nginx",
                        watch : true);

                    var tcs = new TaskCompletionSource <object?>();
                    using var watcher = response.Watch <V1Pod, V1PodList>(
                              onEvent: (@event, pod) =>
                    {
                        // Wait for the readiness-check to pass.
                        if (pod.Status.Conditions.All(c => string.Equals(c.Status, bool.TrueString, StringComparison.OrdinalIgnoreCase)))
                        {
                            tcs.TrySetResult(null);     // Success!
                            output.WriteDebugLine($"Pod '{pod.Metadata.Name}' is ready.");
                        }
                    },
                              onError: ex =>
                    {
                        tcs.TrySetException(ex);
                        output.WriteDebugLine("Watch operation failed.");
                    },
                              onClosed: () =>
                    {
                        // YOLO?
                        tcs.TrySetResult(null);
                        output.WriteDebugLine("Watch operation completed.");
                    });

                    await tcs.Task;
                }
                catch (Exception ex)
                {
                    output.WriteDebugLine("Failed to ingress-nginx pods.");
                    output.WriteDebugLine(ex.ToString());
                    throw new CommandException("Failed to query ingress-nginx pods.", ex);
                }

                output.WriteInfoLine($"Deployed ingress-nginx.");
            }
        }
예제 #2
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 !);

            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 DotnetProjectServiceBuilder(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;

                        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,
                        };

                        await ProjectReader.ReadProjectDetailsAsync(output, project);

                        // 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)
                        };
                        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 = 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"
                        });
                    }
예제 #3
0
        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 !, new ContainerEngine(rootConfig.ContainerEngineType), rootConfig.DashboardPort)
            {
                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))
                    {
                        // TODO: Investigate possible null.
                        var project = new DotnetProjectServiceBuilder(configService.Name !, new FileInfo(configService.ProjectFullPath !), ServiceSource.Configuration);
                        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 !, ServiceSource.Configuration)
                        {
                            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 !, ServiceSource.Configuration)
                        {
                            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, ServiceSource.Configuration)
                        {
                            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(".tye", "deps");
                        if (!Directory.Exists(path))
                        {
                            Directory.CreateDirectory(path);
                        }

                        var clonePath = Path.Combine(rootConfig.Source.DirectoryName !, 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 : rootConfig.Source.DirectoryName, 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,
                            ServiceSource.Configuration)
                        {
                            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, ServiceSource.Configuration);
                        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"
                        });
                    }
예제 #4
0
        public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source, 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 !);

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

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

                using (var directory = TempDirectory.Create())
                {
                    var projectPath = Path.Combine(directory.DirectoryPath, Path.GetRandomFileName() + ".proj");

                    var sb = new StringBuilder();
                    sb.AppendLine("<Project>");
                    sb.AppendLine("    <ItemGroup>");

                    foreach (var project in projectServices)
                    {
                        var expandedProject = Environment.ExpandEnvironmentVariables(project.Project !);
                        project.ProjectFullPath = Path.Combine(config.Source.DirectoryName !, expandedProject);

                        if (!File.Exists(project.ProjectFullPath))
                        {
                            throw new CommandException($"Failed to locate project: '{project.ProjectFullPath}'.");
                        }

                        sb.AppendLine($"        <MicrosoftTye_ProjectServices " +
                                      $"Include=\"{project.ProjectFullPath}\" " +
                                      $"Name=\"{project.Name}\" " +
                                      $"BuildProperties=\"{(project.BuildProperties.Any() ? project.BuildProperties.Select(kvp => $"{kvp.Name}={kvp.Value}").Aggregate((a, b) => a + ";" + b) : string.Empty)}\" />");
                    }
                    sb.AppendLine(@"    </ItemGroup>");

                    sb.AppendLine($@"    <Target Name=""MicrosoftTye_EvaluateProjects"">");
                    sb.AppendLine($@"        <MsBuild Projects=""@(MicrosoftTye_ProjectServices)"" "
                                  + $@"Properties=""%(BuildProperties);"
                                  + $@"MicrosoftTye_ProjectName=%(Name)"" "
                                  + $@"Targets=""MicrosoftTye_GetProjectMetadata"" BuildInParallel=""true"" />");

                    sb.AppendLine("    </Target>");
                    sb.AppendLine("</Project>");
                    File.WriteAllText(projectPath, sb.ToString());

                    output.WriteDebugLine("Restoring and evaluating projects");

                    var msbuildEvaluationResult = await ProcessUtil.RunAsync(
                        "dotnet",
                        $"build " +
                        $"\"{projectPath}\" " +
                        $"/p:CustomAfterMicrosoftCommonTargets={Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ProjectEvaluation.targets")} " +
                        $"/nologo",
                        throwOnError : false,
                        workingDirectory : directory.DirectoryPath);

                    // 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.
                    if (msbuildEvaluationResult.ExitCode != 0)
                    {
                        output.WriteDebugLine($"Evaluating project failed with exit code {msbuildEvaluationResult.ExitCode}:" +
                                              $"{Environment.NewLine}Ouptut: {msbuildEvaluationResult.StandardOutput}" +
                                              $"{Environment.NewLine}Error: {msbuildEvaluationResult.StandardError}");
                    }

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

                    foreach (var line in msbuildEvaluationOutput)
                    {
                        if (line.Trim().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}");
                        }
                    }

                    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 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++;
                        }
                    }
                }
            }
        }