Example #1
0
        public static async Task ExecuteAsync(OutputContext output, string imageName, string imageTag)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

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

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

            output.WriteDebugLine("Running 'docker push'.");
            output.WriteCommandLine("docker", $"push {imageName}:{imageTag}");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"docker",
                $"push {imageName}:{imageTag}",
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'docker push' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("'docker push' failed.");
            }
        }
        private static async Task DeployApplicationManifestAsync(OutputContext output, ApplicationBuilder application, string applicationName, string environment)
        {
            using var step = output.BeginStep("Deploying Application Manifests...");

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

            {
                using var stream = File.OpenWrite(tempFile.FilePath);
                using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);

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

            output.WriteDebugLine("Running 'kubectl apply'.");
            output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.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 '{applicationName}'.");

            step.MarkComplete();
        }
Example #3
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
        {
            if (SkipWithoutDotnetProject(output, service, out var project))
            {
                return;
            }

            if (SkipWithoutContainerInfo(output, service, out var container))
            {
                return;
            }

            if (container.UseMultiphaseDockerfile != false)
            {
                return;
            }

            // NOTE: we're intentionally not cleaning up here. It's the responsibility of whomever consumes
            // the publish output to do cleanup.
            var outputDirectory = TempDirectory.Create();

            output.WriteDebugLine("Running 'dotnet publish'.");
            var dotnetPublishArguments = project.BuildProperties.TryGetValue("TargetFramework", out var framework)
                ? $"publish \"{project.ProjectFile.FullName}\" -c Release -f {framework} -o \"{outputDirectory.DirectoryPath}\" /nologo"
                : $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory.DirectoryPath}\" /nologo";

            output.WriteCommandLine("dotnet", dotnetPublishArguments);

            var publishResult = await ProcessUtil.RunAsync(
                $"dotnet",
                dotnetPublishArguments,
                project.ProjectFile.DirectoryName,
                throwOnError : false);

            output.WriteDebugLine($"Done running 'dotnet publish' exit code: {publishResult.ExitCode}");
            if (publishResult.ExitCode != 0)
            {
                outputDirectory.Dispose();
                output.WriteInfoLine($"'dotnet publish' failed. Error:");

                foreach (var line in publishResult.StandardOutput.Split(Environment.NewLine))
                {
                    output.WriteInfoLine(line);
                }

                foreach (var line in publishResult.StandardError.Split(Environment.NewLine))
                {
                    output.WriteInfoLine(line);
                }

                throw new CommandException("'dotnet publish' failed.");
            }

            output.WriteDebugLine($"Created Publish Output: '{outputDirectory.DirectoryPath}'");
            service.Outputs.Add(new ProjectPublishOutput(outputDirectory.DirectoryInfo));
        }
Example #4
0
        private static void EnsureMSBuildRegistered(OutputContext?output, FileInfo projectFile)
        {
            if (!registered)
            {
                lock (@lock)
                {
                    output?.WriteDebugLine("Locating .NET SDK...");

                    // It says VisualStudio - but on .NET Core, it defaults to just DotNetSdk.
                    // https://github.com/microsoft/MSBuildLocator/blob/v1.2.6/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs#L23
                    //
                    // Resolve the SDK from the project directory and fall back to the global SDK.
                    // We're making the assumption that all of the projects want to use the same
                    // SDK version. This library is going load a single version of the SDK's
                    // assemblies into our process, so we can't use support SDKs at once without
                    // getting really tricky.
                    //
                    // The .NET SDK-based discovery uses `dotnet --info` and returns the SDK
                    // in use for the directory.
                    //
                    // https://github.com/microsoft/MSBuildLocator/blob/v1.2.6/src/MSBuildLocator/DotNetSdkLocationHelper.cs#L68
                    var instance = MSBuildLocator
                                   .QueryVisualStudioInstances(new VisualStudioInstanceQueryOptions {
                        WorkingDirectory = projectFile.DirectoryName
                    })
                                   .FirstOrDefault();

                    if (instance == null)
                    {
                        instance = MSBuildLocator
                                   .QueryVisualStudioInstances()
                                   .FirstOrDefault();
                    }

                    if (instance == null)
                    {
                        throw new CommandException($"Failed to resolve dotnet in {projectFile.Directory} or the PATH. Make sure the .NET SDK is installed and is on the PATH.");
                    }

                    output?.WriteDebugLine("Found .NET SDK at: " + instance.MSBuildPath);

                    try
                    {
                        MSBuildLocator.RegisterInstance(instance);
                        output?.WriteDebugLine("Registered .NET SDK.");
                    }
                    finally
                    {
                        registered = true;
                    }
                }
            }
        }
