Exemple #1
0
        public static Task ReadProjectDetailsAsync(OutputContext output, ProjectServiceBuilder project)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

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

            EnsureMSBuildRegistered(output, project.ProjectFile);

            EvaluateProject(output, project);

            if (!SemVersion.TryParse(project.Version, out var version))
            {
                output.WriteInfoLine($"No version or invalid version '{project.Version}' found, using default.");
                version         = new SemVersion(0, 1, 0);
                project.Version = version.ToString();
            }

            return(Task.CompletedTask);
        }
        public static void ApplyContainerDefaults(ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container)
        {
            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));
            }

            if (container.BaseImageName == null &&
                project.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App"))
            {
                container.BaseImageName = "mcr.microsoft.com/dotnet/core/aspnet";
            }
            else if (container.BaseImageName == null)
            {
                container.BaseImageName = "mcr.microsoft.com/dotnet/core/runtime";
            }

            if (container.BaseImageTag == null &&
                project.TargetFramework == "netcoreapp3.1")
            {
                container.BaseImageTag = "3.1";
            }
            else if (container.BaseImageTag == null &&
                     project.TargetFramework == "netcoreapp3.0")
            {
                container.BaseImageTag = "3.0";
            }

            if (container.BaseImageTag == null)
            {
                throw new CommandException($"Unsupported TFM {project.TargetFramework}.");
            }

            container.BuildImageName ??= "mcr.microsoft.com/dotnet/core/sdk";
            container.BuildImageTag ??= "3.1";

            if (container.ImageName == null && application.Registry?.Hostname == null)
            {
                container.ImageName ??= project.Name.ToLowerInvariant();
            }
            else if (container.ImageName == null && application.Registry?.Hostname != null)
            {
                container.ImageName ??= $"{application.Registry?.Hostname}/{project.Name.ToLowerInvariant()}";
            }

            container.ImageTag ??= project.Version?.Replace("+", "-") ?? "latest";
        }
Exemple #3
0
        public static SidecarBuilder GetOrAddSidecar(ProjectServiceBuilder project)
        {
            // Bring your rain boots.
            project.RelocateDiagnosticsDomainSockets = true;

            var sidecar = project.Sidecars.FirstOrDefault(s => s.Name == "tye-diag-agent");

            if (sidecar is object)
            {
                return(sidecar);
            }

            sidecar = new SidecarBuilder("tye-diag-agent", "rynowak/tye-diag-agent", "0.1")
            {
                Args =
                {
                    "--kubernetes=true",
                    $"--service={project.Name}",
                    $"--assemblyName={project.AssemblyName}",
                },
            };
            project.Sidecars.Add(sidecar);
            return(sidecar);
        }
Exemple #4
0
        public static async Task BuildHelmChartAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart)
        {
            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));
            }

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

            var projectDirectory    = project.ProjectFile.DirectoryName;
            var outputDirectoryPath = Path.Combine(projectDirectory, "bin");

            using var tempDirectory = TempDirectory.Create();

            HelmChartGenerator.ApplyHelmChartDefaults(application, project, container, chart);

            var chartRoot = Path.Combine(projectDirectory, "charts");
            var chartPath = Path.Combine(chartRoot, chart.ChartName);

            output.WriteDebugLine($"Looking for existing chart in '{chartPath}'.");
            if (Directory.Exists(chartPath))
            {
                output.WriteDebugLine($"Found existing chart in '{chartPath}'.");
            }
            else
            {
                chartRoot = tempDirectory.DirectoryPath;
                chartPath = Path.Combine(chartRoot, chart.ChartName);
                output.WriteDebugLine($"Generating chart in '{chartPath}'.");
                await HelmChartGenerator.GenerateAsync(output, application, project, container, chart, new DirectoryInfo(tempDirectory.DirectoryPath));
            }

            output.WriteDebugLine("Running 'helm package'.");
            output.WriteCommandLine("helm", $"package -d \"{outputDirectoryPath}\" --version {project.Version.Replace('+', '-')} --app-version {project.Version.Replace('+', '-')}");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                "helm",
                $"package . -d \"{outputDirectoryPath}\" --version {project.Version.Replace('+', '-')} --app-version {project.Version.Replace('+', '-')}",
                workingDir : chartPath,
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Running 'helm package' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("Running 'helm package' failed.");
            }

            output.WriteInfoLine($"Created Helm Chart: {Path.Combine(outputDirectoryPath, chart.ChartName + "-" + project.Version.Replace('+', '-') + ".tgz")}");
        }
