public abstract Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service);
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.Instance.IsKubectlInstalled.Value) { throw new CommandException($"Cannot validate ingress because kubectl is not installed."); } if (!await KubectlDetector.Instance.IsKubectlConnectedToCluster.Value) { throw new CommandException($"Cannot validate ingress because kubectl is not connected to a cluster."); } output.WriteDebugLine($"Validating ingress class '{ingressClass}'."); 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); // 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 requirments 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 Process.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. output.WriteDebugLine($"Running 'kubectl apply'"); output.WriteCommandLine("kubectl", $"apply -f \"https://aka.ms/tye/ingress/deploy\""); var capture = output.Capture(); var exitCode = await Process.ExecuteAsync( $"kubectl", $"apply -f \"https://aka.ms/tye/ingress/deploy\"", 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($"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/extensions/v1beta1/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 static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source) { if (source is null) { throw new ArgumentNullException(nameof(source)); } var queue = new Queue <(ConfigApplication, HashSet <string>)>(); var visited = new HashSet <string>(StringComparer.OrdinalIgnoreCase); var rootConfig = ConfigFactory.FromFile(source); rootConfig.Validate(); var root = new ApplicationBuilder(source, rootConfig.Name ?? source.Directory.Name.ToLowerInvariant()); root.Namespace = rootConfig.Namespace; queue.Enqueue((rootConfig, new HashSet <string>())); while (queue.Count > 0) { var item = queue.Dequeue(); var config = item.Item1; // dependencies represents a set of all dependencies var dependencies = item.Item2; if (!visited.Add(config.Source.FullName)) { continue; } if (config == rootConfig && !string.IsNullOrEmpty(config.Registry)) { root.Registry = new ContainerRegistry(config.Registry); } if (config == rootConfig) { root.Network = rootConfig.Network; } foreach (var configExtension in config.Extensions) { var extension = new ExtensionConfiguration((string)configExtension["name"]); foreach (var kvp in configExtension) { if (kvp.Key == "name") { continue; } extension.Data.Add(kvp.Key, kvp.Value); } root.Extensions.Add(extension); } foreach (var configService in config.Services) { ServiceBuilder service; if (root.Services.Any(s => s.Name == configService.Name)) { // Even though this service has already created a service, we still need // to update dependency information AddToRootServices(root, dependencies, configService.Name); continue; } if (!string.IsNullOrEmpty(configService.Project)) { var expandedProject = Environment.ExpandEnvironmentVariables(configService.Project); var projectFile = new FileInfo(Path.Combine(config.Source.DirectoryName, expandedProject)); var project = new ProjectServiceBuilder(configService.Name !, projectFile); service = project; project.Build = configService.Build ?? true; project.Args = configService.Args; foreach (var buildProperty in configService.BuildProperties) { project.BuildProperties.Add(buildProperty.Name, buildProperty.Value); } project.Replicas = configService.Replicas ?? 1; await ProjectReader.ReadProjectDetailsAsync(output, project); // We don't apply more container defaults here because we might need // to prompt for the registry name. project.ContainerInfo = new ContainerInfo() { UseMultiphaseDockerfile = false, }; // Do k8s by default. project.ManifestInfo = new KubernetesManifestInfo(); } else if (!string.IsNullOrEmpty(configService.Image) || !string.IsNullOrEmpty(configService.DockerFile)) { var container = new ContainerServiceBuilder(configService.Name !, configService.Image !) { Args = configService.Args, Replicas = configService.Replicas ?? 1, DockerFile = configService.DockerFile != null?Path.Combine(source.DirectoryName, configService.DockerFile) : null, DockerFileContext = configService.DockerFileContext != null?Path.Combine(source.DirectoryName, configService.DockerFileContext) : null }; 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(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; } else if (!string.IsNullOrEmpty(configService.Include)) { var expandedYaml = Environment.ExpandEnvironmentVariables(configService.Include); var nestedConfig = GetNestedConfig(rootConfig, Path.Combine(config.Source.DirectoryName, expandedYaml)); queue.Enqueue((nestedConfig, new HashSet <string>())); AddToRootServices(root, dependencies, configService.Name); continue; } else if (!string.IsNullOrEmpty(configService.Repository)) { // clone to .tye folder var path = Path.Join(rootConfig.Source.DirectoryName, ".tye", "deps"); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } var clonePath = Path.Combine(path, configService.Name); if (!Directory.Exists(clonePath)) { if (!await GitDetector.Instance.IsGitInstalled.Value) { throw new CommandException($"Cannot clone repository {configService.Repository} because git is not installed. Please install git if you'd like to use \"repository\" in tye.yaml."); } var result = await ProcessUtil.RunAsync("git", $"clone {configService.Repository} {clonePath}", workingDirectory : path, throwOnError : false); if (result.ExitCode != 0) { throw new CommandException($"Failed to clone repository {configService.Repository} with exit code {result.ExitCode}.{Environment.NewLine}{result.StandardError}{result.StandardOutput}."); } } if (!ConfigFileFinder.TryFindSupportedFile(clonePath, out var file, out var errorMessage)) { throw new CommandException(errorMessage !); } // pick different service type based on what is in the repo. var nestedConfig = GetNestedConfig(rootConfig, file); queue.Enqueue((nestedConfig, new HashSet <string>())); AddToRootServices(root, dependencies, configService.Name); continue; } else if (configService.External) { var external = new ExternalServiceBuilder(configService.Name !); service = external; } else { throw new CommandException("Unable to determine service type."); } // Add dependencies to ourself before adding ourself to avoid self reference service.Dependencies.UnionWith(dependencies); AddToRootServices(root, dependencies, service.Name); root.Services.Add(service); // If there are no bindings and we're in ASP.NET Core project then add an HTTP and HTTPS binding if (configService.Bindings.Count == 0 && service is ProjectServiceBuilder project2 && project2.IsAspNet) { // HTTP is the default binding service.Bindings.Add(new BindingBuilder() { Protocol = "http" }); service.Bindings.Add(new BindingBuilder() { Name = "https", Protocol = "https" }); }
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 projectFile = new FileInfo(Path.Combine(builder.Source.DirectoryName, configService.Project)); 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); container.Args = configService.Args; container.Replicas = configService.Replicas ?? 1; service = container; } else if (!string.IsNullOrEmpty(configService.Executable)) { var executable = new ExecutableServiceBuilder(configService.Name, configService.Executable); executable.Args = configService.Args; executable.WorkingDirectory = configService.WorkingDirectory; executable.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, }; if (binding.ConnectionString == 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); }
private static Command CreateRunCommand() { var command = new Command("run", "run the application") { CommonArguments.Path_Required, new Option("--no-build") { Description = "Do not build project files before running.", Required = false }, new Option("--port") { Description = "The port to run control plane on.", Argument = new Argument <int?>("port"), Required = false }, new Option("--logs") { Description = "Write structured application logs to the specified log provider. Supported providers are 'console', 'elastic' (Elasticsearch), 'ai' (ApplicationInsights), 'seq'.", Argument = new Argument <string>("logs"), Required = false }, new Option("--dtrace") { Description = "Write distributed traces to the specified tracing provider. Supported providers are 'zipkin'.", Argument = new Argument <string>("trace"), Required = false, }, new Option("--metrics") { Description = "Write metrics to the specified metrics provider.", Argument = new Argument <string>("metrics"), Required = false }, new Option("--debug") { Argument = new Argument <string[]>("service") { Arity = ArgumentArity.ZeroOrMore, }, Description = "Wait for debugger attach to specific service. Specify \"*\" to wait for all services.", Required = false }, new Option("--docker") { Description = "Run projects as docker containers.", Required = false }, new Option("--dashboard") { Description = "Launch dashboard on run.", Required = false }, StandardOptions.Verbosity, }; command.Handler = CommandHandler.Create <RunCommandArguments>(async args => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (args.Path is null) { throw new CommandException("No project or solution file was found."); } var output = new OutputContext(args.Console, Verbosity.Info); output.WriteInfoLine("Loading Application Details..."); var application = await ApplicationFactory.CreateAsync(output, args.Path); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); } var options = new HostOptions() { Dashboard = args.Dashboard, Docker = args.Docker, NoBuild = args.NoBuild, Port = args.Port, // parsed later by the diagnostics code DistributedTraceProvider = args.Dtrace, LoggingProvider = args.Logs, MetricsProvider = args.Metrics, }; options.Debug.AddRange(args.Debug); await application.ProcessExtensionsAsync(options, output, ExtensionContext.OperationKind.LocalRun); InitializeThreadPoolSettings(application.Services.Count); output.WriteInfoLine("Launching Tye Host..."); output.WriteInfoLine(string.Empty); await using var host = new TyeHost(application.ToHostingApplication(), options); await host.RunAsync(); }); return(command); }
public static async Task <KubernetesIngressOutput> CreateIngress( OutputContext output, ApplicationBuilder application, IngressBuilder ingress) { var root = new YamlMappingNode(); var k8sVersion = await KubectlDetector.GetKubernetesServerVersion(output); root.Add("kind", "Ingress"); if (k8sVersion >= MinIngressVersion) { root.Add("apiVersion", "networking.k8s.io/v1"); } else { root.Add("apiVersion", "extensions/v1beta1"); } var metadata = new YamlMappingNode(); root.Add("metadata", metadata); metadata.Add("name", ingress.Name); if (!string.IsNullOrEmpty(application.Namespace)) { metadata.Add("namespace", application.Namespace); } var annotations = new YamlMappingNode(); metadata.Add("annotations", annotations); annotations.Add("kubernetes.io/ingress.class", new YamlScalarNode("nginx") { Style = ScalarStyle.SingleQuoted, }); annotations.Add("nginx.ingress.kubernetes.io/rewrite-target", new YamlScalarNode("/$2") { Style = ScalarStyle.SingleQuoted, }); var labels = new YamlMappingNode(); metadata.Add("labels", labels); labels.Add("app.kubernetes.io/part-of", new YamlScalarNode(application.Name) { Style = ScalarStyle.SingleQuoted, }); var spec = new YamlMappingNode(); root.Add("spec", spec); if (ingress.Rules.Count == 0) { return(new KubernetesIngressOutput(ingress.Name, new YamlDocument(root))); } var rules = new YamlSequenceNode(); spec.Add("rules", rules); // k8s ingress is grouped by host first, then grouped by path foreach (var hostgroup in ingress.Rules.GroupBy(r => r.Host)) { var rule = new YamlMappingNode(); rules.Add(rule); if (!string.IsNullOrEmpty(hostgroup.Key)) { rule.Add("host", hostgroup.Key); } var http = new YamlMappingNode(); rule.Add("http", http); var paths = new YamlSequenceNode(); http.Add("paths", paths); foreach (var ingressRule in hostgroup) { var path = new YamlMappingNode(); paths.Add(path); var backend = new YamlMappingNode(); path.Add("backend", backend); var service = application.Services.FirstOrDefault(s => s.Name == ingressRule.Service); if (service is null) { throw new InvalidOperationException($"Could not resolve service '{ingressRule.Service}'."); } var binding = service.Bindings.FirstOrDefault(b => b.Name is null || b.Name == "http"); if (binding is null) { throw new InvalidOperationException($"Could not resolve an http binding for service '{service.Name}'."); } if (k8sVersion >= MinIngressVersion) { var backendService = new YamlMappingNode(); backend.Add("service", backendService); backendService.Add("name", ingressRule.Service); var backendPort = new YamlMappingNode(); backendService.Add("port", backendPort); backendPort.Add("number", (binding.Port ?? 80).ToString(CultureInfo.InvariantCulture)); path.Add("pathType", "Prefix"); } else { backend.Add("serviceName", ingressRule.Service); backend.Add("servicePort", (binding.Port ?? 80).ToString(CultureInfo.InvariantCulture)); } // Tye implements path matching similar to this example: // https://kubernetes.github.io/ingress-nginx/examples/rewrite/ // // Therefore our rewrite-target is set to $2 - we want to make sure we have // two capture groups. if (string.IsNullOrEmpty(ingressRule.Path) || ingressRule.Path == "/") { path.Add("path", "/()(.*)"); // () is an empty capture group. } else { if (ingressRule.PreservePath) { path.Add("path", $"/()({ingressRule.Path.Trim('/')}.*)"); } else { var regex = $"{ingressRule.Path.TrimEnd('/')}(/|$)(.*)"; path.Add("path", regex); } } // Only support prefix matching for now } } return(new KubernetesIngressOutput(ingress.Name, new YamlDocument(root))); }
public ApplicationExecutor(OutputContext output) { this.output = output; }
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 !) { Namespace = rootConfig.Namespace }; queue.Enqueue((rootConfig, new HashSet <string>())); while (queue.TryDequeue(out var item)) { // dependencies represents a set of all dependencies var(config, dependencies) = item; if (!visited.Add(config.Source.FullName)) { continue; } if (config == rootConfig && !string.IsNullOrEmpty(config.Registry)) { root.Registry = new ContainerRegistry(config.Registry); } if (config == rootConfig) { root.Network = rootConfig.Network; } foreach (var configExtension in config.Extensions) { var extension = new ExtensionConfiguration((string)configExtension["name"]); foreach (var kvp in configExtension) { if (kvp.Key == "name") { continue; } extension.Data.Add(kvp.Key, kvp.Value); } root.Extensions.Add(extension); } var services = filter?.ServicesFilter != null? config.Services.Where(filter.ServicesFilter).ToList() : config.Services; var sw = Stopwatch.StartNew(); // Project services will be restored and evaluated before resolving all other services. // This batching will mitigate the performance cost of running MSBuild out of process. var projectServices = services.Where(s => !string.IsNullOrEmpty(s.Project)); var projectMetadata = new Dictionary <string, string>(); var msbuildEvaluationResult = await EvaluateProjectsAsync( projects : projectServices, configRoot : config.Source.DirectoryName !, output : output); var msbuildEvaluationOutput = msbuildEvaluationResult .StandardOutput .Split(Environment.NewLine); var multiTFMProjects = new List <ConfigService>(); foreach (var line in msbuildEvaluationOutput) { var trimmed = line.Trim(); if (trimmed.StartsWith("Microsoft.Tye metadata: ")) { var values = line.Split(':', 3); var projectName = values[1].Trim(); var metadataPath = values[2].Trim(); projectMetadata.Add(projectName, metadataPath); output.WriteDebugLine($"Resolved metadata for service {projectName} at {metadataPath}"); } else if (trimmed.StartsWith("Microsoft.Tye cross-targeting project: ")) { var values = line.Split(':', 2); var projectName = values[1].Trim(); var multiTFMConfigService = projectServices.First(p => string.Equals(p.Name, projectName, StringComparison.OrdinalIgnoreCase)); multiTFMConfigService.BuildProperties.Add(new BuildProperty { Name = "TargetFramework", Value = framework ?? string.Empty }); multiTFMProjects.Add(multiTFMConfigService); } } if (multiTFMProjects.Any()) { output.WriteDebugLine("Re-evaluating multi-targeted projects"); var multiTFMEvaluationResult = await EvaluateProjectsAsync( projects : multiTFMProjects, configRoot : config.Source.DirectoryName !, output : output); var multiTFMEvaluationOutput = multiTFMEvaluationResult .StandardOutput .Split(Environment.NewLine); foreach (var line in multiTFMEvaluationOutput) { var trimmed = line.Trim(); if (trimmed.StartsWith("Microsoft.Tye metadata: ")) { var values = line.Split(':', 3); var projectName = values[1].Trim(); var metadataPath = values[2].Trim(); projectMetadata.Add(projectName, metadataPath); output.WriteDebugLine($"Resolved metadata for service {projectName} at {metadataPath}"); } else if (trimmed.StartsWith("Microsoft.Tye cross-targeting project: ")) { var values = line.Split(':', 2); var projectName = values[1].Trim(); throw new CommandException($"Unable to run {projectName}. Your project targets multiple frameworks. Specify which framework to run using '--framework' or a build property in tye.yaml."); } } } output.WriteDebugLine($"Restore and project evaluation took: {sw.Elapsed.TotalMilliseconds}ms"); foreach (var configService in services) { ServiceBuilder service; if (root.Services.Any(s => s.Name == configService.Name)) { // Even though this service has already created a service, we still need // to update dependency information AddToRootServices(root, dependencies, configService.Name); continue; } if (!string.IsNullOrEmpty(configService.Project)) { 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 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 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); } builder.Extensions.Add(extension); } 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 prompt for the registry name. project.ContainerInfo = new ContainerInfo() { UseMultiphaseDockerfile = false, }; // 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; } 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.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) { ingress.Outputs.Add(await KubernetesManifestGenerator.CreateIngress(output, application, ingress)); }
public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application) { using var step = output.BeginStep("Applying Kubernetes Manifests..."); if (await KubectlDetector.GetKubernetesServerVersion(output) == null) { throw new CommandException($"Cannot apply manifests because kubectl is not installed."); } if (!await KubectlDetector.IsKubectlConnectedToClusterAsync(output)) { throw new CommandException($"Cannot apply manifests because kubectl is not connected to a cluster."); } using var tempFile = TempFile.Create(); output.WriteInfoLine($"Writing output to '{tempFile.FilePath}'."); { await using var stream = File.OpenWrite(tempFile.FilePath); await using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), leaveOpen: true); await ApplicationYamlWriter.WriteAsync(output, writer, application); } var ns = $"namespace ${application.Namespace}"; if (string.IsNullOrEmpty(application.Namespace)) { ns = "current namespace"; } output.WriteDebugLine($"Running 'kubectl apply' in ${ns}"); output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\""); var capture = output.Capture(); var exitCode = await ProcessUtil.ExecuteAsync( $"kubectl", $"apply -f \"{tempFile.FilePath}\"", System.Environment.CurrentDirectory, stdOut : capture.StdOut, stdErr : capture.StdErr); output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}"); if (exitCode != 0) { throw new CommandException("'kubectl apply' failed."); } output.WriteInfoLine($"Deployed application '{application.Name}'."); if (application.Ingress.Count > 0) { output.WriteInfoLine($"Waiting for ingress to be deployed. This may take a long time."); foreach (var ingress in application.Ingress) { using var ingressStep = output.BeginStep($"Retrieving details for {ingress.Name}..."); var done = false; Action <string> complete = line => { done = line != "''"; if (done) { output.WriteInfoLine($"IngressIP: {line}"); } }; var retries = 0; while (!done && retries < 60) { var ingressExitCode = await ProcessUtil.ExecuteAsync( "kubectl", $"get ingress {ingress.Name} -o jsonpath='{{..ip}}'", Environment.CurrentDirectory, complete, capture.StdErr); if (ingressExitCode != 0) { throw new CommandException("'kubectl get ingress' failed"); } if (!done) { await Task.Delay(2000); retries++; } } } } }
public static async Task ExecuteUndeployAsync(OutputContext output, ApplicationBuilder 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)); } }
// For layering reasons this has to live in the `tye` project. We don't want to leak // the extensions themselves into Tye.Core. public static async Task ProcessExtensionsAsync(this ApplicationBuilder application, OutputContext output, ExtensionContext.OperationKind operation) { foreach (var extensionConfig in application.Extensions) { if (!WellKnownExtensions.Extensions.TryGetValue(extensionConfig.Name, out var extension)) { throw new CommandException($"Could not find the extension '{extensionConfig.Name}'."); } var context = new ExtensionContext(application, output, operation); await extension.ProcessAsync(context, extensionConfig); } }
private static async Task ExecuteDeployAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive, bool force) { var watch = System.Diagnostics.Stopwatch.StartNew(); 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."); } await application.ProcessExtensionsAsync(options : null, output, ExtensionContext.OperationKind.Deploy); ApplyRegistry(output, application, interactive, requireRegistry: true); var executor = new ApplicationExecutor(output) { ServiceSteps = { new ApplyContainerDefaultsStep(), new CombineStep() { Environment = environment, }, new PublishProjectStep(), new BuildDockerImageStep() { Environment = environment, }, new PushDockerImageStep() { Environment = environment, }, new ValidateSecretStep() { Environment = environment, Interactive= interactive, Force = force, }, new GenerateServiceKubernetesManifestStep() { Environment = environment, }, }, IngressSteps = { new ValidateIngressStep() { Environment = environment, Interactive = interactive, Force = force, }, new GenerateIngressKubernetesManifestStep(), }, ApplicationSteps = { new DeployApplicationKubernetesManifestStep(), } }; await executor.ExecuteAsync(application); watch.Stop(); TimeSpan elapsedTime = watch.Elapsed; output.WriteAlwaysLine($"Time Elapsed: {elapsedTime.Hours:00}:{elapsedTime.Minutes:00}:{elapsedTime.Seconds:00}:{elapsedTime.Milliseconds / 10:00}"); }
public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application);
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(); } }
public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application, IngressBuilder ingres);
private static Command CreateRunCommand(string[] args) { var command = new Command("run", "run the application") { CommonArguments.Path_Required, }; // TODO: We'll need to support a --build-args command.AddOption(new Option("--no-build") { Description = "Do not build project files before running.", Required = false }); command.AddOption(new Option("--port") { Description = "The port to run control plane on.", Argument = new Argument <int>("port"), Required = false }); command.AddOption(new Option("--logs") { Description = "Write structured application logs to the specified log providers. Supported providers are console, elastic (Elasticsearch), ai (ApplicationInsights), seq.", Argument = new Argument <string>("logs"), Required = false }); command.AddOption(new Option("--dtrace") { Description = "Write distributed traces to the specified providers. Supported providers are zipkin.", Argument = new Argument <string>("logs"), Required = false }); command.AddOption(new Option("--debug") { Argument = new Argument <string[]>("service"), Description = "Wait for debugger attach to specific service. Specify \"*\" to wait for all services.", Required = false }); command.AddOption(new Option("--docker") { Description = "Run projects as docker containers.", Required = false }); command.Handler = CommandHandler.Create <IConsole, FileInfo, string[]>(async(console, path, debug) => { // Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654 if (path is null) { throw new CommandException("No project or solution file was found."); } var output = new OutputContext(console, Verbosity.Quiet); var application = await ApplicationFactory.CreateAsync(output, path); await application.ProcessExtensionsAsync(ExtensionContext.OperationKind.LocalRun); InitializeThreadPoolSettings(application.Services.Count); if (application.Services.Count == 0) { throw new CommandException($"No services found in \"{application.Source.Name}\""); } using var host = new TyeHost(application.ToHostingApplication(), args, debug); await host.RunAsync(); }); return(command); }
public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service);
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) { continue; } 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.ContainerPort ?? binding.Port.Value).ToString()); } return(new KubernetesServiceOutput(project.Name, new YamlDocument(root))); }
public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service) { // No need to do this computation for a non-project since we're not deploying it. if (!(service is ProjectServiceBuilder project)) { return(Task.CompletedTask); } // Compute ASPNETCORE_URLS based on the bindings exposed by *this* project. foreach (var binding in service.Bindings) { if (binding.Protocol == null && binding.ConnectionString == null) { binding.Protocol = "http"; } if (binding.Port == null && binding.Protocol == "http") { binding.Port = 80; } if (binding.Protocol == "http") { var port = binding.Port ?? 80; var urls = $"http://*{(port == 80 ? "" : (":" + port.ToString()))}"; project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("ASPNETCORE_URLS") { Value = urls, }); project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("PORT") { Value = port.ToString(CultureInfo.InvariantCulture), }); break; } } // Process bindings and turn them into environment variables and secrets. There's // some duplication with the code in m8s (Application.cs) for populating environments. // // service.Service.Bindings is the bindings OUT - this step computes bindings IN. var bindings = new ComputedBindings(); service.Outputs.Add(bindings); foreach (var other in application.Services) { if (object.ReferenceEquals(service, other)) { continue; } foreach (var binding in other.Bindings) { // The other thing is a project, and will be deployed along with this // service. var configName = (binding.Name is null || binding.Name == other.Name) ? other.Name.ToUpperInvariant() : $"{other.Name.ToUpperInvariant()}__{binding.Name.ToUpperInvariant()}"; if (other is ProjectServiceBuilder) { if (!string.IsNullOrEmpty(binding.ConnectionString)) { // Special case for connection strings bindings.Bindings.Add(new EnvironmentVariableInputBinding($"CONNECTIONSTRING__{configName}", binding.ConnectionString)); continue; } 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.Protocol == null) { binding.Protocol = "http"; } if (binding.Port == null && binding.Protocol == "http") { binding.Port = 80; } if (!string.IsNullOrEmpty(binding.Protocol)) { bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PROTOCOL", binding.Protocol)); } if (binding.Port != null) { bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PORT", binding.Port.Value.ToString())); } bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__HOST", binding.Host ?? other.Name)); } else { // The other service is not a project, so we'll use secrets. bindings.Bindings.Add(new SecretInputBinding( name: $"binding-{Environment}-{other.Name}-{binding.Name ?? other.Name}-secret", filename: $"CONNECTIONSTRING__{configName}", other, binding)); } } } return(Task.CompletedTask); }
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 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 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.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))); }
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, 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); }
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 ServiceOutput 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 (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))); }
private static async Task ExecuteDeployAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive, bool force) { if (!await KubectlDetector.Instance.IsKubectlInstalled.Value) { throw new CommandException($"Cannot apply manifests because kubectl is not installed."); } if (!await KubectlDetector.Instance.IsKubectlConnectedToCluster.Value) { throw new CommandException($"Cannot apply manifests because kubectl is not connected to a cluster."); } await application.ProcessExtensionsAsync(options : null, output, ExtensionContext.OperationKind.Deploy); ApplyRegistry(output, application, interactive, requireRegistry: true); var executor = new ApplicationExecutor(output) { ServiceSteps = { new ApplyContainerDefaultsStep(), new CombineStep() { Environment = environment, }, new PublishProjectStep(), new BuildDockerImageStep() { Environment = environment, }, new PushDockerImageStep() { Environment = environment, }, new ValidateSecretStep() { Environment = environment, Interactive= interactive, Force = force, }, new GenerateServiceKubernetesManifestStep() { Environment = environment, }, }, IngressSteps = { new ValidateIngressStep() { Environment = environment, Interactive = interactive, Force = force, }, new GenerateIngressKubernetesManifestStep(), }, ApplicationSteps = { new DeployApplicationKubernetesManifestStep(), } }; await executor.ExecuteAsync(application); }