Example #5
0
        public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
        {
            var yaml = service.Outputs.OfType <IYamlManifestOutput>().ToArray();

            if (yaml.Length == 0)
            {
                output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping.");
                return;
            }

            if (!await KubectlDetector.Instance.IsKubectlInstalled.Value)
            {
                throw new CommandException($"Cannot apply manifests for '{service.Service.Name}' because kubectl is not installed.");
            }

            if (!await KubectlDetector.Instance.IsKubectlConnectedToCluster.Value)
            {
                throw new CommandException($"Cannot apply manifests for '{service.Service.Name}' because kubectl is not connected to a cluster.");
            }

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

            {
                using var stream = File.OpenWrite(tempFile.FilePath);
                using var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: -1, leaveOpen: true);
                var yamlStream = new YamlStream(yaml.Select(y => y.Yaml));
                yamlStream.Save(writer, assignAnchors: false);
            }

            // kubectl apply logic is implemented in the client in older versions of k8s. The capability
            // to get the same behavior in the server isn't present in every version that's relevant.
            //
            // https://kubernetes.io/docs/reference/using-api/api-concepts/#server-side-apply
            //
            output.WriteDebugLine("Running 'kubectl apply'.");
            output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.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 service '{service.FriendlyName}'.");
        }
Example #6
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application)
        {
            using var step = output.BeginStep("");

            if (!await KubectlDetector.IsKubectlInstalledAsync(output))
            {
                throw new CommandException($"Cannot apply manifests because kubectl is not installed.");
            }

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

            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 Process.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}'.");
        }
Example #7
0
        public static async Task WriteDockerfileAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, string filePath)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

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

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

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

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

            var entryPoint = Path.GetFileNameWithoutExtension(project.RelativeFilePath);

            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.");
        }
Example #8
0
        public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
        {
            var yaml = service.Outputs.OfType <IYamlManifestOutput>().ToArray();

            if (yaml.Length == 0)
            {
                output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping.");
                return(Task.CompletedTask);
            }

            var outputFilePath = Path.Combine(OutputDirectory.FullName, $"{service.Service.Name}.yaml");

            output.WriteInfoLine($"Writing output to '{outputFilePath}'.");
            if (File.Exists(outputFilePath) && !Force)
            {
                throw new CommandException($"'{service.Service.Name}.yaml' already exists for project. use '--force' to overwrite.");
            }

            File.Delete(outputFilePath);

            using var stream = File.OpenWrite(outputFilePath);
            using var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: -1, leaveOpen: true);
            var yamlStream = new YamlStream(yaml.Select(y => y.Yaml));

            yamlStream.Save(writer, assignAnchors: false);

            return(Task.CompletedTask);
        }
Example #9
0
        public static async Task BuildContainerImageFromDockerFileAsync(OutputContext output, ApplicationBuilder application, DockerFileServiceBuilder containerService, ContainerInfo container)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

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

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

            if (containerService.DockerFile is null)
            {
                throw new ArgumentNullException(nameof(containerService.DockerFile));
            }

            var dockerFileInfo   = new FileInfo(containerService.DockerFile);
            var contextDirectory = containerService.DockerFileContext ?? dockerFileInfo.DirectoryName;
            var dockerFilePath   = Path.Combine(dockerFileInfo.DirectoryName, "Dockerfile");

            output.WriteDebugLine($"Using existing Dockerfile '{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}\"",
                new FileInfo(containerService.DockerFile).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}'");
            containerService.Outputs.Add(new DockerImageOutput(container.ImageName !, container.ImageTag !));
        }
Example #10
0
        private static void EnsureMSBuildRegistered(OutputContext?output, FileInfo projectFile)
        {
            if (!registered)
            {
                lock (@lock)
                {
                    output?.WriteDebugLine("Locating .NET SDK...");

                    // It says VisualStudio - but we'll just use .NET SDK
                    var instances = MSBuildLocator.QueryVisualStudioInstances(new VisualStudioInstanceQueryOptions()
                    {
                        DiscoveryTypes = DiscoveryType.DotNetSdk,

                        // Using the project as the working directory. We're making the assumption that
                        // all of the projects want to use the same SDK version. This library is going
                        // load a single version of the SDK's assemblies into our process, so we can't
                        // use support SDKs at once without getting really tricky.
                        //
                        // The .NET SDK-based discovery uses `dotnet --info` and returns the SDK
                        // in use for the directory.
                        //
                        // https://github.com/microsoft/MSBuildLocator/blob/master/src/MSBuildLocator/MSBuildLocator.cs#L320
                        WorkingDirectory = projectFile.DirectoryName,
                    });

                    var instance = instances.SingleOrDefault();
                    if (instance == null)
                    {
                        throw new CommandException("Failed to find dotnet. Make sure the .NET SDK is installed and on the PATH.");
                    }

                    output?.WriteDebugLine("Found .NET SDK at: " + instance.MSBuildPath);

                    try
                    {
                        MSBuildLocator.RegisterInstance(instance);
                        output?.WriteDebug("Registered .NET SDK.");
                    }
                    finally
                    {
                        registered = true;
                    }
                }
            }
        }
