public static async Task <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source, ApplicationFactoryFilter?filter = null) { if (source is null) { throw new ArgumentNullException(nameof(source)); } var queue = new Queue <(ConfigApplication, HashSet <string>)>(); var visited = new HashSet <string>(StringComparer.OrdinalIgnoreCase); var rootConfig = ConfigFactory.FromFile(source); rootConfig.Validate(); var root = new ApplicationBuilder(source, rootConfig.Name !); root.Namespace = rootConfig.Namespace; queue.Enqueue((rootConfig, new HashSet <string>())); while (queue.Count > 0) { var item = queue.Dequeue(); var config = item.Item1; // dependencies represents a set of all dependencies var dependencies = item.Item2; if (!visited.Add(config.Source.FullName)) { continue; } if (config == rootConfig && !string.IsNullOrEmpty(config.Registry)) { root.Registry = new ContainerRegistry(config.Registry); } if (config == rootConfig) { root.Network = rootConfig.Network; } foreach (var configExtension in config.Extensions) { var extension = new ExtensionConfiguration((string)configExtension["name"]); foreach (var kvp in configExtension) { if (kvp.Key == "name") { continue; } extension.Data.Add(kvp.Key, kvp.Value); } root.Extensions.Add(extension); } var services = filter?.ServicesFilter != null? config.Services.Where(filter.ServicesFilter).ToList() : config.Services; 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 expandedProject = Environment.ExpandEnvironmentVariables(configService.Project); var projectFile = new FileInfo(Path.Combine(config.Source.DirectoryName, expandedProject)); var project = new DotnetProjectServiceBuilder(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; 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, }; await ProjectReader.ReadProjectDetailsAsync(output, project); // 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 = 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 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 <ApplicationBuilder> CreateAsync(OutputContext output, FileInfo source) { if (source is null) { throw new ArgumentNullException(nameof(source)); } var config = ConfigFactory.FromFile(source); config.Validate(); var builder = new ApplicationBuilder(source, config.Name ?? source.Directory.Name.ToLowerInvariant()); if (!string.IsNullOrEmpty(config.Registry)) { builder.Registry = new ContainerRegistry(config.Registry); } builder.Network = config.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); } 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; 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)) { 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 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); }
internal static async Task<TemporaryApplicationAdapter> CreateApplicationAdapterAsync(OutputContext output, ConfigApplication application, bool interactive, bool requireRegistry) { var globals = new ApplicationGlobals() { Name = application.Name, Registry = application.Registry is null ? null : new ContainerRegistry(application.Registry), }; var services = new List<Tye.ServiceEntry>(); foreach (var configService in application.Services) { if (configService.Project is string projectFile) { var fullPathProjectFile = Path.Combine(application.Source.DirectoryName, projectFile); var project = new Project(fullPathProjectFile); var service = new Service(configService.Name) { Source = project, }; service.Replicas = configService.Replicas ?? 1; foreach (var configBinding in configService.Bindings) { var binding = new ServiceBinding(configBinding.Name ?? service.Name) { ConnectionString = configBinding.ConnectionString, Host = configBinding.Host, Port = configBinding.Port, Protocol = configBinding.Protocol, }; binding.Protocol ??= "http"; if (binding.Port == null && configBinding.AutoAssignPort) { if (binding.Protocol == "http" || binding.Protocol == null) { binding.Port = 80; } else if (binding.Protocol == "https") { binding.Port = 443; } } service.Bindings.Add(binding); } var serviceEntry = new ServiceEntry(service, configService.Name); await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(fullPathProjectFile), project); var container = new ContainerInfo() { UseMultiphaseDockerfile = false, }; service.GeneratedAssets.Container = container; services.Add(serviceEntry); } else { // For a non-project, we don't really need much info about it, just the name and bindings var service = new Service(configService.Name); foreach (var configBinding in configService.Bindings) { service.Bindings.Add(new ServiceBinding(configBinding.Name ?? service.Name) { ConnectionString = configBinding.ConnectionString, Host = configBinding.Host, Port = configBinding.Port, Protocol = configBinding.Protocol, }); } var serviceEntry = new ServiceEntry(service, configService.Name); services.Add(serviceEntry); } } var temporaryApplication = new TemporaryApplicationAdapter(application, globals, services); if (temporaryApplication.Globals.Registry?.Hostname == null && interactive) { var registry = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)", allowEmpty: !requireRegistry); if (!string.IsNullOrWhiteSpace(registry)) { temporaryApplication.Globals.Registry = new ContainerRegistry(registry.Trim()); } } else if (temporaryApplication.Globals.Registry?.Hostname == null && requireRegistry) { throw new CommandException("A registry is required for deploy operations. Add the registry to 'tye.yaml' or use '-i' for interactive mode."); } else { // No registry specified, and that's OK! } foreach (var service in temporaryApplication.Services) { if (service.Service.Source is Project project && service.Service.GeneratedAssets.Container is ContainerInfo container) { DockerfileGenerator.ApplyContainerDefaults(temporaryApplication, service, project, container); } } return temporaryApplication; }
public override async Task <object?> ExecuteAsync() { if (callbacks.Count == 0) { return(Task.FromResult <object?>(null)); } if (callbacks.Count > 1) { throw new InvalidOperationException("More than one application type is not supported."); } var kvp = callbacks.Single(); var type = kvp.Key; var delegates = kvp.Value; output.WriteDebugLine($"Creating instance of application type '{type}'."); var application = Activator.CreateInstance(type); output.WriteDebugLine($"Done creating instance of application type '{type}'."); var wrapper = new ApplicationWrapper(application !, rootDirectory); wrapper.Globals.Name ??= name; foreach (var service in wrapper.Services) { output.WriteDebugLine($"Found service '{service.FriendlyName} {{ Name: {service.Service.Name} }}'."); string?projectRelativeFilePath = null; string?projectFilePath = null; if (solution != null) { var project = FindProjectInSolution(solution, service.FriendlyName); if (project == null) { output.WriteDebugLine($"Could not find project for service '{service.FriendlyName}'."); continue; } output.WriteDebugLine($"Found project '{project.RelativePath}' for service '{service.FriendlyName}'."); projectRelativeFilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar); projectFilePath = project.AbsolutePath.Replace('\\', Path.DirectorySeparatorChar); } else if (projectFile != null) { var normalized = Names.NormalizeToFriendly(Path.GetFileNameWithoutExtension(projectFile.Name)); if (!string.Equals(normalized, service.FriendlyName)) { output.WriteDebugLine($"Skipping service '{service.FriendlyName}'."); continue; } projectRelativeFilePath = projectFile.FullName; projectFilePath = projectFile.FullName; } if (projectFilePath != null) { var project = new Project(projectRelativeFilePath !); await ProjectReader.ReadProjectDetailsAsync(output, new FileInfo(projectFilePath), project); service.Service.Source = project; // Apply defaults to everything that has a project. var container = new ContainerInfo(); service.Service.GeneratedAssets.Container = container; DockerfileGenerator.ApplyContainerDefaults(wrapper, service, project, container); } } output.WriteDebugLine($"Running {delegates.Count} customization callbacks."); for (var i = 0; i < delegates.Count; i++) { delegates[i].DynamicInvoke(application); } output.WriteDebugLine($"Done running {delegates.Count} customization callbacks."); return(application); }