Exemple #5
0
        private static void EvaluateProject(OutputContext output, ProjectServiceBuilder project)
        {
            var sw = Stopwatch.StartNew();

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

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

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

            try
            {
                AssemblyLoadContext.Default.Resolving += ResolveAssembly;
                var result = projectInstance.Build(
                    targets: new[] { "Restore", "ResolveReferences", "ResolvePackageDependenciesDesignTime", "PrepareResources", "GetAssemblyAttributes", },
                    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 both InformationalVersion and Version is more resilient in the face of build failures.
            var version = 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>();

            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, project.IntermediateOutputPath);
            project.TargetPath             = Path.Combine(project.ProjectFile.DirectoryName, project.TargetPath);
            project.PublishDir             = Path.Combine(project.ProjectFile.DirectoryName, project.PublishDir);

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

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

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

            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 && assemblyName.Name.StartsWith("NuGet."))
                {
                    var msbuildDirectory = Environment.GetEnvironmentVariable("MSBuildExtensionsPath") !;
                    var assemblyFilePath = Path.Combine(msbuildDirectory, assemblyName.Name + ".dll");
                    if (File.Exists(assemblyFilePath))
                    {
                        return(context.LoadFromAssemblyPath(assemblyFilePath));
                    }
                }

                return(default);
Exemple #6
0
            protected bool SkipWithoutProject(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out ProjectServiceBuilder project)
            {
                if (output is null)
                {
                    throw new ArgumentNullException(nameof(output));
                }

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

                if (service is ProjectServiceBuilder p)
                {
                    project = p;
                    return(false);
                }

                output.WriteInfoLine($"Service '{service.Name}' does not have a project associated. Skipping.");
                project = default !;
Exemple #7
0
        public static KubernetesDeploymentOutput 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 (!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);

            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 #8
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)
                {
                    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.Port.Value.ToString());
                }
            }

            return(new KubernetesServiceOutput(project.Name, new YamlDocument(root)));
        }
        public static OamComponentOutput CreateOamComponent(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project)
        {
            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));
            }

            var root = new YamlMappingNode();

            root.Add("kind", "ComponentSchematic");
            root.Add("apiVersion", "core.oam.dev/v1alpha1");

            var metadata = new YamlMappingNode();

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

            var spec = new YamlMappingNode();

            root.Add("spec", spec);
            spec.Add("workloadType", "core.oam.dev/v1alpha1.Server");

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

            return(new OamComponentOutput(project.Name, new YamlDocument(root)));
        }
        public static ServiceOutput CreateDeployment(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project)
        {
            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));
            }

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

            var labels = new YamlMappingNode();

            metadata.Add("labels", labels);
            labels.Add("app.kubernetes.io/name", project.Name);
            labels.Add("app.kubernetes.io/part-of", application.Name);

            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);
            matchLabels.Add("app.kubernetes.io/name", project.Name);

            var template = new YamlMappingNode();

            spec.Add("template", template);

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

            labels = new YamlMappingNode();
            metadata.Add("labels", labels);
            labels.Add("app.kubernetes.io/name", project.Name);
            labels.Add("app.kubernetes.io/part-of", application.Name);

            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.OfType <EnvironmentVariableInputBinding>().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,
                                  } },
                            });
                        }
                    }
                }

                if (bindings is object && bindings.Bindings.OfType <SecretInputBinding>().Any())
                {
                    var volumeMounts = new YamlSequenceNode();
                    container.Add("volumeMounts", volumeMounts);

                    foreach (var binding in bindings.Bindings.OfType <SecretInputBinding>())
                    {
                        var volumeMount = new YamlMappingNode();
                        volumeMounts.Add(volumeMount);

                        volumeMount.Add("name", $"{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}");
                        volumeMount.Add("mountPath", $"/var/tye/bindings/{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}");
                        volumeMount.Add("readOnly", "true");
                    }
                }

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

            if (bindings.Bindings.OfType <SecretInputBinding>().Any())
            {
                var volumes = new YamlSequenceNode();
                spec.Add("volumes", volumes);

                foreach (var binding in bindings.Bindings.OfType <SecretInputBinding>())
                {
                    var volume = new YamlMappingNode();
                    volumes.Add(volume);
                    volume.Add("name", $"{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}");

                    var secret = new YamlMappingNode();
                    volume.Add("secret", secret);
                    secret.Add("secretName", binding.Name !);

                    var items = new YamlSequenceNode();
                    secret.Add("items", items);

                    var item = new YamlMappingNode();
                    items.Add(item);
                    item.Add("key", "connectionstring");
                    item.Add("path", binding.Filename);
                }
            }

            return(new KubernetesDeploymentOutput(project.Name, new YamlDocument(root)));
        }