Example #11
0
            protected bool SkipForEnvironment(OutputContext output, ServiceEntry service, string environment)
            {
                if (!service.AppliesToEnvironment(environment))
                {
                    output.WriteDebugLine($"Service '{service.FriendlyName}' is not part of environment '{environment}'. Skipping.");
                    return(true);
                }

                return(false);
            }
Example #12
0
        public static async Task WriteDockerfileAsync(OutputContext output, ApplicationBuilder application, DotnetProjectServiceBuilder 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: new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), bufferSize: -1, leaveOpen: true);

            var entryPoint = project.AssemblyName;

            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.");
        }
Example #13
0
        public static async Task ExecuteAsync(OutputContext output, string registryHostname, string chartFilePath)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

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

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

            // NOTE: this is currently hardcoded to use ACR and uses the azure CLI to do
            // the push operation. Helm 3 has support for pushing to OCI repositories,
            // but it's not documented that it works yet for azure.
            if (!registryHostname.EndsWith(".azurecr.io"))
            {
                throw new CommandException("Currently pushing of helm charts is limited to '*.azurecr.io'.");
            }

            var registryName = registryHostname.Substring(0, registryHostname.Length - ".azurecr.io".Length);

            output.WriteDebugLine("Running 'az acr helm push'.");
            output.WriteCommandLine("az", $"acr helm push --name {registryName} \"{chartFilePath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"az",
                $"acr helm push --name {registryName} \"{chartFilePath}\"",
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'az acr helm push' exit code: {exitCode}");
            if (exitCode != 0)
            {
                throw new CommandException("'az acr helm push' failed.");
            }
        }
Example #14
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
        {
            if (SkipWithoutProject(output, service, out var project))
            {
                return;
            }

            if (SkipWithoutContainerInfo(output, service, out var container))
            {
                return;
            }

            if (container.UseMultiphaseDockerfile != false)
            {
                return;
            }

            // NOTE: we're intentionally not cleaning up here. It's the responsibility of whomever consumes
            // the publish output to do cleanup.
            var outputDirectory = TempDirectory.Create();

            output.WriteDebugLine("Running 'dotnet publish'.");
            output.WriteCommandLine("dotnet", $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory.DirectoryPath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"dotnet",
                $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory.DirectoryPath}\"",
                project.ProjectFile.DirectoryName,
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

            output.WriteDebugLine($"Done running 'dotnet publish' exit code: {exitCode}");
            if (exitCode != 0)
            {
                outputDirectory.Dispose();
                throw new CommandException("'dotnet publish' failed.");
            }

            output.WriteDebugLine($"Created Publish Output: '{outputDirectory.DirectoryPath}'");
            service.Outputs.Add(new ProjectPublishOutput(outputDirectory.DirectoryInfo));
        }
Example #15
0
        public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
        {
            if (SkipWithoutProject(output, service, out var project))
            {
                return;
            }

            if (SkipWithoutContainerInfo(output, service, out var container))
            {
                return;
            }

            if (container.UseMultiphaseDockerfile != false)
            {
                return;
            }

            var projectFilePath = Path.Combine(application.RootDirectory, project.RelativeFilePath);
            var outputDirectory = Path.Combine(Path.GetDirectoryName(projectFilePath) !, "bin", "Release", project.TargetFramework, "publish");

            output.WriteDebugLine("Running 'dotnet publish'.");
            output.WriteCommandLine("dotnet", $"publish \"{projectFilePath}\" -c Release -o \"{outputDirectory}\"");
            var capture  = output.Capture();
            var exitCode = await Process.ExecuteAsync(
                $"dotnet",
                $"publish \"{projectFilePath}\" -c Release -o \"{outputDirectory}\"",
                application.GetProjectDirectory(project),
                stdOut : capture.StdOut,
                stdErr : capture.StdErr);

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

            output.WriteInfoLine($"Created Publish Output: '{outputDirectory}'");
            service.Outputs.Add(new ProjectPublishOutput(new DirectoryInfo(outputDirectory)));
        }
Example #16
0
        private static void EvaluateAzureFunctionProject(OutputContext output, AzureFunctionServiceBuilder project, string metadataFile)
        {
            var sw = Stopwatch.StartNew();

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

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

            project.AzureFunctionsVersion = GetMetadataValueOrNull("AzureFunctionsVersion");

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

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

            string?GetMetadataValueOrNull(string key) => metadata !.TryGetValue(key, out var value) ? value : null;
        }
