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(); }
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)); }
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; } } } }
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}'."); }
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}'."); }
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."); }
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); }
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 !)); }
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; } } } }
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); }
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."); }
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."); } }
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)); }
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))); }
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; }
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); }
private static void LogIt(OutputContext output) { output.WriteDebugLine("Loaded: " + typeof(ProjectInstance).Assembly.FullName); output.WriteDebugLine("Loaded From: " + typeof(ProjectInstance).Assembly.Location); }
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);
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" }); }
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."); } }
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}'."); }
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")}"); }
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); } }
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" }); }
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 !)); }
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); }
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)); } }
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);