Exemple #11
0
        private static void EvaluateProject(OutputContext output, ProjectServiceBuilder 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>();

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

            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 #12
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


                var volumeMounts = new YamlSequenceNode();
                var localMounts  = new YamlSequenceNode();

                if (project.Volumes.Count > 0)
                {
                    container.Add("volumeMounts", volumeMounts);
                    spec.Add("volumes", localMounts);

                    foreach (var vol in project.Volumes)
                    {
                        var volumeMount = new YamlMappingNode();
                        volumeMounts.Add(volumeMount);
                        volumeMount.Add("name", vol.Name);
                        volumeMount.Add("mountPath", vol.Target);

                        var localMount = new YamlMappingNode();
                        var hostPath   = new YamlMappingNode();
                        var path       = new YamlMappingNode();

                        localMounts.Add(localMount);
                        localMount.Add("name", vol.Name);
                        localMount.Add("hostPath", hostPath);
                        hostPath.Add("path", vol.Source);
                    }
                }

                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
                        if (!container.Children.ContainsKey("volumeMounts"))
                        {
                            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)));
        }
        public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source)
        {
            if (source is null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            var config = ConfigFactory.FromFile(source);

            ValidateConfigApplication(config);

            var builder = new ApplicationBuilder(source, config.Name ?? source.Directory.Name.ToLowerInvariant());

            if (!string.IsNullOrEmpty(config.Registry))
            {
                builder.Registry = new ContainerRegistry(config.Registry);
            }

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

                    project.Build    = configService.Build ?? true;
                    project.Args     = configService.Args;
                    project.Replicas = configService.Replicas ?? 1;

                    await ProjectReader.ReadProjectDetailsAsync(output, project);

                    // We don't apply more container defaults here because we might need
                    // to prompty for the registry name.
                    project.ContainerInfo = new ContainerInfo()
                    {
                        UseMultiphaseDockerfile = false,
                    };
                }
                else if (!string.IsNullOrEmpty(configService.Image))
                {
                    var container = new ContainerServiceBuilder(configService.Name, configService.Image)
                    {
                        Args     = configService.Args,
                        Replicas = configService.Replicas ?? 1
                    };
                    service = container;
                }
                else if (!string.IsNullOrEmpty(configService.Executable))
                {
                    var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable);
                    var workingDirectory   = "";

                    // Special handling of .dlls as executables (it will be executed as dotnet {dll})
                    if (Path.GetExtension(expandedExecutable) == ".dll")
                    {
                        expandedExecutable = Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, expandedExecutable));
                        workingDirectory   = Path.GetDirectoryName(expandedExecutable) !;
                    }

                    var executable = new ExecutableServiceBuilder(configService.Name, expandedExecutable)
                    {
                        Args             = configService.Args,
                        WorkingDirectory = configService.WorkingDirectory != null?
                                           Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) :
                                               workingDirectory,
                                               Replicas = configService.Replicas ?? 1
                    };
                    service = executable;
                }
                else if (configService.External)
                {
                    var external = new ExternalServiceBuilder(configService.Name);
                    service = external;
                }
                else
                {
                    throw new CommandException("Unable to determine service type.");
                }

                builder.Services.Add(service);

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

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

                    service.Bindings.Add(binding);
                }

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

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

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

                builder.Ingress.Add(ingress);

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

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

            return(builder);
        }