Example #17
0
        public static Task WriteAsync(OutputContext output, StreamWriter writer, ApplicationBuilder application)
        {
            var yaml = application.Services.SelectMany(s => s.Outputs.OfType <IYamlManifestOutput>()).ToArray();

            if (yaml.Length == 0)
            {
                output.WriteDebugLine($"No yaml manifests found. Skipping.");
                return(Task.CompletedTask);
            }

            var yamlStream = new YamlStream(yaml.Select(y => y.Yaml));

            yamlStream.Save(writer, assignAnchors: false);

            return(Task.CompletedTask);
        }
Example #18
0
 private static void LogIt(OutputContext output)
 {
     output.WriteDebugLine("Loaded: " + typeof(ProjectInstance).Assembly.FullName);
     output.WriteDebugLine("Loaded From: " + typeof(ProjectInstance).Assembly.Location);
 }
Example #19
0
        private static void EvaluateProject(OutputContext output, DotnetProjectServiceBuilder project)
        {
            var sw = Stopwatch.StartNew();

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

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

            ProjectInstance projectInstance;

            Microsoft.Build.Evaluation.Project msbuildProject;

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

            try
            {
                AssemblyLoadContext.Default.Resolving += ResolveAssembly;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                return(default);
Example #20
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 !);

            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)}" +
                                      $"{(string.IsNullOrEmpty(framework) ? string.Empty : $";TargetFramework={framework}")}" +
                                      $"\" />");
                    }
                    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}\" " +
                        // CustomAfterMicrosoftCommonTargets is imported by non-crosstargeting (single TFM) projects
                        $"/p:CustomAfterMicrosoftCommonTargets={Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "ProjectEvaluation.targets")} " +
                        // CustomAfterMicrosoftCommonCrossTargetingTargets is imported by crosstargeting (multi-TFM) projects
                        // This ensures projects properties are evaluated correctly. However, multi-TFM projects must specify
                        // a specific TFM to build/run/publish and will otherwise throw an exception.
                        $"/p:CustomAfterMicrosoftCommonCrossTargetingTargets={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]);

                        if (framework != null && project.TargetFrameworks.Any())
                        {
                            // Only use the TargetFramework for the "--framework" if it's a multi-targeted project and an override is provided
                            project.BuildProperties["TargetFramework"] = framework;
                        }

                        // 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"
                        });
                    }
Example #21
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.");
            }
        }
Example #22
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
        {
            var bindings = service.Outputs.OfType <ComputedBindings>().FirstOrDefault();

            if (bindings is null)
            {
                return;
            }

            foreach (var binding in bindings.Bindings)
            {
                if (binding is SecretInputBinding secretInputBinding)
                {
                    if (!Secrets.Add(secretInputBinding.Name))
                    {
                        output.WriteDebugLine($"Already validated secret '{secretInputBinding.Name}'.");
                        continue;
                    }

                    output.WriteDebugLine($"Validating secret '{secretInputBinding.Name}'.");

                    var config = KubernetesClientConfiguration.BuildDefaultConfig();

                    // Workaround for https://github.com/kubernetes-client/csharp/issues/372
                    var store = await KubernetesClientConfiguration.LoadKubeConfigAsync();

                    var context = store.Contexts.Where(c => c.Name == config.CurrentContext).FirstOrDefault();
                    config.Namespace ??= context?.ContextDetails?.Namespace;

                    var kubernetes = new Kubernetes(config);

                    try
                    {
                        var result = await kubernetes.ReadNamespacedSecretWithHttpMessagesAsync(secretInputBinding.Name, config.Namespace ?? "default");

                        output.WriteInfoLine($"Found existing secret '{secretInputBinding.Name}'.");
                        continue;
                    }
                    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.");
                        continue;
                    }

                    if (!Interactive)
                    {
                        throw new CommandException(
                                  $"The secret '{secretInputBinding.Name}' used for service '{secretInputBinding.Service.Name}' is missing from the deployment environment. " +
                                  $"Rerun the command with --interactive to specify the value interactively, or with --force to skip validation. Alternatively " +
                                  $"use the following command to manually create the secret." + System.Environment.NewLine +
                                  $"kubectl create secret generic {secretInputBinding.Name} --from-literal=connectionstring=<value>");
                    }

                    // If we get here then we should create the secret.
                    var text = output.Prompt($"Enter the connection string to use for service '{secretInputBinding.Service.Name}'", allowEmpty: true);
                    if (string.IsNullOrWhiteSpace(text))
                    {
                        output.WriteAlways($"Skipping creation of secret for '{secretInputBinding.Service.Name}'. This may prevent creation of pods until secrets are created.");
                        output.WriteAlways($"Manually create a secret with:");
                        output.WriteAlways($"kubectl create secret generic {secretInputBinding.Name} --from-literal=connectionstring=<value>");
                        continue;
                    }

                    var secret = new V1Secret(type: "Opaque", stringData: new Dictionary <string, string>()
                    {
                        { "connectionstring", text },
                    });
                    secret.Metadata      = new V1ObjectMeta();
                    secret.Metadata.Name = secretInputBinding.Name;

                    output.WriteDebugLine($"Creating secret '{secret.Metadata.Name}'.");

                    try
                    {
                        await kubernetes.CreateNamespacedSecretWithHttpMessagesAsync(secret, config.Namespace ?? "default");

                        output.WriteInfoLine($"Created secret '{secret.Metadata.Name}'.");
                    }
                    catch (Exception ex)
                    {
                        output.WriteDebugLine("Failed to create secret.");
                        output.WriteDebugLine(ex.ToString());
                        throw new CommandException("Failed to create secret.", ex);
                    }
                }
            }

            var yaml = service.Outputs.OfType <IYamlManifestOutput>().ToArray();

            if (yaml.Length == 0)
            {
                output.WriteDebugLine($"No yaml manifests found for service '{service.Name}'. Skipping.");
                return;
            }

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

            {
                await using var stream = File.OpenWrite(tempFile.FilePath);
                await using var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: -1, leaveOpen: true);
                var yamlStream = new YamlStream(yaml.Select(y => y.Yaml));
                yamlStream.Save(writer, assignAnchors: false);
            }

            // kubectl apply logic is implemented in the client in older versions of k8s. The capability
            // to get the same behavior in the server isn't present in every version that's relevant.
            //
            // https://kubernetes.io/docs/reference/using-api/api-concepts/#server-side-apply
            //
            output.WriteDebugLine("Running 'kubectl apply'.");
            output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
            var capture  = output.Capture();
            var exitCode = await Process.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 service '{service.Name}'.");
        }
