public static Task ReadProjectDetailsAsync(OutputContext output, ProjectServiceBuilder project) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } EnsureMSBuildRegistered(output, project.ProjectFile); EvaluateProject(output, project); if (!SemVersion.TryParse(project.Version, out var version)) { output.WriteInfoLine($"No version or invalid version '{project.Version}' found, using default."); version = new SemVersion(0, 1, 0); project.Version = version.ToString(); } return(Task.CompletedTask); }
public static void ApplyContainerDefaults(ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container) { if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (container.BaseImageName == null && project.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App")) { container.BaseImageName = "mcr.microsoft.com/dotnet/core/aspnet"; } else if (container.BaseImageName == null) { container.BaseImageName = "mcr.microsoft.com/dotnet/core/runtime"; } if (container.BaseImageTag == null && project.TargetFramework == "netcoreapp3.1") { container.BaseImageTag = "3.1"; } else if (container.BaseImageTag == null && project.TargetFramework == "netcoreapp3.0") { container.BaseImageTag = "3.0"; } if (container.BaseImageTag == null) { throw new CommandException($"Unsupported TFM {project.TargetFramework}."); } container.BuildImageName ??= "mcr.microsoft.com/dotnet/core/sdk"; container.BuildImageTag ??= "3.1"; if (container.ImageName == null && application.Registry?.Hostname == null) { container.ImageName ??= project.Name.ToLowerInvariant(); } else if (container.ImageName == null && application.Registry?.Hostname != null) { container.ImageName ??= $"{application.Registry?.Hostname}/{project.Name.ToLowerInvariant()}"; } container.ImageTag ??= project.Version?.Replace("+", "-") ?? "latest"; }
public static SidecarBuilder GetOrAddSidecar(ProjectServiceBuilder project) { // Bring your rain boots. project.RelocateDiagnosticsDomainSockets = true; var sidecar = project.Sidecars.FirstOrDefault(s => s.Name == "tye-diag-agent"); if (sidecar is object) { return(sidecar); } sidecar = new SidecarBuilder("tye-diag-agent", "rynowak/tye-diag-agent", "0.1") { Args = { "--kubernetes=true", $"--service={project.Name}", $"--assemblyName={project.AssemblyName}", }, }; project.Sidecars.Add(sidecar); return(sidecar); }
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 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);
protected bool SkipWithoutProject(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out ProjectServiceBuilder project) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (service is null) { throw new ArgumentNullException(nameof(service)); } if (service is ProjectServiceBuilder p) { project = p; return(false); } output.WriteInfoLine($"Service '{service.Name}' does not have a project associated. Skipping."); project = default !;
public static KubernetesDeploymentOutput CreateDeployment( OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, DeploymentManifestInfo deployment) { var bindings = project.Outputs.OfType <ComputedBindings>().FirstOrDefault(); var root = new YamlMappingNode(); root.Add("kind", "Deployment"); root.Add("apiVersion", "apps/v1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", project.Name); if (!string.IsNullOrEmpty(application.Namespace)) { metadata.Add("namespace", application.Namespace); } if (deployment.Annotations.Count > 0) { var annotations = new YamlMappingNode(); metadata.Add("annotations", annotations); foreach (var annotation in deployment.Annotations) { annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value) { Style = ScalarStyle.SingleQuoted, }); } } var labels = new YamlMappingNode(); metadata.Add("labels", labels); foreach (var label in deployment.Labels) { labels.Add(label.Key, new YamlScalarNode(label.Value) { Style = ScalarStyle.SingleQuoted, }); } var spec = new YamlMappingNode(); root.Add("spec", spec); spec.Add("replicas", project.Replicas.ToString()); var selector = new YamlMappingNode(); spec.Add("selector", selector); var matchLabels = new YamlMappingNode(); selector.Add("matchLabels", matchLabels); // We need the name so we can use it with matchLabels. if (!deployment.Labels.TryGetValue("app.kubernetes.io/name", out var matchLabelsLabelValue)) { throw new InvalidOperationException("The label 'app.kubernetes.io/name` is required."); } matchLabels.Add("app.kubernetes.io/name", matchLabelsLabelValue); var template = new YamlMappingNode(); spec.Add("template", template); metadata = new YamlMappingNode(); template.Add("metadata", metadata); if (deployment.Annotations.Count > 0) { var annotations = new YamlMappingNode(); metadata.Add("annotations", annotations); foreach (var annotation in deployment.Annotations) { annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value) { Style = ScalarStyle.SingleQuoted, }); } } labels = new YamlMappingNode(); metadata.Add("labels", labels); foreach (var label in deployment.Labels) { labels.Add(label.Key, new YamlScalarNode(label.Value) { Style = ScalarStyle.SingleQuoted, }); } spec = new YamlMappingNode(); template.Add("spec", spec); var containers = new YamlSequenceNode(); spec.Add("containers", containers); var images = project.Outputs.OfType <DockerImageOutput>(); foreach (var image in images) { var container = new YamlMappingNode(); containers.Add(container); container.Add("name", project.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{image.ImageName}:{image.ImageTag}"); container.Add("imagePullPolicy", "Always"); // helps avoid problems with development + weak versioning if (project.EnvironmentVariables.Count > 0 || // We generate ASPNETCORE_URLS if there are bindings for http project.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) || // We generate environment variables for other services if there dependencies (bindings is object && bindings.Bindings.Any())) { var env = new YamlSequenceNode(); container.Add("env", env); foreach (var kvp in project.EnvironmentVariables) { env.Add(new YamlMappingNode() { { "name", kvp.Name }, { "value", new YamlScalarNode(kvp.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } if (bindings is object) { foreach (var binding in bindings.Bindings.OfType <EnvironmentVariableInputBinding>()) { env.Add(new YamlMappingNode() { { "name", binding.Name }, { "value", new YamlScalarNode(binding.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } foreach (var binding in bindings.Bindings.OfType <SecretInputBinding>()) { //- name: SECRET_USERNAME // valueFrom: // secretKeyRef: // name: mysecret // key: username if (binding is SecretConnectionStringInputBinding connectionStringBinding) { env.Add(new YamlMappingNode() { { "name", connectionStringBinding.KeyName }, { "valueFrom", new YamlMappingNode() { { "secretKeyRef", new YamlMappingNode() { { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, { "key", new YamlScalarNode("connectionstring") { Style = ScalarStyle.SingleQuoted } }, } }, } }, }); } else if (binding is SecretUrlInputBinding urlBinding) { env.Add(new YamlMappingNode() { { "name", $"{urlBinding.KeyNameBase}__PROTOCOL" }, { "valueFrom", new YamlMappingNode() { { "secretKeyRef", new YamlMappingNode() { { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, { "key", new YamlScalarNode("protocol") { Style = ScalarStyle.SingleQuoted } }, } }, } }, }); env.Add(new YamlMappingNode() { { "name", $"{urlBinding.KeyNameBase}__HOST" }, { "valueFrom", new YamlMappingNode() { { "secretKeyRef", new YamlMappingNode() { { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, { "key", new YamlScalarNode("host") { Style = ScalarStyle.SingleQuoted } }, } }, } }, }); env.Add(new YamlMappingNode() { { "name", $"{urlBinding.KeyNameBase}__PORT" }, { "valueFrom", new YamlMappingNode() { { "secretKeyRef", new YamlMappingNode() { { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, { "key", new YamlScalarNode("port") { Style = ScalarStyle.SingleQuoted } }, } }, } }, }); } } } } if (project.Bindings.Count > 0) { var ports = new YamlSequenceNode(); container.Add("ports", ports); foreach (var binding in project.Bindings) { if (binding.Protocol == "https") { // We skip https for now in deployment, because the E2E requires certificates // and we haven't done those features yet. continue; } if (binding.Port != null) { var containerPort = new YamlMappingNode(); ports.Add(containerPort); containerPort.Add("containerPort", binding.Port.Value.ToString()); } } } } return(new KubernetesDeploymentOutput(project.Name, new YamlDocument(root))); }
public static KubernetesServiceOutput CreateService( OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, DeploymentManifestInfo deployment, ServiceManifestInfo service) { var root = new YamlMappingNode(); root.Add("kind", "Service"); root.Add("apiVersion", "v1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", project.Name); if (!string.IsNullOrEmpty(application.Namespace)) { metadata.Add("namespace", application.Namespace); } if (service.Annotations.Count > 0) { var annotations = new YamlMappingNode(); metadata.Add("annotations", annotations); foreach (var annotation in service.Annotations) { annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value) { Style = ScalarStyle.SingleQuoted, }); } } var labels = new YamlMappingNode(); metadata.Add("labels", labels); foreach (var label in service.Labels) { labels.Add(label.Key, new YamlScalarNode(label.Value) { Style = ScalarStyle.SingleQuoted, }); } var spec = new YamlMappingNode(); root.Add("spec", spec); var selector = new YamlMappingNode(); spec.Add("selector", selector); // We need the name so we can use it with selector. if (!deployment.Labels.TryGetValue("app.kubernetes.io/name", out var selectorLabelValue)) { throw new InvalidOperationException("The label 'app.kubernetes.io/name` is required."); } selector.Add("app.kubernetes.io/name", selectorLabelValue); spec.Add("type", "ClusterIP"); var ports = new YamlSequenceNode(); spec.Add("ports", ports); // We figure out the port based on bindings foreach (var binding in project.Bindings) { if (binding.Protocol == "https") { // We skip https for now in deployment, because the E2E requires certificates // and we haven't done those features yet. continue; } if (binding.Port != null) { var port = new YamlMappingNode(); ports.Add(port); port.Add("name", binding.Name ?? binding.Protocol ?? "http"); port.Add("protocol", "TCP"); // we use assume TCP. YOLO port.Add("port", binding.Port.Value.ToString()); port.Add("targetPort", binding.Port.Value.ToString()); } } return(new KubernetesServiceOutput(project.Name, new YamlDocument(root))); }
public static OamComponentOutput CreateOamComponent(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } var root = new YamlMappingNode(); root.Add("kind", "ComponentSchematic"); root.Add("apiVersion", "core.oam.dev/v1alpha1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", project.Name); var spec = new YamlMappingNode(); root.Add("spec", spec); spec.Add("workloadType", "core.oam.dev/v1alpha1.Server"); var containers = new YamlSequenceNode(); spec.Add("containers", containers); var images = project.Outputs.OfType <DockerImageOutput>(); foreach (var image in images) { var container = new YamlMappingNode(); containers.Add(container); container.Add("name", project.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{image.ImageName}:{image.ImageTag}"); } return(new OamComponentOutput(project.Name, new YamlDocument(root))); }
public static ServiceOutput CreateDeployment(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } var bindings = project.Outputs.OfType <ComputedBindings>().FirstOrDefault(); var root = new YamlMappingNode(); root.Add("kind", "Deployment"); root.Add("apiVersion", "apps/v1"); var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", project.Name); var labels = new YamlMappingNode(); metadata.Add("labels", labels); labels.Add("app.kubernetes.io/name", project.Name); labels.Add("app.kubernetes.io/part-of", application.Name); var spec = new YamlMappingNode(); root.Add("spec", spec); spec.Add("replicas", project.Replicas.ToString()); var selector = new YamlMappingNode(); spec.Add("selector", selector); var matchLabels = new YamlMappingNode(); selector.Add("matchLabels", matchLabels); matchLabels.Add("app.kubernetes.io/name", project.Name); var template = new YamlMappingNode(); spec.Add("template", template); metadata = new YamlMappingNode(); template.Add("metadata", metadata); labels = new YamlMappingNode(); metadata.Add("labels", labels); labels.Add("app.kubernetes.io/name", project.Name); labels.Add("app.kubernetes.io/part-of", application.Name); spec = new YamlMappingNode(); template.Add("spec", spec); var containers = new YamlSequenceNode(); spec.Add("containers", containers); var images = project.Outputs.OfType <DockerImageOutput>(); foreach (var image in images) { var container = new YamlMappingNode(); containers.Add(container); container.Add("name", project.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{image.ImageName}:{image.ImageTag}"); container.Add("imagePullPolicy", "Always"); // helps avoid problems with development + weak versioning if (project.EnvironmentVariables.Count > 0 || // We generate ASPNETCORE_URLS if there are bindings for http project.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) || // We generate environment variables for other services if there dependencies (bindings is object && bindings.Bindings.OfType <EnvironmentVariableInputBinding>().Any())) { var env = new YamlSequenceNode(); container.Add("env", env); foreach (var kvp in project.EnvironmentVariables) { env.Add(new YamlMappingNode() { { "name", kvp.Name }, { "value", new YamlScalarNode(kvp.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } if (bindings is object) { foreach (var binding in bindings.Bindings.OfType <EnvironmentVariableInputBinding>()) { env.Add(new YamlMappingNode() { { "name", binding.Name }, { "value", new YamlScalarNode(binding.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } } } if (bindings is object && bindings.Bindings.OfType <SecretInputBinding>().Any()) { var volumeMounts = new YamlSequenceNode(); container.Add("volumeMounts", volumeMounts); foreach (var binding in bindings.Bindings.OfType <SecretInputBinding>()) { var volumeMount = new YamlMappingNode(); volumeMounts.Add(volumeMount); volumeMount.Add("name", $"{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}"); volumeMount.Add("mountPath", $"/var/tye/bindings/{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}"); volumeMount.Add("readOnly", "true"); } } if (project.Bindings.Count > 0) { var ports = new YamlSequenceNode(); container.Add("ports", ports); foreach (var binding in project.Bindings) { if (binding.Protocol == "https") { // We skip https for now in deployment, because the E2E requires certificates // and we haven't done those features yet. continue; } if (binding.Port != null) { var containerPort = new YamlMappingNode(); ports.Add(containerPort); containerPort.Add("containerPort", binding.Port.Value.ToString()); } } } } if (bindings.Bindings.OfType <SecretInputBinding>().Any()) { var volumes = new YamlSequenceNode(); spec.Add("volumes", volumes); foreach (var binding in bindings.Bindings.OfType <SecretInputBinding>()) { var volume = new YamlMappingNode(); volumes.Add(volume); volume.Add("name", $"{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}"); var secret = new YamlMappingNode(); volume.Add("secret", secret); secret.Add("secretName", binding.Name !); var items = new YamlSequenceNode(); secret.Add("items", items); var item = new YamlMappingNode(); items.Add(item); item.Add("key", "connectionstring"); item.Add("path", binding.Filename); } } return(new KubernetesDeploymentOutput(project.Name, new YamlDocument(root))); }
private static void EvaluateProject(OutputContext output, ProjectServiceBuilder project) { var sw = Stopwatch.StartNew(); // Currently we only log at debug level. var logger = new ConsoleLogger( verbosity: LoggerVerbosity.Normal, write: message => output.WriteDebug(message), colorSet: null, colorReset: null); // We need to isolate projects from each other for testing. MSBuild does not support // loading the same project twice in the same collection. var projectCollection = new ProjectCollection(); ProjectInstance projectInstance; Microsoft.Build.Evaluation.Project msbuildProject; try { output.WriteDebugLine($"Loading project '{project.ProjectFile.FullName}'."); msbuildProject = Microsoft.Build.Evaluation.Project.FromFile(project.ProjectFile.FullName, new ProjectOptions() { ProjectCollection = projectCollection, GlobalProperties = project.BuildProperties }); projectInstance = msbuildProject.CreateProjectInstance(); output.WriteDebugLine($"Loaded project '{project.ProjectFile.FullName}'."); } catch (Exception ex) { throw new CommandException($"Failed to load project: '{project.ProjectFile.FullName}'.", ex); } try { AssemblyLoadContext.Default.Resolving += ResolveAssembly; output.WriteDebugLine($"Restoring project '{project.ProjectFile.FullName}'."); // Similar to what MSBuild does for restore: // https://github.com/microsoft/msbuild/blob/3453beee039fb6f5ccc54ac783ebeced31fec472/src/MSBuild/XMake.cs#L1417 // // We need to do restore as a separate operation var restoreRequest = new BuildRequestData( projectInstance, targetsToBuild: new[] { "Restore" }, hostServices: null, flags: BuildRequestDataFlags.ClearCachesAfterBuild | BuildRequestDataFlags.SkipNonexistentTargets | BuildRequestDataFlags.IgnoreMissingEmptyAndInvalidImports); var parameters = new BuildParameters(projectCollection) { Loggers = new[] { logger, }, }; // We don't really look at the result, because it's not clear we should halt totally // if restore fails. var restoreResult = BuildManager.DefaultBuildManager.Build(parameters, restoreRequest); output.WriteDebugLine($"Restored project '{project.ProjectFile.FullName}'."); msbuildProject.MarkDirty(); projectInstance = msbuildProject.CreateProjectInstance(); var targets = new List <string>() { "ResolveReferences", "ResolvePackageDependenciesDesignTime", "PrepareResources", "GetAssemblyAttributes", }; var result = projectInstance.Build( targets: targets.ToArray(), loggers: new[] { logger, }); // If the build fails, we're not really blocked from doing our work. // For now we just log the output to debug. There are errors that occur during // running these targets we don't really care as long as we get the data. } finally { AssemblyLoadContext.Default.Resolving -= ResolveAssembly; } // Reading a few different version properties to be more resilient. var version = projectInstance.GetProperty("AssemblyInformationalVersion")?.EvaluatedValue ?? projectInstance.GetProperty("InformationalVersion")?.EvaluatedValue ?? projectInstance.GetProperty("Version").EvaluatedValue; project.Version = version; output.WriteDebugLine($"Found application version: {version}"); var targetFrameworks = projectInstance.GetPropertyValue("TargetFrameworks"); project.TargetFrameworks = targetFrameworks.Split(';', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty <string>(); project.RunCommand = projectInstance.GetPropertyValue("RunCommand"); project.RunArguments = projectInstance.GetPropertyValue("RunArguments"); project.TargetPath = projectInstance.GetPropertyValue("TargetPath"); project.PublishDir = projectInstance.GetPropertyValue("PublishDir"); project.AssemblyName = projectInstance.GetPropertyValue("AssemblyName"); project.IntermediateOutputPath = projectInstance.GetPropertyValue("IntermediateOutputPath"); output.WriteDebugLine($"RunCommand={project.RunCommand}"); output.WriteDebugLine($"RunArguments={project.RunArguments}"); output.WriteDebugLine($"TargetPath={project.TargetPath}"); output.WriteDebugLine($"PublishDir={project.PublishDir}"); output.WriteDebugLine($"AssemblyName={project.AssemblyName}"); output.WriteDebugLine($"IntermediateOutputPath={project.IntermediateOutputPath}"); // Normalize directories to their absolute paths project.IntermediateOutputPath = Path.Combine(project.ProjectFile.DirectoryName, NormalizePath(project.IntermediateOutputPath)); project.TargetPath = Path.Combine(project.ProjectFile.DirectoryName, NormalizePath(project.TargetPath)); project.PublishDir = Path.Combine(project.ProjectFile.DirectoryName, NormalizePath(project.PublishDir)); var targetFramework = projectInstance.GetPropertyValue("TargetFramework"); project.TargetFramework = targetFramework; output.WriteDebugLine($"Found target framework: {targetFramework}"); // TODO: Parse the name and version manually out of the TargetFramework field if it's non-null project.TargetFrameworkName = projectInstance.GetPropertyValue("_ShortFrameworkIdentifier"); project.TargetFrameworkVersion = projectInstance.GetPropertyValue("_ShortFrameworkVersion") ?? projectInstance.GetPropertyValue("_TargetFrameworkVersionWithoutV"); var sharedFrameworks = projectInstance.GetItems("FrameworkReference").Select(i => i.EvaluatedInclude).ToList(); project.Frameworks.AddRange(sharedFrameworks.Select(s => new Framework(s))); output.WriteDebugLine($"Found shared frameworks: {string.Join(", ", sharedFrameworks)}"); bool PropertyIsTrue(string property) { return(projectInstance.GetPropertyValue(property) is string s && !string.IsNullOrEmpty(s) && bool.Parse(s)); } project.IsAspNet = project.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App") || projectInstance.GetPropertyValue("MicrosoftNETPlatformLibrary") == "Microsoft.AspNetCore.App" || PropertyIsTrue("_AspNetCoreAppSharedFxIsEnabled") || PropertyIsTrue("UsingMicrosoftNETSdkWeb"); output.WriteDebugLine($"IsAspNet={project.IsAspNet}"); output.WriteDebugLine($"Evaluation Took: {sw.Elapsed.TotalMilliseconds}ms"); // The Microsoft.Build.Locator doesn't handle the loading of other assemblies // that are shipped with MSBuild (ex NuGet). // // This means that the set of assemblies that need special handling depends on the targets // that we run :( // // This is workaround for this limitation based on the targets we need to run // to resolve references and versions. // // See: https://github.com/microsoft/MSBuildLocator/issues/86 Assembly?ResolveAssembly(AssemblyLoadContext context, AssemblyName assemblyName) { if (assemblyName.Name is object) { var msbuildDirectory = Environment.GetEnvironmentVariable("MSBuildExtensionsPath") !; var assemblyFilePath = Path.Combine(msbuildDirectory, assemblyName.Name + ".dll"); if (File.Exists(assemblyFilePath)) { return(context.LoadFromAssemblyPath(assemblyFilePath)); } } return(default);
public static KubernetesDeploymentOutput CreateDeployment( OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, DeploymentManifestInfo deployment) { var bindings = project.Outputs.OfType <ComputedBindings>().FirstOrDefault(); var root = new YamlMappingNode { { "kind", "Deployment" }, { "apiVersion", "apps/v1" } }; var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", project.Name); if (!string.IsNullOrEmpty(application.Namespace)) { metadata.Add("namespace", application.Namespace); } if (deployment.Annotations.Count > 0) { var annotations = new YamlMappingNode(); metadata.Add("annotations", annotations); foreach (var annotation in deployment.Annotations) { annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value) { Style = ScalarStyle.SingleQuoted, }); } } var labels = new YamlMappingNode(); metadata.Add("labels", labels); foreach (var label in deployment.Labels) { labels.Add(label.Key, new YamlScalarNode(label.Value) { Style = ScalarStyle.SingleQuoted, }); } var spec = new YamlMappingNode(); root.Add("spec", spec); spec.Add("replicas", project.Replicas.ToString()); var selector = new YamlMappingNode(); spec.Add("selector", selector); var matchLabels = new YamlMappingNode(); selector.Add("matchLabels", matchLabels); // We need the name so we can use it with matchLabels. if (!deployment.Labels.TryGetValue("app.kubernetes.io/name", out var matchLabelsLabelValue)) { throw new InvalidOperationException("The label 'app.kubernetes.io/name` is required."); } matchLabels.Add("app.kubernetes.io/name", matchLabelsLabelValue); var template = new YamlMappingNode(); spec.Add("template", template); metadata = new YamlMappingNode(); template.Add("metadata", metadata); if (deployment.Annotations.Count > 0) { var annotations = new YamlMappingNode(); metadata.Add("annotations", annotations); foreach (var annotation in deployment.Annotations) { annotations.Add(annotation.Key, new YamlScalarNode(annotation.Value) { Style = ScalarStyle.SingleQuoted, }); } } labels = new YamlMappingNode(); metadata.Add("labels", labels); foreach (var label in deployment.Labels) { labels.Add(label.Key, new YamlScalarNode(label.Value) { Style = ScalarStyle.SingleQuoted, }); } spec = new YamlMappingNode(); template.Add("spec", spec); if (project.Sidecars.Count > 0) { // Share process namespace when we have sidecars. So we can list other processes. // see: https://kubernetes.io/docs/tasks/configure-pod-container/share-process-namespace/#understanding-process-namespace-sharing spec.Add("shareProcessNamespace", new YamlScalarNode("true") { Style = ScalarStyle.Plain }); } if (project.RelocateDiagnosticsDomainSockets) { // Our diagnostics functionality uses $TMPDIR to locate other dotnet processes through // eventpipe. see: https://github.com/dotnet/diagnostics/blob/master/documentation/design-docs/ipc-protocol.md#transport // // In order for diagnostics features to 'find' each other, we need to make $TMPDIR into // something shared. // // see: https://kubernetes.io/docs/tasks/access-application-cluster/communicate-containers-same-pod-shared-volume/ project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("TMPDIR") { Value = "/var/tye/diagnostics", }); foreach (var sidecar in project.Sidecars) { sidecar.EnvironmentVariables.Add(new EnvironmentVariableBuilder("TMPDIR") { Value = "/var/tye/diagnostics", }); } } var containers = new YamlSequenceNode(); spec.Add("containers", containers); var images = project.Outputs.OfType <DockerImageOutput>(); foreach (var image in images) { var container = new YamlMappingNode(); containers.Add(container); container.Add("name", project.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{image.ImageName}:{image.ImageTag}"); container.Add("imagePullPolicy", "Always"); // helps avoid problems with development + weak versioning var volumeMounts = new YamlSequenceNode(); var localMounts = new YamlSequenceNode(); if (project.Volumes.Count > 0) { container.Add("volumeMounts", volumeMounts); spec.Add("volumes", localMounts); foreach (var vol in project.Volumes) { var volumeMount = new YamlMappingNode(); volumeMounts.Add(volumeMount); volumeMount.Add("name", vol.Name); volumeMount.Add("mountPath", vol.Target); var localMount = new YamlMappingNode(); var hostPath = new YamlMappingNode(); var path = new YamlMappingNode(); localMounts.Add(localMount); localMount.Add("name", vol.Name); localMount.Add("hostPath", hostPath); hostPath.Add("path", vol.Source); } } if (project.EnvironmentVariables.Count > 0 || // We generate ASPNETCORE_URLS if there are bindings for http project.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) || // We generate environment variables for other services if there dependencies bindings?.Bindings.Count > 0) { var env = new YamlSequenceNode(); container.Add("env", env); foreach (var kvp in project.EnvironmentVariables) { env.Add(new YamlMappingNode() { { "name", kvp.Name }, { "value", new YamlScalarNode(kvp.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } if (bindings != null) { AddEnvironmentVariablesForComputedBindings(env, bindings); } if (project.RelocateDiagnosticsDomainSockets) { // volumeMounts: // - name: shared-data // mountPath: /usr/share/nginx/html if (!container.Children.ContainsKey("volumeMounts")) { container.Add("volumeMounts", volumeMounts); } var volumeMount = new YamlMappingNode(); volumeMounts.Add(volumeMount); volumeMount.Add("name", "tye-diagnostics"); volumeMount.Add("mountPath", "/var/tye/diagnostics"); } } if (project.Bindings.Count > 0) { var ports = new YamlSequenceNode(); container.Add("ports", ports); foreach (var binding in project.Bindings) { if (binding.Protocol == "https") { // We skip https for now in deployment, because the E2E requires certificates // and we haven't done those features yet. continue; } if (binding.Port == null) { continue; } var containerPort = new YamlMappingNode(); ports.Add(containerPort); containerPort.Add("containerPort", (binding.ContainerPort ?? binding.Port.Value).ToString()); } } if (project.Liveness != null) { AddProbe(project, container, project.Liveness !, "livenessProbe"); } if (project.Readiness != null) { AddProbe(project, container, project.Readiness !, "readinessProbe"); } } foreach (var sidecar in project.Sidecars) { var container = new YamlMappingNode(); containers.Add(container); container.Add("name", sidecar.Name); // NOTE: to really support multiple images we'd need to generate unique names. container.Add("image", $"{sidecar.ImageName}:{sidecar.ImageTag}"); container.Add("imagePullPolicy", "Always"); // helps avoid problems with development + weak versioning if (sidecar.Args.Count > 0) { var args = new YamlSequenceNode(); container.Add("args", args); foreach (var arg in sidecar.Args) { args.Add(new YamlScalarNode(arg) { Style = ScalarStyle.SingleQuoted, }); } } var sidecarBindings = sidecar.Outputs.OfType <ComputedBindings>().FirstOrDefault(); if (sidecar.EnvironmentVariables.Count > 0 || sidecarBindings?.Bindings.Count > 0) { var env = new YamlSequenceNode(); container.Add("env", env); foreach (var kvp in sidecar.EnvironmentVariables) { env.Add(new YamlMappingNode() { { "name", kvp.Name }, { "value", new YamlScalarNode(kvp.Value) { Style = ScalarStyle.SingleQuoted, } }, }); } if (sidecarBindings != null) { AddEnvironmentVariablesForComputedBindings(env, sidecarBindings); } } if (!project.RelocateDiagnosticsDomainSockets) { continue; } // volumeMounts: // - name: shared-data // mountPath: /usr/share/nginx/html var volumeMounts = new YamlSequenceNode(); container.Add("volumeMounts", volumeMounts); var volumeMount = new YamlMappingNode(); volumeMounts.Add(volumeMount); volumeMount.Add("name", "tye-diagnostics"); volumeMount.Add("mountPath", "/var/tye/diagnostics"); } if (!project.RelocateDiagnosticsDomainSockets) { return(new KubernetesDeploymentOutput(project.Name, new YamlDocument(root))); } // volumes: // - name: shared-data // emptyDir: {} var volumes = new YamlSequenceNode(); spec.Add("volumes", volumes); var volume = new YamlMappingNode(); volumes.Add(volume); volume.Add("name", "tye-diagnostics"); volume.Add("emptyDir", new YamlMappingNode()); return(new KubernetesDeploymentOutput(project.Name, new YamlDocument(root))); }
public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source) { if (source is null) { throw new ArgumentNullException(nameof(source)); } var config = ConfigFactory.FromFile(source); ValidateConfigApplication(config); var builder = new ApplicationBuilder(source, config.Name ?? source.Directory.Name.ToLowerInvariant()); if (!string.IsNullOrEmpty(config.Registry)) { builder.Registry = new ContainerRegistry(config.Registry); } foreach (var configService in config.Services) { ServiceBuilder service; if (!string.IsNullOrEmpty(configService.Project)) { var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project); var projectFile = new FileInfo(Path.Combine(builder.Source.DirectoryName, expandedProject)); var project = new ProjectServiceBuilder(configService.Name, projectFile); service = project; project.Build = configService.Build ?? true; project.Args = configService.Args; project.Replicas = configService.Replicas ?? 1; await ProjectReader.ReadProjectDetailsAsync(output, project); // We don't apply more container defaults here because we might need // to prompty for the registry name. project.ContainerInfo = new ContainerInfo() { UseMultiphaseDockerfile = false, }; } else if (!string.IsNullOrEmpty(configService.Image)) { var container = new ContainerServiceBuilder(configService.Name, configService.Image) { Args = configService.Args, Replicas = configService.Replicas ?? 1 }; service = container; } else if (!string.IsNullOrEmpty(configService.Executable)) { var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable); var workingDirectory = ""; // Special handling of .dlls as executables (it will be executed as dotnet {dll}) if (Path.GetExtension(expandedExecutable) == ".dll") { expandedExecutable = Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, expandedExecutable)); workingDirectory = Path.GetDirectoryName(expandedExecutable) !; } var executable = new ExecutableServiceBuilder(configService.Name, expandedExecutable) { Args = configService.Args, WorkingDirectory = configService.WorkingDirectory != null? Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) : workingDirectory, Replicas = configService.Replicas ?? 1 }; service = executable; } else if (configService.External) { var external = new ExternalServiceBuilder(configService.Name); service = external; } else { throw new CommandException("Unable to determine service type."); } builder.Services.Add(service); foreach (var configBinding in configService.Bindings) { var binding = new BindingBuilder() { Name = configBinding.Name, AutoAssignPort = configBinding.AutoAssignPort, ConnectionString = configBinding.ConnectionString, Host = configBinding.Host, ContainerPort = configBinding.ContainerPort, Port = configBinding.Port, Protocol = configBinding.Protocol, }; // Assume HTTP for projects only (containers may be different) if (binding.ConnectionString == null && configService.Project != null) { binding.Protocol ??= "http"; } service.Bindings.Add(binding); } foreach (var configEnvVar in configService.Configuration) { var envVar = new EnvironmentVariable(configEnvVar.Name, configEnvVar.Value); if (service is ProjectServiceBuilder project) { project.EnvironmentVariables.Add(envVar); } else if (service is ContainerServiceBuilder container) { container.EnvironmentVariables.Add(envVar); } else if (service is ExecutableServiceBuilder executable) { executable.EnvironmentVariables.Add(envVar); } else if (service is ExternalServiceBuilder) { throw new CommandException("External services do not support environment variables."); } else { throw new CommandException("Unable to determine service type."); } } foreach (var configVolume in configService.Volumes) { var volume = new VolumeBuilder(configVolume.Source, configVolume.Target); if (service is ProjectServiceBuilder project) { project.Volumes.Add(volume); } else if (service is ContainerServiceBuilder container) { container.Volumes.Add(volume); } else if (service is ExecutableServiceBuilder executable) { throw new CommandException("Executable services do not support volumes."); } else if (service is ExternalServiceBuilder) { throw new CommandException("External services do not support volumes."); } else { throw new CommandException("Unable to determine service type."); } } } foreach (var configIngress in config.Ingress) { var ingress = new IngressBuilder(configIngress.Name); ingress.Replicas = configIngress.Replicas ?? 1; builder.Ingress.Add(ingress); foreach (var configBinding in configIngress.Bindings) { var binding = new IngressBindingBuilder() { AutoAssignPort = configBinding.AutoAssignPort, Name = configBinding.Name, Port = configBinding.Port, Protocol = configBinding.Protocol ?? "http", }; ingress.Bindings.Add(binding); } foreach (var configRule in configIngress.Rules) { var rule = new IngressRuleBuilder() { Host = configRule.Host, Path = configRule.Path, Service = configRule.Service, }; ingress.Rules.Add(rule); } } return(builder); }
public static async Task BuildContainerImageAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } using var tempFile = TempFile.Create(); var dockerFilePath = Path.Combine(project.ProjectFile.DirectoryName, "Dockerfile"); if (File.Exists(dockerFilePath)) { output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'."); } else { await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, tempFile.FilePath); dockerFilePath = tempFile.FilePath; } // We need to know if this is a single-phase or multi-phase Dockerfile because the context directory will be // different depending on that choice. string contextDirectory; if (container.UseMultiphaseDockerfile ?? true) { contextDirectory = "."; } else { var publishOutput = project.Outputs.OfType <ProjectPublishOutput>().FirstOrDefault(); if (publishOutput is null) { throw new InvalidOperationException("We should have published the project for a single-phase Dockerfile."); } contextDirectory = publishOutput.Directory.FullName; } output.WriteDebugLine("Running 'docker build'."); output.WriteCommandLine("docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\""); var capture = output.Capture(); var exitCode = await Process.ExecuteAsync( $"docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"", project.ProjectFile.DirectoryName, stdOut : capture.StdOut, stdErr : capture.StdErr); output.WriteDebugLine($"Done running 'docker build' exit code: {exitCode}"); if (exitCode != 0) { throw new CommandException("'docker build' failed."); } output.WriteInfoLine($"Created Docker Image: '{container.ImageName}:{container.ImageTag}'"); project.Outputs.Add(new DockerImageOutput(container.ImageName !, container.ImageTag !)); }
public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source) { if (source is null) { throw new ArgumentNullException(nameof(source)); } var config = ConfigFactory.FromFile(source); ValidateConfigApplication(config); var builder = new ApplicationBuilder(source, config.Name ?? source.Directory.Name.ToLowerInvariant()); if (!string.IsNullOrEmpty(config.Registry)) { builder.Registry = new ContainerRegistry(config.Registry); } foreach (var configService in config.Services) { ServiceBuilder service; if (!string.IsNullOrEmpty(configService.Project)) { var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project); var projectFile = new FileInfo(Path.Combine(builder.Source.DirectoryName, expandedProject)); var project = new ProjectServiceBuilder(configService.Name, projectFile); service = project; project.Build = configService.Build ?? true; project.Args = configService.Args; project.Replicas = configService.Replicas ?? 1; await ProjectReader.ReadProjectDetailsAsync(output, project); // We don't apply more container defaults here because we might need // to prompty for the registry name. project.ContainerInfo = new ContainerInfo() { UseMultiphaseDockerfile = false, }; } else if (!string.IsNullOrEmpty(configService.Image)) { var container = new ContainerServiceBuilder(configService.Name, configService.Image) { Args = configService.Args, Replicas = configService.Replicas ?? 1 }; service = container; } else if (!string.IsNullOrEmpty(configService.Executable)) { var expandedExecutable = Environment.ExpandEnvironmentVariables(configService.Executable); var workingDirectory = ""; // Special handling of .dlls as executables (it will be executed as dotnet {dll}) if (Path.GetExtension(expandedExecutable) == ".dll") { expandedExecutable = Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, expandedExecutable)); workingDirectory = Path.GetDirectoryName(expandedExecutable) !; } var executable = new ExecutableServiceBuilder(configService.Name, expandedExecutable) { Args = configService.Args, WorkingDirectory = configService.WorkingDirectory != null? Path.GetFullPath(Path.Combine(builder.Source.Directory.FullName, Environment.ExpandEnvironmentVariables(configService.WorkingDirectory))) : workingDirectory, Replicas = configService.Replicas ?? 1 }; service = executable; } else if (configService.External) { var external = new ExternalServiceBuilder(configService.Name); service = external; } else { throw new CommandException("Unable to determine service type."); } builder.Services.Add(service); // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding if (configService.Bindings.Count == 0 && service is ProjectServiceBuilder project2 && project2.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App")) { // HTTP is the default binding service.Bindings.Add(new BindingBuilder() { AutoAssignPort = true, Protocol = "http" }); service.Bindings.Add(new BindingBuilder() { Name = "https", AutoAssignPort = true, Protocol = "https" }); }
public static async Task GenerateAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (chart is null) { throw new ArgumentNullException(nameof(chart)); } if (outputDirectory is null) { throw new ArgumentNullException(nameof(outputDirectory)); } ApplyHelmChartDefaults(application, project, container, chart); // The directory with the charts needs to be the same as the chart name var chartDirectoryPath = Path.Combine(outputDirectory.FullName, chart.ChartName); Directory.CreateDirectory(chartDirectoryPath); var templateDirectoryPath = Path.Combine( Path.GetDirectoryName(typeof(HelmChartGenerator).Assembly.Location) !, "Templates", "Helm"); DirectoryCopy.Copy(templateDirectoryPath, chartDirectoryPath); // Write Chart.yaml // // apiVersion: v1 // name: <appname> // version: <version> // appVersion: <version> await File.WriteAllLinesAsync(Path.Combine(chartDirectoryPath, "Chart.yaml"), new[] { $"apiVersion: v1", $"name: {chart.ChartName}", $"# helm requires the version and appVersion to specified in Chart.yaml", $"# tye will override these values when packaging the chart", $"version: {project.Version.Replace('+', '-')}", $"appVersion: {project.Version.Replace('+', '-')}" }); // Write values.yaml // // image: // repository: rynowak.azurecr.io/rochambot/gamemaster await File.WriteAllLinesAsync(Path.Combine(chartDirectoryPath, "values.yaml"), new[] { $"image:", $" repository: {container.ImageName}", }); }
public static async Task WriteDockerfileAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, string filePath) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } if (filePath is null) { throw new ArgumentNullException(nameof(filePath)); } await using var stream = File.OpenWrite(filePath); await using var writer = new StreamWriter(stream, encoding: Encoding.UTF8, bufferSize: -1, leaveOpen: true); var entryPoint = Path.GetFileNameWithoutExtension(project.ProjectFile.Name); output.WriteDebugLine($"Writing Dockerfile to '{filePath}'."); if (container.UseMultiphaseDockerfile ?? true) { await WriteMultiphaseDockerfileAsync(writer, entryPoint, container); } else { await WriteLocalPublishDockerfileAsync(writer, entryPoint, container); } output.WriteDebugLine("Done writing Dockerfile."); }
public static async Task BuildContainerImageAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container) { if (output is null) { throw new ArgumentNullException(nameof(output)); } if (application is null) { throw new ArgumentNullException(nameof(application)); } if (project is null) { throw new ArgumentNullException(nameof(project)); } if (container is null) { throw new ArgumentNullException(nameof(container)); } string contextDirectory; var dockerFilePath = Path.Combine(project.ProjectFile.DirectoryName, "Dockerfile"); TempFile? tempFile = null; TempDirectory?tempDirectory = null; try { // We need to know if this is a single-phase or multi-phase Dockerfile because the context directory will be // different depending on that choice. // // For the cases where generate a Dockerfile, we have the constraint that we need // to place it on the same drive (Windows) as the docker context. if (container.UseMultiphaseDockerfile ?? true) { // For a multi-phase Docker build, the context is always the project directory. contextDirectory = "."; if (File.Exists(dockerFilePath)) { output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'."); } else { // We need to write the file, let's stick it under obj. Directory.CreateDirectory(project.IntermediateOutputPath); dockerFilePath = Path.Combine(project.IntermediateOutputPath, "Dockerfile"); // Clean up file when done building image tempFile = new TempFile(dockerFilePath); await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, tempFile.FilePath); } } else { // For a single-phase Docker build the context is always the directory containing the publish // output. We need to put the Dockerfile in the context directory so it's on the same drive (Windows). var publishOutput = project.Outputs.OfType <ProjectPublishOutput>().FirstOrDefault(); if (publishOutput is null) { throw new InvalidOperationException("We should have published the project for a single-phase Dockerfile."); } contextDirectory = publishOutput.Directory.FullName; // Clean up directory when done building image tempDirectory = new TempDirectory(publishOutput.Directory); if (File.Exists(dockerFilePath)) { output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'."); File.Copy(dockerFilePath, Path.Combine(contextDirectory, "Dockerfile")); dockerFilePath = Path.Combine(contextDirectory, "Dockerfile"); } else { // No need to clean up, it's in a directory we're already cleaning up. dockerFilePath = Path.Combine(contextDirectory, "Dockerfile"); await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, dockerFilePath); } } output.WriteDebugLine("Running 'docker build'."); output.WriteCommandLine("docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\""); var capture = output.Capture(); var exitCode = await Process.ExecuteAsync( $"docker", $"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"", project.ProjectFile.DirectoryName, stdOut : capture.StdOut, stdErr : capture.StdErr); output.WriteDebugLine($"Done running 'docker build' exit code: {exitCode}"); if (exitCode != 0) { throw new CommandException("'docker build' failed."); } output.WriteInfoLine($"Created Docker Image: '{container.ImageName}:{container.ImageTag}'"); project.Outputs.Add(new DockerImageOutput(container.ImageName !, container.ImageTag !)); } finally { tempDirectory?.Dispose(); tempFile?.Dispose(); } }