Tye is a tool that makes developing, testing, and deploying microservices and distributed applications easier. Project Tye includes a local orchestrator to make developing microservices easier and the ability to deploy microservices to Kubernetes with min
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

430 lines
20 KiB

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Tye.ConfigModel;
namespace Microsoft.Tye
{
public static class ApplicationFactory
{
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!);
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 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)
};
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" });
}
else
{
foreach (var configBinding in configService.Bindings)
{
var binding = new BindingBuilder()
{
Name = configBinding.Name,
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 EnvironmentVariableBuilder(configEnvVar.Name) { Value = 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.Name, 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;
root.Ingress.Add(ingress);
foreach (var configBinding in configIngress.Bindings)
{
var binding = new IngressBindingBuilder()
{
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!, // validated elsewhere
};
ingress.Rules.Add(rule);
}
}
}
return root;
}
private static string? GetDockerFileContext(FileInfo source, ConfigService configService)
{
if (configService.DockerFileContext == null)
{
return null;
}
// On windows, calling docker build with an aboslute path that ends in a trailing slash fails,
// but it's the exact opposite on linux, where it needs to have the trailing slash.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return Path.TrimEndingDirectorySeparator(Path.Combine(source.DirectoryName, configService.DockerFileContext));
}
else
{
var path = Path.Combine(source.DirectoryName, configService.DockerFileContext);
if (!Path.EndsInDirectorySeparator(path))
{
return path += Path.DirectorySeparatorChar;
}
return path;
}
}
private static ConfigApplication GetNestedConfig(ConfigApplication rootConfig, string? file)
{
var nestedConfig = ConfigFactory.FromFile(new FileInfo(file));
nestedConfig.Validate();
if (nestedConfig.Name != rootConfig.Name)
{
throw new CommandException($"Nested configuration must have the same \"name\" in the tye.yaml. Root config: {rootConfig.Source}, nested config: {nestedConfig.Source}");
}
return nestedConfig;
}
private static void AddToRootServices(ApplicationBuilder root, HashSet<string> dependencies, string serviceName)
{
// Add ourselves in the set of all current dependencies.
dependencies.Add(serviceName);
// Iterate through all services and add the current services as a dependency (except ourselves)
foreach (var s in root.Services)
{
if (dependencies.Contains(s.Name, StringComparer.OrdinalIgnoreCase)
&& !s.Name.Equals(serviceName, StringComparison.OrdinalIgnoreCase))
{
s.Dependencies.Add(serviceName);
}
}
}
private static ProbeBuilder GetProbeBuilder(ConfigProbe config) => new ProbeBuilder()
{
Http = config.Http != null ? GetHttpProberBuilder(config.Http) : null,
InitialDelay = config.InitialDelay,
Period = config.Period,
Timeout = config.Timeout,
SuccessThreshold = config.SuccessThreshold,
FailureThreshold = config.FailureThreshold
};
private static HttpProberBuilder GetHttpProberBuilder(ConfigHttpProber config) => new HttpProberBuilder()
{
Path = config.Path,
Headers = config.Headers,
Port = config.Port,
Protocol = config.Protocol
};
}
}