Example #23
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")}");
        }
Example #24
0
        private static async Task EvaluateMSBuildAsync(OutputContext output, FileInfo projectFile, Project project)
        {
            try
            {
                output.WriteDebugLine("Installing msbuild targets.");
                TargetInstaller.Install(projectFile.FullName);
                output.WriteDebugLine("Installed msbuild targets.");
            }
            catch (Exception ex)
            {
                throw new CommandException("Failed to install targets.", ex);
            }

            var outputFilePath = Path.GetTempFileName();

            try
            {
                var capture     = output.Capture();
                var programRoot = Path.GetDirectoryName(typeof(Program).Assembly.Location);

                var restore = string.Empty;
                if (!File.Exists(Path.Combine(projectFile.DirectoryName, "obj", "project.assets.json")))
                {
                    restore = "/restore";
                }

                output.WriteDebugLine("Running 'dotnet msbuild'.");
                var msbuildCommand = $"msbuild {restore} /t:EvaluateTyeProjectInfo \"/p:TyeTargetLocation={programRoot}\" \"/p:TyeOutputFilePath={outputFilePath}\"";

                output.WriteCommandLine("dotnet", msbuildCommand);
                var exitCode = await Process.ExecuteAsync(
                    $"dotnet",
                    msbuildCommand,
                    workingDir : projectFile.DirectoryName,
                    stdOut : capture.StdOut,
                    stdErr : capture.StdErr);

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

                var lines = await File.ReadAllLinesAsync(outputFilePath);

                for (var i = 0; i < lines.Length; i++)
                {
                    var line = lines[i];
                    if (line.StartsWith("version="))
                    {
                        project.Version = line.Substring("version=".Length).Trim();
                        output.WriteDebugLine($"Found application version: {line}");
                        continue;
                    }

                    if (line.StartsWith("tfm"))
                    {
                        project.TargetFramework = line.Substring("tfm=".Length).Trim();
                        output.WriteDebugLine($"Found target framework: {line}");
                        continue;
                    }

                    if (line.StartsWith("frameworks="))
                    {
                        var right = line.Substring("frameworks=".Length).Trim();
                        project.Frameworks.AddRange(right.Split(",").Select(s => new Framework(s)));
                        output.WriteDebugLine($"Found shared frameworks: {line}");
                        continue;
                    }
                }
            }
            finally
            {
                File.Delete(outputFilePath);
            }
        }