Exemple #14
0
        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));
            }

            using var tempFile = TempFile.Create();

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

            if (File.Exists(dockerFilePath))
            {
                output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'.");
            }
            else
            {
                await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, tempFile.FilePath);

                dockerFilePath = tempFile.FilePath;
            }

            // 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.
            string contextDirectory;

            if (container.UseMultiphaseDockerfile ?? true)
            {
                contextDirectory = ".";
            }
            else
            {
                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;
            }

            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 !));
        }
        public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source)
        {
            if (source is null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            var config = ConfigFactory.FromFile(source);

            ValidateConfigApplication(config);

            var builder = new ApplicationBuilder(source, config.Name ?? source.Directory.Name.ToLowerInvariant());

            if (!string.IsNullOrEmpty(config.Registry))
            {
                builder.Registry = new ContainerRegistry(config.Registry);
            }

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

                    project.Build    = configService.Build ?? true;
                    project.Args     = configService.Args;
                    project.Replicas = configService.Replicas ?? 1;

                    await ProjectReader.ReadProjectDetailsAsync(output, project);

                    // We don't apply more container defaults here because we might need
                    // to prompty for the registry name.
                    project.ContainerInfo = new ContainerInfo()
                    {
                        UseMultiphaseDockerfile = false,
                    };
                }
                else if (!string.IsNullOrEmpty(configService.Image))
                {
                    var container = new ContainerServiceBuilder(configService.Name, configService.Image)
                    {
                        Args     = configService.Args,
                        Replicas = configService.Replicas ?? 1
                    };
                    service = container;
                }
                else if (!string.IsNullOrEmpty(configService.Executable))
                {
                    var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable);
                    var workingDirectory   = "";

                    // Special handling of .dlls as executables (it will be executed as dotnet {dll})
                    if (Path.GetExtension(expandedExecutable) == ".dll")
                    {
                        expandedExecutable = Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, expandedExecutable));
                        workingDirectory   = Path.GetDirectoryName(expandedExecutable) !;
                    }

                    var executable = new ExecutableServiceBuilder(configService.Name, expandedExecutable)
                    {
                        Args             = configService.Args,
                        WorkingDirectory = configService.WorkingDirectory != null?
                                           Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) :
                                               workingDirectory,
                                               Replicas = configService.Replicas ?? 1
                    };
                    service = executable;
                }
                else if (configService.External)
                {
                    var external = new ExternalServiceBuilder(configService.Name);
                    service = external;
                }
                else
                {
                    throw new CommandException("Unable to determine service type.");
                }

                builder.Services.Add(service);

                // 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.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App"))
                {
                    // HTTP is the default binding
                    service.Bindings.Add(new BindingBuilder()
                    {
                        AutoAssignPort = true,
                        Protocol       = "http"
                    });

                    service.Bindings.Add(new BindingBuilder()
                    {
                        Name           = "https",
                        AutoAssignPort = true,
                        Protocol       = "https"
                    });
                }
Exemple #16
0
        public static async Task GenerateAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory)
        {
            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));
            }

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

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

            ApplyHelmChartDefaults(application, project, container, chart);

            // The directory with the charts needs to be the same as the chart name
            var chartDirectoryPath = Path.Combine(outputDirectory.FullName, chart.ChartName);

            Directory.CreateDirectory(chartDirectoryPath);

            var templateDirectoryPath = Path.Combine(
                Path.GetDirectoryName(typeof(HelmChartGenerator).Assembly.Location) !,
                "Templates",
                "Helm");

            DirectoryCopy.Copy(templateDirectoryPath, chartDirectoryPath);

            // Write Chart.yaml
            //
            // apiVersion: v1
            // name: <appname>
            // version: <version>
            // appVersion: <version>
            await File.WriteAllLinesAsync(Path.Combine(chartDirectoryPath, "Chart.yaml"), new[]
            {
                $"apiVersion: v1",
                $"name: {chart.ChartName}",
                $"# helm requires the version and appVersion to specified in Chart.yaml",
                $"# tye will override these values when packaging the chart",
                $"version: {project.Version.Replace('+', '-')}",
                $"appVersion: {project.Version.Replace('+', '-')}"
            });

            // Write values.yaml
            //
            // image:
            //   repository: rynowak.azurecr.io/rochambot/gamemaster
            await File.WriteAllLinesAsync(Path.Combine(chartDirectoryPath, "values.yaml"), new[]
            {
                $"image:",
                $"  repository: {container.ImageName}",
            });
        }
Exemple #17
0
        public static async Task WriteDockerfileAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, string filePath)
        {
            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));
            }

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

            await using var stream = File.OpenWrite(filePath);
            await using var writer = new StreamWriter(stream, encoding: Encoding.UTF8, bufferSize: -1, leaveOpen: true);

            var entryPoint = Path.GetFileNameWithoutExtension(project.ProjectFile.Name);

            output.WriteDebugLine($"Writing Dockerfile to '{filePath}'.");
            if (container.UseMultiphaseDockerfile ?? true)
            {
                await WriteMultiphaseDockerfileAsync(writer, entryPoint, container);
            }
            else
            {
                await WriteLocalPublishDockerfileAsync(writer, entryPoint, container);
            }
            output.WriteDebugLine("Done writing Dockerfile.");
        }
        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();
            }
        }