Example #25
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 !);

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

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

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

                var multiTFMProjects = new List <ConfigService>();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                        // Do k8s by default.
                        dockerFile.ManifestInfo = new KubernetesManifestInfo();
                    }
                    else if (!string.IsNullOrEmpty(configService.Executable))
                    {
                        var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable);
                        var workingDirectory   = "";

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

                        var executable = new ExecutableServiceBuilder(configService.Name !, expandedExecutable)
                        {
                            Args             = configService.Args,
                            WorkingDirectory = configService.WorkingDirectory != null?
                                               Path.GetFullPath(Path.Combine(config.Source.Directory !.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) :
                                                   workingDirectory,
                                                   Replicas = configService.Replicas ?? 1
                        };
                        service = executable;

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

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

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

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

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

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

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

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

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

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

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

                        AddToRootServices(root, dependencies, configService.Name);

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

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

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

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

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

                    AddToRootServices(root, dependencies, service.Name);

                    root.Services.Add(service);

                    // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding
                    if (configService.Bindings.Count == 0 &&
                        service is ProjectServiceBuilder project2 &&
                        project2.IsAspNet)
                    {
                        // HTTP is the default binding
                        service.Bindings.Add(new BindingBuilder()
                        {
                            Protocol = "http"
                        });
                        service.Bindings.Add(new BindingBuilder()
                        {
                            Name = "https", Protocol = "https"
                        });
                    }
Example #26
0
        public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
        {
            var bindings = service.Outputs.OfType <ComputedBindings>().FirstOrDefault();

            if (bindings is null)
            {
                return;
            }

            foreach (var binding in bindings.Bindings)
            {
                if (binding is SecretInputBinding secretInputBinding)
                {
                    if (!Secrets.Add(secretInputBinding.Name))
                    {
                        output.WriteDebugLine($"Already validated secret '{secretInputBinding.Name}'.");
                        continue;
                    }

                    output.WriteDebugLine($"Validating secret '{secretInputBinding.Name}'.");

                    var config = KubernetesClientConfiguration.BuildDefaultConfig();

                    // Workaround for https://github.com/kubernetes-client/csharp/issues/372
                    var store = await KubernetesClientConfiguration.LoadKubeConfigAsync();

                    var context = store.Contexts.Where(c => c.Name == config.CurrentContext).FirstOrDefault();

                    // Use namespace of application, or current context, or 'default'
                    config.Namespace = application.Namespace;
                    config.Namespace ??= context?.ContextDetails?.Namespace ?? "default";

                    var kubernetes = new Kubernetes(config);

                    try
                    {
                        var result = await kubernetes.ReadNamespacedSecretWithHttpMessagesAsync(secretInputBinding.Name, config.Namespace);

                        output.WriteInfoLine($"Found existing secret '{secretInputBinding.Name}'.");
                        continue;
                    }
                    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.");
                        continue;
                    }

                    if (!Interactive && secretInputBinding is SecretConnectionStringInputBinding)
                    {
                        throw new CommandException(
                                  $"The secret '{secretInputBinding.Name}' used for service '{secretInputBinding.Service.Name}' is missing from the deployment environment. " +
                                  $"Rerun the command with --interactive to specify the value interactively, or with --force to skip validation. Alternatively " +
                                  $"use the following command to manually create the secret." + System.Environment.NewLine +
                                  $"kubectl create secret generic {secretInputBinding.Name} --namespace {config.Namespace} --from-literal=connectionstring=<value>");
                    }

                    if (!Interactive && secretInputBinding is SecretUrlInputBinding)
                    {
                        throw new CommandException(
                                  $"The secret '{secretInputBinding.Name}' used for service '{secretInputBinding.Service.Name}' is missing from the deployment environment. " +
                                  $"Rerun the command with --interactive to specify the value interactively, or with --force to skip validation. Alternatively " +
                                  $"use the following command to manually create the secret." + System.Environment.NewLine +
                                  $"kubectl create secret generic {secretInputBinding.Name} --namespace {config.Namespace} --from-literal=protocol=<value> --from-literal=host=<value> --from-literal=port=<value>");
                    }

                    V1Secret secret;
                    if (secretInputBinding is SecretConnectionStringInputBinding)
                    {
                        // If we get here then we should create the secret.
                        var text = output.Prompt($"Enter the connection string to use for service '{secretInputBinding.Service.Name}'", allowEmpty: true);
                        if (string.IsNullOrWhiteSpace(text))
                        {
                            output.WriteAlwaysLine($"Skipping creation of secret for '{secretInputBinding.Service.Name}'. This may prevent creation of pods until secrets are created.");
                            output.WriteAlwaysLine($"Manually create a secret with:");
                            output.WriteAlwaysLine($"kubectl create secret generic {secretInputBinding.Name} --namespace {config.Namespace} --from-literal=connectionstring=<value>");
                            continue;
                        }

                        secret = new V1Secret(type: "Opaque", stringData: new Dictionary <string, string>()
                        {
                            { "connectionstring", text },
                        });
                    }
                    else if (secretInputBinding is SecretUrlInputBinding)
                    {
                        // If we get here then we should create the secret.
                        string text;
                        Uri?   uri = null;
                        while (true)
                        {
                            text = output.Prompt($"Enter the URI to use for service '{secretInputBinding.Service.Name}'", allowEmpty: true);
                            if (string.IsNullOrEmpty(text))
                            {
                                break; // skip
                            }
                            else if (Uri.TryCreate(text, UriKind.Absolute, out uri))
                            {
                                break; // success
                            }

                            output.WriteAlwaysLine($"Invalid URI: '{text}'");
                        }

                        if (string.IsNullOrWhiteSpace(text))
                        {
                            output.WriteAlwaysLine($"Skipping creation of secret for '{secretInputBinding.Service.Name}'. This may prevent creation of pods until secrets are created.");
                            output.WriteAlwaysLine($"Manually create a secret with:");
                            output.WriteAlwaysLine($"kubectl create secret generic {secretInputBinding.Name} --namespace {config.Namespace} --from-literal=protocol=<value> --from-literal=host=<value> --from-literal=port=<value>");
                            continue;
                        }

                        secret = new V1Secret(type: "Opaque", stringData: new Dictionary <string, string>()
                        {
                            { "protocol", uri !.Scheme },
        public static async Task BuildContainerImageAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container)
        {
            if (output is null)
            {
                throw new ArgumentNullException(nameof(output));
            }

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

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

            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(application.GetProjectDirectory(project), Path.GetDirectoryName(project.RelativeFilePath) !, "Dockerfile");

            if (File.Exists(dockerFilePath))
            {
                output.WriteDebugLine($"Using existing dockerfile '{dockerFilePath}'.");
            }
            else
            {
                await DockerfileGenerator.WriteDockerfileAsync(output, application, service, 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 = service.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}\"",
                application.GetProjectDirectory(project),
                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}'");
            service.Outputs.Add(new DockerImageOutput(container.ImageName !, container.ImageTag !));
        }
Example #28
0
        private static void EvaluateProject(OutputContext output, DotnetProjectServiceBuilder project, string metadataFile)
        {
            var sw = Stopwatch.StartNew();

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

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

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

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

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

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

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

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

            var targetFramework = GetMetadataValueOrEmpty("TargetFramework");

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

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

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

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

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

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

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

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

            string?GetMetadataValueOrNull(string key) => metadata !.TryGetValue(key, out var value) ? value : null;
            string GetMetadataValueOrEmpty(string key) => metadata !.TryGetValue(key, out var value) ? value : string.Empty;
            bool MetadataIsTrue(string key) => metadata !.TryGetValue(key, out var value) && bool.Parse(value);
        }
Example #29
0
        public static async Task ExecuteUndeployAsync(OutputContext output, ConfigApplication application, string @namespace, bool interactive, bool whatIf)
        {
            var config = KubernetesClientConfiguration.BuildDefaultConfig();

            var kubernetes = new Kubernetes(config);

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

            // Due to some limitations in the k8s SDK we currently have a hardcoded list of resource
            // types that we handle deletes for. If we start adding extensibility for the *kinds* of
            // k8s resources we create, or the ability to deploy additional files along with the
            // resources we understand then we should revisit this.
            //
            // Basically the challenges are:
            //
            // - kubectl api-resources --all (and similar) are implemented client-side (n+1 problem)
            // - the C# k8s SDK doesn't have an untyped api for operations on arbitrary resources, the
            //   closest thing is the custom resource APIs
            // - Legacy resources without an api group don't follow the same URL scheme as more modern
            //   ones, and thus cannot be addressed using the custom resource APIs.
            //
            // So solving 'undeploy' generically would involve doing a bunch of work to query things
            // generically, including going outside of what's provided by the SDK.
            //
            // - querying api-resources
            // - querying api-groups
            // - handcrafing requests to list for each resource
            // - handcrafting requests to delete each resource
            var resources = new List <Resource>();

            var applicationName = application.Name;

            try
            {
                output.WriteDebugLine("Querying services");
                var response = await kubernetes.ListNamespacedServiceWithHttpMessagesAsync(
                    config.Namespace,
                    labelSelector : $"app.kubernetes.io/part-of={applicationName}");

                foreach (var resource in response.Body.Items)
                {
                    resource.Kind = V1Service.KubeKind;
                }

                resources.AddRange(response.Body.Items.Select(item => new Resource(item.Kind, item.Metadata, DeleteService)));
                output.WriteDebugLine($"Found {response.Body.Items.Count} matching services");
            }
            catch (Exception ex)
            {
                output.WriteDebugLine("Failed to query services.");
                output.WriteDebugLine(ex.ToString());
                throw new CommandException("Unable connect to kubernetes.", ex);
            }

            try
            {
                output.WriteDebugLine("Querying deployments");
                var response = await kubernetes.ListNamespacedDeploymentWithHttpMessagesAsync(
                    config.Namespace,
                    labelSelector : $"app.kubernetes.io/part-of={applicationName}");

                foreach (var resource in response.Body.Items)
                {
                    resource.Kind = V1Deployment.KubeKind;
                }

                resources.AddRange(response.Body.Items.Select(item => new Resource(item.Kind, item.Metadata, DeleteDeployment)));
                output.WriteDebugLine($"Found {response.Body.Items.Count} matching deployments");
            }
            catch (Exception ex)
            {
                output.WriteDebugLine("Failed to query deployments.");
                output.WriteDebugLine(ex.ToString());
                throw new CommandException("Unable connect to kubernetes.", ex);
            }

            try
            {
                output.WriteDebugLine("Querying secrets");
                var response = await kubernetes.ListNamespacedSecretWithHttpMessagesAsync(
                    config.Namespace,
                    labelSelector : $"app.kubernetes.io/part-of={applicationName}");

                foreach (var resource in response.Body.Items)
                {
                    resource.Kind = V1Secret.KubeKind;
                }

                resources.AddRange(response.Body.Items.Select(item => new Resource(item.Kind, item.Metadata, DeleteSecret)));
                output.WriteDebugLine($"Found {response.Body.Items.Count} matching secrets");
            }
            catch (Exception ex)
            {
                output.WriteDebugLine("Failed to query secrets.");
                output.WriteDebugLine(ex.ToString());
                throw new CommandException("Unable connect to kubernetes.", ex);
            }

            try
            {
                output.WriteDebugLine("Querying ingresses");
                var response = await kubernetes.ListNamespacedIngressWithHttpMessagesAsync(
                    config.Namespace,
                    labelSelector : $"app.kubernetes.io/part-of={applicationName}");

                foreach (var resource in response.Body.Items)
                {
                    resource.Kind = "Ingress";
                }

                resources.AddRange(response.Body.Items.Select(item => new Resource(item.Kind, item.Metadata, DeleteIngress)));
                output.WriteDebugLine($"Found {response.Body.Items.Count} matching ingress");
            }
            catch (Exception ex)
            {
                output.WriteDebugLine("Failed to query ingress.");
                output.WriteDebugLine(ex.ToString());
                throw new CommandException("Unable connect to kubernetes.", ex);
            }

            output.WriteInfoLine($"Found {resources.Count} resource(s).");

            var exceptions = new List <(Resource resource, HttpOperationException exception)>();

            foreach (var resource in resources)
            {
                var operation = Operations.Delete;
                if (interactive && !output.Confirm($"Delete {resource.Kind} '{resource.Metadata.Name}'?"))
                {
                    operation = Operations.None;
                }

                if (whatIf && operation == Operations.Delete)
                {
                    operation = Operations.Explain;
                }

                if (operation == Operations.None)
                {
                    output.WriteAlwaysLine($"Skipping '{resource.Kind}' '{resource.Metadata.Name}' ...");
                }
                else if (operation == Operations.Explain)
                {
                    output.WriteAlwaysLine($"whatif: Deleting '{resource.Kind}' '{resource.Metadata.Name}' ...");
                }
                else if (operation == Operations.Delete)
                {
                    output.WriteAlwaysLine($"Deleting '{resource.Kind}' '{resource.Metadata.Name}' ...");

                    try
                    {
                        var response = await resource.Deleter(resource.Metadata.Name);

                        output.WriteDebugLine($"Successfully deleted resource: '{resource.Kind}' '{resource.Metadata.Name}'.");
                    }
                    catch (HttpOperationException ex)
                    {
                        output.WriteDebugLine($"Failed to delete resource: '{resource.Kind}' '{resource.Metadata.Name}'.");
                        output.WriteDebugLine(ex.ToString());
                        exceptions.Add((resource, ex));
                    }
                }
            }

            if (exceptions.Count > 0)
            {
                throw new CommandException(
                          $"Failed to delete some resources: " + Environment.NewLine + Environment.NewLine +
                          string.Join(Environment.NewLine, exceptions.Select(e => $"\t'{e.resource.Kind}' '{e.resource.Metadata.Name}': {e.exception.Body}.")));
            }

            Task <Rest.HttpOperationResponse <V1Status> > DeleteService(string name)
            {
                return(kubernetes !.DeleteNamespacedServiceWithHttpMessagesAsync(name, config !.Namespace));
            }

            Task <Rest.HttpOperationResponse <V1Status> > DeleteDeployment(string name)
            {
                return(kubernetes !.DeleteNamespacedDeploymentWithHttpMessagesAsync(name, config !.Namespace));
            }

            Task <Rest.HttpOperationResponse <V1Status> > DeleteSecret(string name)
            {
                return(kubernetes !.DeleteNamespacedSecretWithHttpMessagesAsync(name, config !.Namespace));
            }

            Task <Rest.HttpOperationResponse <V1Status> > DeleteIngress(string name)
            {
                return(kubernetes !.DeleteNamespacedIngressWithHttpMessagesAsync(name, config !.Namespace));
            }
        }
Example #30
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);