Browse Source

Refactor of data model

Merged the data model we use for m8s features with the features
that come from rynowak/opulence. The `ApplicationBuilder` type (and related
types) now capture all of the details we use for everything.

There's a lot of just moving stuff around in this change - the goal
is to have a separate data model for:

- The config we read
- The representation we mutate
- The data model used by Tye.Hosting

So in this PR there's a lot of righthand/lefthand.
pull/182/head
Ryan Nowak 7 years ago
parent
commit
e75f226e2d
  1. 231
      src/Microsoft.Tye.Core/Application.cs
  2. 28
      src/Microsoft.Tye.Core/ApplicationBuilder.cs
  3. 257
      src/Microsoft.Tye.Core/ApplicationFactory.cs
  4. 2
      src/Microsoft.Tye.Core/ApplicationYamlWriter.cs
  5. 17
      src/Microsoft.Tye.Core/BindingBuilder.cs
  6. 13
      src/Microsoft.Tye.Core/BuildDockerImageStep.cs
  7. 47
      src/Microsoft.Tye.Core/CombineStep.cs
  8. 6
      src/Microsoft.Tye.Core/ComputedBindings.cs
  9. 30
      src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs
  10. 2
      src/Microsoft.Tye.Core/ConfigModel/ConfigConfigurationSource.cs
  11. 13
      src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs
  12. 6
      src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs
  13. 6
      src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs
  14. 4
      src/Microsoft.Tye.Core/ConfigModel/ConfigIngressRule.cs
  15. 0
      src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs
  16. 0
      src/Microsoft.Tye.Core/ConfigModel/ConfigServiceBinding.cs
  17. 11
      src/Microsoft.Tye.Core/ConfigModel/ConfigVolume.cs
  18. 27
      src/Microsoft.Tye.Core/ContainerServiceBuilder.cs
  19. 163
      src/Microsoft.Tye.Core/CustomizationPipeline.cs
  20. 10
      src/Microsoft.Tye.Core/DeployServiceYamlStep.cs
  21. 17
      src/Microsoft.Tye.Core/DockerContainerBuilder.cs
  22. 31
      src/Microsoft.Tye.Core/DockerImageOutput.cs
  23. 26
      src/Microsoft.Tye.Core/DockerfileGenerator.cs
  24. 19
      src/Microsoft.Tye.Core/EnvironmentVariable.cs
  25. 27
      src/Microsoft.Tye.Core/ExecutableServiceBuilder.cs
  26. 14
      src/Microsoft.Tye.Core/ExternalServiceBuilder.cs
  27. 6
      src/Microsoft.Tye.Core/GenerateDockerfileStep.cs
  28. 5
      src/Microsoft.Tye.Core/GenerateHelmChartStep.cs
  29. 10
      src/Microsoft.Tye.Core/GenerateKubernetesManifestStep.cs
  30. 6
      src/Microsoft.Tye.Core/GenerateOamComponentStep.cs
  31. 13
      src/Microsoft.Tye.Core/HelmChartBuilder.cs
  32. 13
      src/Microsoft.Tye.Core/HelmChartGenerator.cs
  33. 14
      src/Microsoft.Tye.Core/IngressBindingBuilder.cs
  34. 24
      src/Microsoft.Tye.Core/IngressBuilder.cs
  35. 13
      src/Microsoft.Tye.Core/IngressRuleBuilder.cs
  36. 71
      src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs
  37. 14
      src/Microsoft.Tye.Core/OamApplicationGenerator.cs
  38. 14
      src/Microsoft.Tye.Core/OamComponentGenerator.cs
  39. 23
      src/Microsoft.Tye.Core/ProjectReader.cs
  40. 42
      src/Microsoft.Tye.Core/ProjectServiceBuilder.cs
  41. 11
      src/Microsoft.Tye.Core/PublishProjectStep.cs
  42. 7
      src/Microsoft.Tye.Core/PushDockerImageStep.cs
  43. 19
      src/Microsoft.Tye.Core/ServiceBuilder.cs
  44. 37
      src/Microsoft.Tye.Core/ServiceExecutor.cs
  45. 10
      src/Microsoft.Tye.Core/ServiceOutput.cs
  46. 12
      src/Microsoft.Tye.Core/ValidateSecretStep.cs
  47. 19
      src/Microsoft.Tye.Core/VolumeBuilder.cs
  48. 8
      src/Microsoft.Tye.Core/WriteServiceYamlStep.cs
  49. 78
      src/tye/ApplicationBuilderExtensions.cs
  50. 18
      src/tye/BuildHost.cs
  51. 23
      src/tye/GenerateHost.cs
  52. 1
      src/tye/InitHost.cs
  53. 157
      src/tye/Program.DeployCommand.cs
  54. 3
      src/tye/Program.RunCommand.cs
  55. 30
      src/tye/TemporaryApplicationAdapter.cs
  56. 29
      test/E2ETest/TyeBuildTests.cs
  57. 30
      test/E2ETest/TyeGenerateTests.cs
  58. 21
      test/E2ETest/TyeRunTests.cs
  59. 4
      test/E2ETest/testassets/generate/frontend-backend.yaml
  60. 4
      test/E2ETest/testassets/generate/multi-project.yaml
  61. 2
      test/E2ETest/testassets/generate/single-project-noregistry.yaml
  62. 2
      test/E2ETest/testassets/generate/single-project.yaml

231
src/Microsoft.Tye.Core/Application.cs

@ -1,231 +0,0 @@
// 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.IO;
using System.Linq;
using System.Reflection;
namespace Microsoft.Tye
{
public abstract class Application
{
public abstract ApplicationGlobals Globals { get; }
public abstract string RootDirectory { get; }
public abstract IReadOnlyList<ServiceEntry> Services { get; }
public string GetProjectDirectory(Project project)
{
if (project is null)
{
throw new ArgumentNullException(nameof(project));
}
return Path.GetDirectoryName(Path.Combine(RootDirectory, project.RelativeFilePath))!;
}
}
public class ServiceEntry
{
public ServiceEntry(Service service, string friendlyName, IEnumerable<string>? environments = null)
{
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (friendlyName is null)
{
throw new ArgumentNullException(nameof(friendlyName));
}
Service = service;
FriendlyName = friendlyName;
Environments = environments?.ToArray() ?? Array.Empty<string>();
}
public IReadOnlyList<string> Environments { get; }
public string FriendlyName { get; }
public Service Service { get; }
public List<ServiceOutput> Outputs { get; } = new List<ServiceOutput>();
public bool AppliesToEnvironment(string environment)
{
if (environment is null)
{
throw new ArgumentNullException(nameof(environment));
}
return Environments.Count == 0 || Environments.Contains(environment, StringComparer.OrdinalIgnoreCase);
}
public bool IsMatchForProject(Application application, FileInfo projectFile)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (projectFile is null)
{
throw new ArgumentNullException(nameof(projectFile));
}
if (string.Equals(projectFile.Extension, ".sln", StringComparison.Ordinal))
{
return true;
}
if (Service.Source is Project project &&
string.Equals(application.GetProjectDirectory(project), projectFile.DirectoryName, StringComparison.Ordinal))
{
return true;
}
return false;
}
}
public abstract class ServiceOutput
{
}
public class DockerImageOutput : ServiceOutput
{
public DockerImageOutput(string imageName, string imageTag)
{
if (imageName is null)
{
throw new ArgumentNullException(nameof(imageName));
}
if (imageTag is null)
{
throw new ArgumentNullException(nameof(imageTag));
}
ImageName = imageName;
ImageTag = imageTag;
}
public string ImageName { get; }
public string ImageTag { get; }
}
internal class ApplicationWrapper : Application
{
private readonly object inner;
private ApplicationGlobals? globals;
private List<ServiceEntry>? services;
public ApplicationWrapper(object inner, string rootDirectory)
{
if (inner is null)
{
throw new ArgumentNullException(nameof(inner));
}
if (rootDirectory is null)
{
throw new ArgumentNullException(nameof(rootDirectory));
}
this.inner = inner;
RootDirectory = rootDirectory;
}
public override ApplicationGlobals Globals
{
get
{
if (globals == null)
{
var property = inner.GetType().GetProperty("Globals", BindingFlags.Instance | BindingFlags.Public);
if (property != null && property.PropertyType == typeof(ApplicationGlobals))
{
globals = (ApplicationGlobals?)property.GetValue(inner, null);
}
globals ??= new ApplicationGlobals();
}
return globals!;
}
}
public override string RootDirectory { get; }
public override IReadOnlyList<ServiceEntry> Services
{
get
{
if (services == null)
{
services = new List<ServiceEntry>();
var properties = inner.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
for (var i = 0; i < properties.Length; i++)
{
var property = properties[i];
if (property.PropertyType == typeof(Service))
{
var environments = new List<string>();
var environmentAttributes = property.GetCustomAttributes<EnvironmentAttribute>();
environments.AddRange(environmentAttributes.Select(e => e.EnvironmentName));
var value = property.GetValue(inner, null);
if (value == null)
{
throw new InvalidOperationException("Service properties must return a non-null value.");
}
services.Add(new ServiceEntry((Service)value, property.Name, environments));
}
}
}
return services;
}
}
}
internal class GroveledApplication : Application
{
public GroveledApplication(ApplicationGlobals globals, string rootDirectory, IEnumerable<ServiceEntry> services)
{
if (globals is null)
{
throw new ArgumentNullException(nameof(globals));
}
if (rootDirectory is null)
{
throw new ArgumentNullException(nameof(rootDirectory));
}
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}
Globals = globals;
RootDirectory = rootDirectory;
Services = services.ToArray();
}
public override ApplicationGlobals Globals { get; }
public override string RootDirectory { get; }
public override IReadOnlyList<ServiceEntry> Services { get; }
}
}

28
src/Microsoft.Tye.Core/ApplicationBuilder.cs

@ -0,0 +1,28 @@
// 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.Collections.Generic;
using System.IO;
namespace Microsoft.Tye
{
public sealed class ApplicationBuilder
{
public ApplicationBuilder(FileInfo source, string name)
{
Source = source;
Name = name;
}
public FileInfo Source { get; set; }
public string Name { get; set; }
public ContainerRegistry? Registry { get; set; }
public List<ServiceBuilder> Services { get; } = new List<ServiceBuilder>();
public List<IngressBuilder> Ingress { get; } = new List<IngressBuilder>();
}
}

257
src/Microsoft.Tye.Core/ApplicationFactory.cs

@ -0,0 +1,257 @@
// 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.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 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,
InternalPort = configBinding.InternalPort,
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 void ValidateConfigApplication(ConfigApplication config)
{
var context = new ValidationContext(config);
var results = new List<ValidationResult>();
if (!Validator.TryValidateObject(config, context, results, validateAllProperties: true))
{
throw new CommandException(
"Configuration validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
foreach (var service in config.Services)
{
context = new ValidationContext(service);
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true))
{
throw new CommandException(
$"Service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
foreach (var binding in service.Bindings)
{
context = new ValidationContext(binding);
if (!Validator.TryValidateObject(binding, context, results, validateAllProperties: true))
{
throw new CommandException(
$"Binding '{binding.Name}' of service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}
foreach (var envVar in service.Configuration)
{
context = new ValidationContext(service);
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true))
{
throw new CommandException(
$"Environment variable '{envVar.Name}' of service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}
foreach (var volume in service.Volumes)
{
context = new ValidationContext(service);
if (!Validator.TryValidateObject(service, context, results, validateAllProperties: true))
{
throw new CommandException(
$"Volume '{volume.Source}' of service '{service.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}
}
foreach (var ingress in config.Ingress)
{
// We don't currently recurse into ingress rules or ingress bindings right now.
// There's nothing to validate there.
context = new ValidationContext(ingress);
if (!Validator.TryValidateObject(ingress, context, results, validateAllProperties: true))
{
throw new CommandException(
$"Ingress '{ingress.Name}' validation failed." + Environment.NewLine +
string.Join(Environment.NewLine, results.Select(r => r.ErrorMessage)));
}
}
}
}
}

2
src/Microsoft.Tye.Core/ApplicationYamlWriter.cs

@ -11,7 +11,7 @@ namespace Microsoft.Tye
{
public sealed class ApplicationYamlWriter
{
public static Task WriteAsync(OutputContext output, StreamWriter writer, Application application)
public static Task WriteAsync(OutputContext output, StreamWriter writer, ApplicationBuilder application)
{
var yaml = application.Services.SelectMany(s => s.Outputs.OfType<IYamlManifestOutput>()).ToArray();
if (yaml.Length == 0)

17
src/Microsoft.Tye.Core/BindingBuilder.cs

@ -0,0 +1,17 @@
// 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.
namespace Microsoft.Tye
{
public sealed class BindingBuilder
{
public string? Name { get; set; }
public string? ConnectionString { get; set; }
public bool AutoAssignPort { get; set; }
public int? Port { get; set; }
public int? InternalPort { get; set; }
public string? Host { get; set; }
public string? Protocol { get; set; }
}
}

13
src/Microsoft.Tye.Core/BuildDockerImageStep.cs

@ -12,7 +12,7 @@ namespace Microsoft.Tye
public string Environment { get; set; } = "production";
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
if (SkipWithoutProject(output, service, out var project))
{
@ -24,22 +24,17 @@ namespace Microsoft.Tye
return;
}
if (SkipForEnvironment(output, service, Environment))
{
return;
}
if (!await DockerDetector.Instance.IsDockerInstalled.Value)
{
throw new CommandException($"Cannot generate a docker image for '{service.Service.Name}' because docker is not installed.");
throw new CommandException($"Cannot generate a docker image for '{service.Name}' because docker is not installed.");
}
if (!await DockerDetector.Instance.IsDockerConnectedToDaemon.Value)
{
throw new CommandException($"Cannot generate a docker image for '{service.Service.Name}' because docker is not running.");
throw new CommandException($"Cannot generate a docker image for '{service.Name}' because docker is not running.");
}
await DockerContainerBuilder.BuildContainerImageAsync(output, application, service, project, container);
await DockerContainerBuilder.BuildContainerImageAsync(output, application, project, container);
}
}
}

47
src/Microsoft.Tye.Core/CombineStep.cs

@ -2,6 +2,7 @@
// 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.Globalization;
using System.Threading.Tasks;
namespace Microsoft.Tye
@ -12,23 +13,33 @@ namespace Microsoft.Tye
public string Environment { get; set; } = "production";
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
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.Service.Source is Project))
if (!(service is ProjectServiceBuilder project))
{
return Task.CompletedTask;
}
// Compute ASPNETCORE_URLS based on the bindings exposed by *this* project.
foreach (var binding in service.Service.Bindings)
foreach (var binding in service.Bindings)
{
if (binding.Protocol == "http" || binding.Protocol == null)
if (binding.Protocol == null && binding.ConnectionString == null)
{
binding.Protocol = "http";
}
if (binding.AutoAssignPort && 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()))}";
service.Service.Environment.Add("ASPNETCORE_URLS", urls);
service.Service.Environment.Add("PORT", port);
project.EnvironmentVariables.Add(new EnvironmentVariable("ASPNETCORE_URLS", urls));
project.EnvironmentVariables.Add(new EnvironmentVariable("PORT", port.ToString(CultureInfo.InvariantCulture)));
break;
}
}
@ -47,17 +58,21 @@ namespace Microsoft.Tye
continue;
}
foreach (var binding in other.Service.Bindings)
foreach (var binding in other.Bindings)
{
// The other thing is a project, and will be deployed along with this
// service.
var configName = binding.Name == other.Service.Name ? other.Service.Name.ToUpperInvariant() : $"{other.Service.Name.ToUpperInvariant()}__{binding.Name.ToUpperInvariant()}";
if (other.Service.Source is Project)
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")
@ -67,6 +82,16 @@ namespace Microsoft.Tye
continue;
}
if (binding.Protocol == null)
{
binding.Protocol = "http";
}
if (binding.AutoAssignPort && binding.Port == null && binding.Protocol == "http")
{
binding.Port = 80;
}
if (!string.IsNullOrEmpty(binding.Protocol))
{
bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PROTOCOL", binding.Protocol));
@ -77,13 +102,13 @@ namespace Microsoft.Tye
bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__PORT", binding.Port.Value.ToString()));
}
bindings.Bindings.Add(new EnvironmentVariableInputBinding($"SERVICE__{configName}__HOST", binding.Host ?? other.Service.Name));
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.Service.Name}-{binding.Name}-secret",
name: $"binding-{Environment}-{other.Name}-{binding.Name ?? other.Name}-secret",
filename: $"CONNECTIONSTRING__{configName}",
other,
binding));

6
src/Microsoft.Tye.Core/ComputedBindings.cs

@ -29,7 +29,7 @@ namespace Microsoft.Tye
public sealed class SecretInputBinding : InputBinding
{
public SecretInputBinding(string name, string filename, ServiceEntry service, ServiceBinding binding)
public SecretInputBinding(string name, string filename, ServiceBuilder service, BindingBuilder binding)
{
Name = name;
Filename = filename;
@ -45,7 +45,7 @@ namespace Microsoft.Tye
public string Filename { get; }
// Used for informational purposes
public ServiceEntry Service { get; }
public ServiceBinding Binding { get; }
public ServiceBuilder Service { get; }
public BindingBuilder Binding { get; }
}
}

30
src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs

@ -0,0 +1,30 @@
// 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 YamlDotNet.Serialization;
namespace Microsoft.Tye.ConfigModel
{
// General design note re: nullability - we use [Required] to validate for non-null
// so we use = default! to reflect that. Code that creates a ConfigApplication should
// validate it before deferencing the properties.
public class ConfigApplication
{
// This gets set by all of the code paths that read the application
[YamlIgnore]
public FileInfo Source { get; set; } = default!;
public string? Name { get; set; }
public string? Registry { get; set; }
public List<ConfigService> Services { get; set; } = new List<ConfigService>();
public List<ConfigIngress> Ingress { get; set; } = new List<ConfigIngress>();
}
}

2
src/tye/ConfigModel/ConfigConfigurationSource.cs → src/Microsoft.Tye.Core/ConfigModel/ConfigConfigurationSource.cs

@ -10,8 +10,8 @@ namespace Microsoft.Tye.ConfigModel
{
[Required]
public string Name { get; set; } = default!;
[Required]
public string Value { get; set; } = default!;
public string? Source { get; set; }
}
}

13
src/tye/ConfigModel/ConfigFactory.cs → src/Microsoft.Tye.Core/ConfigModel/ConfigFactory.cs

@ -152,17 +152,8 @@ namespace Microsoft.Tye.ConfigModel
}
}
if (service.Configuration.Count == 0 && launchProfile.TryGetProperty("environmentVariables", out var environmentVariables))
{
foreach (var envVar in environmentVariables.EnumerateObject())
{
service.Configuration.Add(new ConfigConfigurationSource()
{
Name = envVar.Name,
Value = envVar.Value.GetString()
});
}
}
// Don't apply environment variables here. We don't want to carry forward settings from
// development into production.
}
}
}

6
src/tye/ConfigModel/ConfigIngress.cs → src/Microsoft.Tye.Core/ConfigModel/ConfigIngress.cs

@ -2,17 +2,17 @@
// 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.Text;
using System.ComponentModel.DataAnnotations;
namespace Microsoft.Tye.ConfigModel
{
public class ConfigIngress
{
[Required]
public string Name { get; set; } = default!;
public int? Replicas { get; set; }
public List<ConfigIngressRule> Rules { get; set; } = new List<ConfigIngressRule>();
public List<ConfigIngressServiceBinding> Bindings { get; set; } = new List<ConfigIngressServiceBinding>();
public List<ConfigIngressBinding> Bindings { get; set; } = new List<ConfigIngressBinding>();
}
}

6
src/tye/ConfigModel/ConfigIngressServiceBinding.cs → src/Microsoft.Tye.Core/ConfigModel/ConfigIngressBinding.cs

@ -2,13 +2,9 @@
// 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.Text;
namespace Microsoft.Tye.ConfigModel
{
public class ConfigIngressServiceBinding
public class ConfigIngressBinding
{
public string? Name { get; set; }
public bool AutoAssignPort { get; set; }

4
src/tye/ConfigModel/ConfigIngressRule.cs → src/Microsoft.Tye.Core/ConfigModel/ConfigIngressRule.cs

@ -2,10 +2,6 @@
// 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.Text;
namespace Microsoft.Tye.ConfigModel
{
public class ConfigIngressRule

0
src/tye/ConfigModel/ConfigService.cs → src/Microsoft.Tye.Core/ConfigModel/ConfigService.cs

0
src/tye/ConfigModel/ConfigServiceBinding.cs → src/Microsoft.Tye.Core/ConfigModel/ConfigServiceBinding.cs

11
src/tye/ConfigModel/ConfigVolume.cs → src/Microsoft.Tye.Core/ConfigModel/ConfigVolume.cs

@ -2,15 +2,16 @@
// 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.Text;
using System.ComponentModel.DataAnnotations;
namespace Microsoft.Tye.ConfigModel
{
public class ConfigVolume
{
public string? Source { get; set; }
public string? Target { get; set; }
[Required]
public string Source { get; set; } = default!;
[Required]
public string Target { get; set; } = default!;
}
}

27
src/Microsoft.Tye.Core/ContainerServiceBuilder.cs

@ -0,0 +1,27 @@
// 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.Collections.Generic;
namespace Microsoft.Tye
{
public sealed class ContainerServiceBuilder : ServiceBuilder
{
public ContainerServiceBuilder(string name, string image)
: base(name)
{
Image = image;
}
public string Image { get; set; }
public string? Args { get; set; }
public int Replicas { get; set; } = 1;
public List<EnvironmentVariable> EnvironmentVariables { get; } = new List<EnvironmentVariable>();
public List<VolumeBuilder> Volumes { get; } = new List<VolumeBuilder>();
}
}

163
src/Microsoft.Tye.Core/CustomizationPipeline.cs

@ -1,163 +0,0 @@
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
namespace Microsoft.Tye
{
internal class CustomizationPipeline : Pipeline
{
private readonly Dictionary<Type, List<MulticastDelegate>> callbacks = new Dictionary<Type, List<MulticastDelegate>>();
private readonly OutputContext output;
private readonly string rootDirectory;
private readonly string name;
private readonly SolutionFile? solution;
private readonly FileInfo? projectFile;
public CustomizationPipeline(OutputContext output, string rootDirectory, string name, SolutionFile? solution, FileInfo? projectFile)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (rootDirectory is null)
{
throw new ArgumentNullException(nameof(rootDirectory));
}
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
this.output = output;
this.rootDirectory = rootDirectory;
this.name = name;
this.solution = solution;
this.projectFile = projectFile;
}
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;
}
public override void Register<TApplication>(Action<TApplication> callback)
{
if (callback is null)
{
throw new ArgumentNullException(nameof(callback));
}
if (!callbacks.TryGetValue(typeof(TApplication), out var delegates))
{
delegates = new List<MulticastDelegate>();
callbacks.Add(typeof(TApplication), delegates);
}
delegates.Add(callback);
}
private static ProjectInSolution? FindProjectInSolution(SolutionFile solution, string projectName)
{
for (var i = 0; i < solution.ProjectsInOrder.Count; i++)
{
var project = solution.ProjectsInOrder[i];
var normalized = Names.NormalizeToFriendly(project.ProjectName);
if (string.Equals(normalized, projectName, StringComparison.Ordinal) &&
project.AbsolutePath.EndsWith(".csproj", StringComparison.Ordinal))
{
return project;
}
}
return null;
}
}
}

10
src/Microsoft.Tye.Core/DeployServiceYamlStep.cs

@ -17,23 +17,23 @@ namespace Microsoft.Tye
public string Environment { get; set; } = "production";
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
var yaml = service.Outputs.OfType<IYamlManifestOutput>().ToArray();
if (yaml.Length == 0)
{
output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping.");
output.WriteDebugLine($"No yaml manifests found for service '{service.Name}'. Skipping.");
return;
}
if (!await KubectlDetector.Instance.IsKubectlInstalled.Value)
{
throw new CommandException($"Cannot apply manifests for '{service.Service.Name}' because kubectl is not installed.");
throw new CommandException($"Cannot apply manifests for '{service.Name}' because kubectl is not installed.");
}
if (!await KubectlDetector.Instance.IsKubectlConnectedToCluster.Value)
{
throw new CommandException($"Cannot apply manifests for '{service.Service.Name}' because kubectl is not connected to a cluster.");
throw new CommandException($"Cannot apply manifests for '{service.Name}' because kubectl is not connected to a cluster.");
}
using var tempFile = TempFile.Create();
@ -67,7 +67,7 @@ namespace Microsoft.Tye
throw new CommandException("'kubectl apply' failed.");
}
output.WriteInfoLine($"Deployed service '{service.FriendlyName}'.");
output.WriteInfoLine($"Deployed service '{service.Name}'.");
}
}
}

17
src/Microsoft.Tye.Core/DockerContainerBuilder.cs

@ -12,7 +12,7 @@ namespace Microsoft.Tye
{
internal static class DockerContainerBuilder
{
public static async Task BuildContainerImageAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container)
public static async Task BuildContainerImageAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container)
{
if (output is null)
{
@ -24,11 +24,6 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(application));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (project is null)
{
throw new ArgumentNullException(nameof(project));
@ -41,14 +36,14 @@ namespace Microsoft.Tye
using var tempFile = TempFile.Create();
var dockerFilePath = Path.Combine(application.GetProjectDirectory(project), Path.GetDirectoryName(project.RelativeFilePath)!, "Dockerfile");
var dockerFilePath = Path.Combine(project.ProjectFile.DirectoryName, "Dockerfile");
if (File.Exists(dockerFilePath))
{
output.WriteDebugLine($"Using existing Dockerfile '{dockerFilePath}'.");
}
else
{
await DockerfileGenerator.WriteDockerfileAsync(output, application, service, project, container, tempFile.FilePath);
await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, tempFile.FilePath);
dockerFilePath = tempFile.FilePath;
}
@ -61,7 +56,7 @@ namespace Microsoft.Tye
}
else
{
var publishOutput = service.Outputs.OfType<ProjectPublishOutput>().FirstOrDefault();
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.");
@ -76,7 +71,7 @@ namespace Microsoft.Tye
var exitCode = await Process.ExecuteAsync(
$"docker",
$"build \"{contextDirectory}\" -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"",
application.GetProjectDirectory(project),
project.ProjectFile.DirectoryName,
stdOut: capture.StdOut,
stdErr: capture.StdErr);
@ -87,7 +82,7 @@ namespace Microsoft.Tye
}
output.WriteInfoLine($"Created Docker Image: '{container.ImageName}:{container.ImageTag}'");
service.Outputs.Add(new DockerImageOutput(container.ImageName!, container.ImageTag!));
project.Outputs.Add(new DockerImageOutput(container.ImageName!, container.ImageTag!));
}
}
}

31
src/Microsoft.Tye.Core/DockerImageOutput.cs

@ -0,0 +1,31 @@
// 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;
namespace Microsoft.Tye
{
public class DockerImageOutput : ServiceOutput
{
public DockerImageOutput(string imageName, string imageTag)
{
if (imageName is null)
{
throw new ArgumentNullException(nameof(imageName));
}
if (imageTag is null)
{
throw new ArgumentNullException(nameof(imageTag));
}
ImageName = imageName;
ImageTag = imageTag;
}
public string ImageName { get; }
public string ImageTag { get; }
}
}

26
src/Microsoft.Tye.Core/DockerfileGenerator.cs

@ -12,7 +12,7 @@ namespace Microsoft.Tye
{
public static class DockerfileGenerator
{
public static async Task WriteDockerfileAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, string filePath)
public static async Task WriteDockerfileAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, string filePath)
{
if (output is null)
{
@ -24,11 +24,6 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(application));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (project is null)
{
throw new ArgumentNullException(nameof(project));
@ -47,7 +42,7 @@ namespace Microsoft.Tye
using var stream = File.OpenWrite(filePath);
using var writer = new StreamWriter(stream, encoding: Encoding.UTF8, bufferSize: -1, leaveOpen: true);
var entryPoint = Path.GetFileNameWithoutExtension(project.RelativeFilePath);
var entryPoint = Path.GetFileNameWithoutExtension(project.ProjectFile.Name);
output.WriteDebugLine($"Writing Dockerfile to '{filePath}'.");
if (container.UseMultiphaseDockerfile ?? true)
{
@ -80,18 +75,13 @@ namespace Microsoft.Tye
await writer.WriteLineAsync($"ENTRYPOINT [\"dotnet\", \"{applicationEntryPoint}.dll\"]");
}
public static void ApplyContainerDefaults(Application application, ServiceEntry service, Project project, ContainerInfo container)
public static void ApplyContainerDefaults(ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (project is null)
{
throw new ArgumentNullException(nameof(project));
@ -131,16 +121,16 @@ namespace Microsoft.Tye
container.BuildImageName ??= "mcr.microsoft.com/dotnet/core/sdk";
container.BuildImageTag ??= "3.1";
if (container.ImageName == null && application.Globals.Registry?.Hostname == null)
if (container.ImageName == null && application.Registry?.Hostname == null)
{
container.ImageName ??= service.Service.Name.ToLowerInvariant();
container.ImageName ??= project.Name.ToLowerInvariant();
}
else if (container.ImageName == null && application.Globals.Registry?.Hostname != null)
else if (container.ImageName == null && application.Registry?.Hostname != null)
{
container.ImageName ??= $"{application.Globals.Registry?.Hostname}/{service.Service.Name.ToLowerInvariant()}";
container.ImageName ??= $"{application.Registry?.Hostname}/{project.Name.ToLowerInvariant()}";
}
container.ImageTag ??= project.Version.Replace("+", "-");
container.ImageTag ??= project.Version?.Replace("+", "-") ?? "latest";
}
}
}

19
src/Microsoft.Tye.Core/EnvironmentVariable.cs

@ -0,0 +1,19 @@
// 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.
namespace Microsoft.Tye
{
public sealed class EnvironmentVariable
{
public EnvironmentVariable(string name, string value)
{
Name = name;
Value = value;
}
public string Name { get; }
public string Value { get; }
}
}

27
src/Microsoft.Tye.Core/ExecutableServiceBuilder.cs

@ -0,0 +1,27 @@
// 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.Collections.Generic;
namespace Microsoft.Tye
{
public sealed class ExecutableServiceBuilder : ServiceBuilder
{
public ExecutableServiceBuilder(string name, string executable)
: base(name)
{
Executable = executable;
}
public string Executable { get; set; }
public string? WorkingDirectory { get; set; }
public string? Args { get; set; }
public int Replicas { get; set; } = 1;
public List<EnvironmentVariable> EnvironmentVariables { get; } = new List<EnvironmentVariable>();
}
}

14
src/Microsoft.Tye.Core/ExternalServiceBuilder.cs

@ -0,0 +1,14 @@
// 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.
namespace Microsoft.Tye
{
public sealed class ExternalServiceBuilder : ServiceBuilder
{
public ExternalServiceBuilder(string name)
: base(name)
{
}
}
}

6
src/Microsoft.Tye.Core/GenerateDockerfileStep.cs

@ -13,7 +13,7 @@ namespace Microsoft.Tye
public bool Force { get; set; }
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
if (SkipWithoutProject(output, service, out var project))
{
@ -32,7 +32,7 @@ namespace Microsoft.Tye
container.UseMultiphaseDockerfile ??= true;
var dockerFilePath = Path.Combine(application.GetProjectDirectory(project), "Dockerfile");
var dockerFilePath = Path.Combine(project.ProjectFile.DirectoryName, "Dockerfile");
if (File.Exists(dockerFilePath) && !Force)
{
throw new CommandException("'Dockerfile' already exists for project. use '--force' to overwrite.");
@ -40,7 +40,7 @@ namespace Microsoft.Tye
File.Delete(dockerFilePath);
await DockerfileGenerator.WriteDockerfileAsync(output, application, service, project, container, dockerFilePath);
await DockerfileGenerator.WriteDockerfileAsync(output, application, project, container, dockerFilePath);
output.WriteInfoLine($"Generated Dockerfile at '{dockerFilePath}'.");
}
}

5
src/Microsoft.Tye.Core/GenerateHelmChartStep.cs

@ -13,7 +13,7 @@ namespace Microsoft.Tye
public bool Force { get; set; }
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
if (SkipWithoutProject(output, service, out var project))
{
@ -25,7 +25,7 @@ namespace Microsoft.Tye
return;
}
var chartDirectory = Path.Combine(application.GetProjectDirectory(project), "charts");
var chartDirectory = Path.Combine(project.ProjectFile.DirectoryName, "charts");
if (Directory.Exists(chartDirectory) && !Force)
{
throw new CommandException("'charts' directory already exists for project. use '--force' to overwrite.");
@ -39,7 +39,6 @@ namespace Microsoft.Tye
await HelmChartGenerator.GenerateAsync(
output,
application,
service,
project,
container,
chart,

10
src/Microsoft.Tye.Core/GenerateKubernetesManifestStep.cs

@ -13,23 +13,23 @@ namespace Microsoft.Tye
public string Environment { get; set; } = "production";
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
if (SkipWithoutContainerOutput(output, service))
{
return Task.CompletedTask;
}
if (SkipForEnvironment(output, service, Environment))
if (SkipWithoutProject(output, service, out var project))
{
return Task.CompletedTask;
}
service.Outputs.Add(KubernetesManifestGenerator.CreateDeployment(output, application, service));
service.Outputs.Add(KubernetesManifestGenerator.CreateDeployment(output, application, project));
if (service.Service.Bindings.Count > 0)
if (service.Bindings.Count > 0)
{
service.Outputs.Add(KubernetesManifestGenerator.CreateService(output, application, service));
service.Outputs.Add(KubernetesManifestGenerator.CreateService(output, application, project));
}
return Task.CompletedTask;

6
src/Microsoft.Tye.Core/GenerateOamComponentStep.cs

@ -13,19 +13,19 @@ namespace Microsoft.Tye
public string Environment { get; set; } = "production";
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
if (SkipWithoutContainerOutput(output, service))
{
return Task.CompletedTask;
}
if (SkipForEnvironment(output, service, Environment))
if (SkipWithoutProject(output, service, out var project))
{
return Task.CompletedTask;
}
var component = OamComponentGenerator.CreateOamComponent(output, application, service);
var component = OamComponentGenerator.CreateOamComponent(output, application, project);
service.Outputs.Add(component);
return Task.CompletedTask;
}

13
src/Microsoft.Tye.Core/HelmChartBuilder.cs

@ -11,7 +11,7 @@ namespace Microsoft.Tye
{
internal static class HelmChartBuilder
{
public static async Task BuildHelmChartAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, HelmChartStep chart)
public static async Task BuildHelmChartAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart)
{
if (output is null)
{
@ -23,11 +23,6 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(application));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (project is null)
{
throw new ArgumentNullException(nameof(project));
@ -43,11 +38,11 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(chart));
}
var projectDirectory = Path.Combine(application.RootDirectory, Path.GetDirectoryName(project.RelativeFilePath)!);
var projectDirectory = project.ProjectFile.DirectoryName;
var outputDirectoryPath = Path.Combine(projectDirectory, "bin");
using var tempDirectory = TempDirectory.Create();
HelmChartGenerator.ApplyHelmChartDefaults(application, service, container, chart);
HelmChartGenerator.ApplyHelmChartDefaults(application, project, container, chart);
var chartRoot = Path.Combine(projectDirectory, "charts");
var chartPath = Path.Combine(chartRoot, chart.ChartName);
@ -62,7 +57,7 @@ namespace Microsoft.Tye
chartRoot = tempDirectory.DirectoryPath;
chartPath = Path.Combine(chartRoot, chart.ChartName);
output.WriteDebugLine($"Generating chart in '{chartPath}'.");
await HelmChartGenerator.GenerateAsync(output, application, service, project, container, chart, new DirectoryInfo(tempDirectory.DirectoryPath));
await HelmChartGenerator.GenerateAsync(output, application, project, container, chart, new DirectoryInfo(tempDirectory.DirectoryPath));
}
output.WriteDebugLine("Running 'helm package'.");

13
src/Microsoft.Tye.Core/HelmChartGenerator.cs

@ -10,7 +10,7 @@ namespace Microsoft.Tye
{
internal static class HelmChartGenerator
{
public static async Task GenerateAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory)
public static async Task GenerateAsync(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory)
{
if (output is null)
{
@ -22,11 +22,6 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(application));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (project is null)
{
throw new ArgumentNullException(nameof(project));
@ -47,7 +42,7 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(outputDirectory));
}
ApplyHelmChartDefaults(application, service, container, chart);
ApplyHelmChartDefaults(application, project, container, chart);
// The directory with the charts needs to be the same as the chart name
var chartDirectoryPath = Path.Combine(outputDirectory.FullName, chart.ChartName);
@ -87,7 +82,7 @@ namespace Microsoft.Tye
});
}
public static void ApplyHelmChartDefaults(Application application, ServiceEntry service, ContainerInfo container, HelmChartStep chart)
public static void ApplyHelmChartDefaults(ApplicationBuilder application, ServiceBuilder service, ContainerInfo container, HelmChartStep chart)
{
if (application is null)
{
@ -109,7 +104,7 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(chart));
}
chart.ChartName ??= service.Service.Name.ToLowerInvariant();
chart.ChartName ??= service.Name.ToLowerInvariant();
}
}
}

14
src/Microsoft.Tye.Core/IngressBindingBuilder.cs

@ -0,0 +1,14 @@
// 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.
namespace Microsoft.Tye
{
public sealed class IngressBindingBuilder
{
public string? Name { get; set; }
public bool AutoAssignPort { get; set; }
public int? Port { get; set; }
public string? Protocol { get; set; } // HTTP or HTTPS
}
}

24
src/Microsoft.Tye.Core/IngressBuilder.cs

@ -0,0 +1,24 @@
// 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.Collections.Generic;
namespace Microsoft.Tye
{
public sealed class IngressBuilder
{
public IngressBuilder(string name)
{
Name = name;
}
public string Name { get; set; }
public int Replicas { get; set; } = 1;
public List<IngressBindingBuilder> Bindings { get; set; } = new List<IngressBindingBuilder>();
public List<IngressRuleBuilder> Rules { get; set; } = new List<IngressRuleBuilder>();
}
}

13
src/Microsoft.Tye.Core/IngressRuleBuilder.cs

@ -0,0 +1,13 @@
// 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.
namespace Microsoft.Tye
{
public sealed class IngressRuleBuilder
{
public string? Path { get; set; }
public string? Host { get; set; }
public string? Service { get; set; }
}
}

71
src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs

@ -11,7 +11,7 @@ namespace Microsoft.Tye
{
internal static class KubernetesManifestGenerator
{
public static ServiceOutput CreateService(OutputContext output, Application application, ServiceEntry service)
public static ServiceOutput CreateService(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
if (output is null)
{
@ -35,23 +35,20 @@ namespace Microsoft.Tye
var metadata = new YamlMappingNode();
root.Add("metadata", metadata);
metadata.Add("name", service.Service.Name);
metadata.Add("name", service.Name);
var labels = new YamlMappingNode();
metadata.Add("labels", labels);
labels.Add("app.kubernetes.io/name", service.Service.Name);
labels.Add("app.kubernetes.io/name", service.Name);
if (application.Globals.Name is object)
{
labels.Add("app.kubernetes.io/part-of", application.Globals.Name);
}
labels.Add("app.kubernetes.io/part-of", application.Name);
var spec = new YamlMappingNode();
root.Add("spec", spec);
var selector = new YamlMappingNode();
spec.Add("selector", selector);
selector.Add("app.kubernetes.io/name", service.Service.Name);
selector.Add("app.kubernetes.io/name", service.Name);
spec.Add("type", "ClusterIP");
@ -59,7 +56,7 @@ namespace Microsoft.Tye
spec.Add("ports", ports);
// We figure out the port based on bindings
foreach (var binding in service.Service.Bindings)
foreach (var binding in service.Bindings)
{
if (binding.Protocol == "https")
{
@ -80,10 +77,10 @@ namespace Microsoft.Tye
}
}
return new KubernetesServiceOutput(service.Service.Name, new YamlDocument(root));
return new KubernetesServiceOutput(service.Name, new YamlDocument(root));
}
public static ServiceOutput CreateDeployment(OutputContext output, Application application, ServiceEntry service)
public static ServiceOutput CreateDeployment(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project)
{
if (output is null)
{
@ -95,12 +92,12 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(application));
}
if (service is null)
if (project is null)
{
throw new ArgumentNullException(nameof(service));
throw new ArgumentNullException(nameof(project));
}
var bindings = service.Outputs.OfType<ComputedBindings>().FirstOrDefault();
var bindings = project.Outputs.OfType<ComputedBindings>().FirstOrDefault();
var root = new YamlMappingNode();
@ -109,27 +106,24 @@ namespace Microsoft.Tye
var metadata = new YamlMappingNode();
root.Add("metadata", metadata);
metadata.Add("name", service.Service.Name);
metadata.Add("name", project.Name);
var labels = new YamlMappingNode();
metadata.Add("labels", labels);
labels.Add("app.kubernetes.io/name", service.Service.Name);
if (application.Globals.Name is object)
{
labels.Add("app.kubernetes.io/part-of", application.Globals.Name);
}
labels.Add("app.kubernetes.io/name", project.Name);
labels.Add("app.kubernetes.io/part-of", application.Name);
var spec = new YamlMappingNode();
root.Add("spec", spec);
spec.Add("replicas", service.Service.Replicas.ToString());
spec.Add("replicas", project.Replicas.ToString());
var selector = new YamlMappingNode();
spec.Add("selector", selector);
var matchLabels = new YamlMappingNode();
selector.Add("matchLabels", matchLabels);
matchLabels.Add("app.kubernetes.io/name", service.Service.Name);
matchLabels.Add("app.kubernetes.io/name", project.Name);
var template = new YamlMappingNode();
spec.Add("template", template);
@ -139,11 +133,8 @@ namespace Microsoft.Tye
labels = new YamlMappingNode();
metadata.Add("labels", labels);
labels.Add("app.kubernetes.io/name", service.Service.Name);
if (application.Globals.Name is object)
{
labels.Add("app.kubernetes.io/part-of", application.Globals.Name);
}
labels.Add("app.kubernetes.io/name", project.Name);
labels.Add("app.kubernetes.io/part-of", application.Name);
spec = new YamlMappingNode();
template.Add("spec", spec);
@ -151,19 +142,19 @@ namespace Microsoft.Tye
var containers = new YamlSequenceNode();
spec.Add("containers", containers);
var images = service.Outputs.OfType<DockerImageOutput>();
var images = project.Outputs.OfType<DockerImageOutput>();
foreach (var image in images)
{
var container = new YamlMappingNode();
containers.Add(container);
container.Add("name", service.Service.Name); // NOTE: to really support multiple images we'd need to generate unique names.
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 (service.Service.Environment.Count > 0 ||
if (project.EnvironmentVariables.Count > 0 ||
// We generate ASPNETCORE_URLS if there are bindings for http
service.Service.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) ||
project.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) ||
// We generate environment variables for other services if there dependencies
(bindings is object && bindings.Bindings.OfType<EnvironmentVariableInputBinding>().Any()))
@ -171,12 +162,12 @@ namespace Microsoft.Tye
var env = new YamlSequenceNode();
container.Add("env", env);
foreach (var kvp in service.Service.Environment)
foreach (var kvp in project.EnvironmentVariables)
{
env.Add(new YamlMappingNode()
{
{ "name", kvp.Key },
{ "value", new YamlScalarNode(kvp.Value.ToString()) { Style = ScalarStyle.SingleQuoted, } },
{ "name", kvp.Name },
{ "value", new YamlScalarNode(kvp.Value) { Style = ScalarStyle.SingleQuoted, } },
});
}
@ -203,18 +194,18 @@ namespace Microsoft.Tye
var volumeMount = new YamlMappingNode();
volumeMounts.Add(volumeMount);
volumeMount.Add("name", $"{binding.Service.Service.Name}-{binding.Binding.Name}");
volumeMount.Add("mountPath", $"/var/tye/bindings/{binding.Service.Service.Name}-{binding.Binding.Name}");
volumeMount.Add("name", $"{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}");
volumeMount.Add("mountPath", $"/var/tye/bindings/{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}");
volumeMount.Add("readOnly", "true");
}
}
if (service.Service.Bindings.Count > 0)
if (project.Bindings.Count > 0)
{
var ports = new YamlSequenceNode();
container.Add("ports", ports);
foreach (var binding in service.Service.Bindings)
foreach (var binding in project.Bindings)
{
if (binding.Protocol == "https")
{
@ -242,7 +233,7 @@ namespace Microsoft.Tye
{
var volume = new YamlMappingNode();
volumes.Add(volume);
volume.Add("name", $"{binding.Service.Service.Name}-{binding.Binding.Name}");
volume.Add("name", $"{binding.Service.Name}-{binding.Binding.Name ?? binding.Service.Name}");
var secret = new YamlMappingNode();
volume.Add("secret", secret);
@ -258,7 +249,7 @@ namespace Microsoft.Tye
}
}
return new KubernetesDeploymentOutput(service.Service.Name, new YamlDocument(root));
return new KubernetesDeploymentOutput(project.Name, new YamlDocument(root));
}
}
}

14
src/Microsoft.Tye.Core/OamApplicationGenerator.cs

@ -13,7 +13,7 @@ namespace Microsoft.Tye
{
internal static class OamApplicationGenerator
{
public static Task WriteOamApplicationAsync(TextWriter writer, OutputContext output, Application application, string applicationName, string environment)
public static Task WriteOamApplicationAsync(TextWriter writer, OutputContext output, ApplicationBuilder application, string environment)
{
if (writer is null)
{
@ -30,11 +30,6 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(application));
}
if (applicationName is null)
{
throw new ArgumentNullException(nameof(applicationName));
}
if (environment is null)
{
throw new ArgumentNullException(nameof(environment));
@ -45,10 +40,7 @@ namespace Microsoft.Tye
foreach (var service in application.Services)
{
if (service.AppliesToEnvironment(environment))
{
componentManifests.AddRange(service.Outputs.OfType<OamComponentOutput>());
}
componentManifests.AddRange(service.Outputs.OfType<OamComponentOutput>());
}
var root = new YamlMappingNode();
@ -58,7 +50,7 @@ namespace Microsoft.Tye
var metadata = new YamlMappingNode();
root.Add("metadata", metadata);
metadata.Add("name", applicationName.ToLowerInvariant());
metadata.Add("name", application.Name);
var spec = new YamlMappingNode();
root.Add("spec", spec);

14
src/Microsoft.Tye.Core/OamComponentGenerator.cs

@ -10,7 +10,7 @@ namespace Microsoft.Tye
{
internal static class OamComponentGenerator
{
public static OamComponentOutput CreateOamComponent(OutputContext output, Application application, ServiceEntry service)
public static OamComponentOutput CreateOamComponent(OutputContext output, ApplicationBuilder application, ProjectServiceBuilder project)
{
if (output is null)
{
@ -22,9 +22,9 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(application));
}
if (service is null)
if (project is null)
{
throw new ArgumentNullException(nameof(service));
throw new ArgumentNullException(nameof(project));
}
var root = new YamlMappingNode();
@ -34,7 +34,7 @@ namespace Microsoft.Tye
var metadata = new YamlMappingNode();
root.Add("metadata", metadata);
metadata.Add("name", service.Service.Name);
metadata.Add("name", project.Name);
var spec = new YamlMappingNode();
root.Add("spec", spec);
@ -43,16 +43,16 @@ namespace Microsoft.Tye
var containers = new YamlSequenceNode();
spec.Add("containers", containers);
var images = service.Outputs.OfType<DockerImageOutput>();
var images = project.Outputs.OfType<DockerImageOutput>();
foreach (var image in images)
{
var container = new YamlMappingNode();
containers.Add(container);
container.Add("name", service.Service.Name); // NOTE: to really support multiple images we'd need to generate unique names.
container.Add("name", project.Name); // NOTE: to really support multiple images we'd need to generate unique names.
container.Add("image", $"{image.ImageName}:{image.ImageTag}");
}
return new OamComponentOutput(service.Service.Name, new YamlDocument(root));
return new OamComponentOutput(project.Name, new YamlDocument(root));
}
}
}

23
src/Microsoft.Tye.Core/ProjectReader.cs

@ -59,7 +59,7 @@ namespace Microsoft.Tye
}
}
public static Task ReadProjectDetailsAsync(OutputContext output, FileInfo projectFile, Project project)
public static Task ReadProjectDetailsAsync(OutputContext output, ProjectServiceBuilder project)
{
if (output is null)
{
@ -71,9 +71,9 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(project));
}
EnsureMSBuildRegistered(output, projectFile);
EnsureMSBuildRegistered(output, project.ProjectFile);
EvaluateProject(output, projectFile, project);
EvaluateProject(output, project);
if (!SemVersion.TryParse(project.Version, out var version))
{
@ -131,9 +131,16 @@ namespace Microsoft.Tye
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void LogIt(OutputContext output)
{
output.WriteDebugLine("Loaded: " + typeof(ProjectInstance).Assembly.FullName);
output.WriteDebugLine("Loaded From: " + typeof(ProjectInstance).Assembly.Location);
}
// Do not load MSBuild types before using EnsureMSBuildRegistered.
[MethodImpl(MethodImplOptions.NoInlining)]
private static void EvaluateProject(OutputContext output, FileInfo projectFile, Project project)
private static void EvaluateProject(OutputContext output, ProjectServiceBuilder project)
{
var sw = Stopwatch.StartNew();
@ -145,17 +152,17 @@ namespace Microsoft.Tye
try
{
output.WriteDebugLine($"Loading project '{projectFile.FullName}'.");
var msbuildProject = Microsoft.Build.Evaluation.Project.FromFile(projectFile.FullName, new ProjectOptions()
output.WriteDebugLine($"Loading project '{project.ProjectFile.FullName}'.");
var msbuildProject = Microsoft.Build.Evaluation.Project.FromFile(project.ProjectFile.FullName, new ProjectOptions()
{
ProjectCollection = projectCollection,
});
projectInstance = msbuildProject.CreateProjectInstance();
output.WriteDebugLine($"Loaded project '{projectFile.FullName}'.");
output.WriteDebugLine($"Loaded project '{project.ProjectFile.FullName}'.");
}
catch (Exception ex)
{
throw new CommandException($"Failed to load project: '{projectFile.FullName}'.", ex);
throw new CommandException($"Failed to load project: '{project.ProjectFile.FullName}'.", ex);
}
// Currently we only log at debug level.

42
src/Microsoft.Tye.Core/ProjectServiceBuilder.cs

@ -0,0 +1,42 @@
// 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.Collections.Generic;
using System.IO;
namespace Microsoft.Tye
{
public sealed class ProjectServiceBuilder : ServiceBuilder
{
public ProjectServiceBuilder(string name, FileInfo projectFile)
: base(name)
{
ProjectFile = projectFile;
}
public FileInfo ProjectFile { get; }
public int Replicas { get; set; } = 1;
public bool Build { get; set; }
public string? Args { get; set; }
public FrameworkCollection Frameworks { get; } = new FrameworkCollection();
// This is always set on the ApplicationFactory codepath.
public string TargetFramework { get; set; } = default!;
// This is always set on the ApplicationFactory codepath.
public string Version { get; set; } = default!;
// Data used for building containers
public ContainerInfo? ContainerInfo { get; set; }
public List<EnvironmentVariable> EnvironmentVariables { get; } = new List<EnvironmentVariable>();
// Used when running in a container locally.
public List<VolumeBuilder> Volumes { get; } = new List<VolumeBuilder>();
}
}

11
src/Microsoft.Tye.Core/PublishProjectStep.cs

@ -13,7 +13,7 @@ namespace Microsoft.Tye
{
public override string DisplayText => "Publishing Project...";
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
if (SkipWithoutProject(output, service, out var project))
{
@ -30,16 +30,15 @@ namespace Microsoft.Tye
return;
}
var projectFilePath = Path.Combine(application.RootDirectory, project.RelativeFilePath);
var outputDirectory = Path.Combine(Path.GetDirectoryName(projectFilePath)!, "bin", "Release", project.TargetFramework, "publish");
var outputDirectory = Path.Combine(project.ProjectFile.DirectoryName, "bin", "Release", project.TargetFramework, "publish");
output.WriteDebugLine("Running 'dotnet publish'.");
output.WriteCommandLine("dotnet", $"publish \"{projectFilePath}\" -c Release -o \"{outputDirectory}\"");
output.WriteCommandLine("dotnet", $"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory}\"");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
$"dotnet",
$"publish \"{projectFilePath}\" -c Release -o \"{outputDirectory}\"",
application.GetProjectDirectory(project),
$"publish \"{project.ProjectFile.FullName}\" -c Release -o \"{outputDirectory}\"",
project.ProjectFile.DirectoryName,
stdOut: capture.StdOut,
stdErr: capture.StdErr);

7
src/Microsoft.Tye.Core/PushDockerImageStep.cs

@ -13,7 +13,7 @@ namespace Microsoft.Tye
public string Environment { get; set; } = "production";
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
if (SkipWithoutProject(output, service, out var _))
{
@ -25,11 +25,6 @@ namespace Microsoft.Tye
return;
}
if (SkipForEnvironment(output, service, Environment))
{
return;
}
foreach (var image in service.Outputs.OfType<DockerImageOutput>())
{
await DockerPush.ExecuteAsync(output, image.ImageName, image.ImageTag);

19
src/Microsoft.Tye.Core/ServiceBuilder.cs

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace Microsoft.Tye
{
public abstract class ServiceBuilder
{
public ServiceBuilder(string name)
{
Name = name;
}
public string Name { get; }
public List<BindingBuilder> Bindings { get; } = new List<BindingBuilder>();
// TODO: this is temporary while refactoring
public List<ServiceOutput> Outputs { get; } = new List<ServiceOutput>();
}
}

37
src/Microsoft.Tye.Core/ServiceExecutor.cs

@ -13,10 +13,10 @@ namespace Microsoft.Tye
public sealed class ServiceExecutor
{
private readonly OutputContext output;
private readonly Application application;
private readonly ApplicationBuilder application;
private readonly Step[] steps;
public ServiceExecutor(OutputContext output, Application application, IEnumerable<Step> steps)
public ServiceExecutor(OutputContext output, ApplicationBuilder application, IEnumerable<Step> steps)
{
if (output is null)
{
@ -38,9 +38,9 @@ namespace Microsoft.Tye
this.steps = steps.ToArray();
}
public async Task ExecuteAsync(ServiceEntry service)
public async Task ExecuteAsync(ServiceBuilder service)
{
using var tracker = output.BeginStep($"Processing Service '{service.FriendlyName}'...");
using var tracker = output.BeginStep($"Processing Service '{service.Name}'...");
for (var i = 0; i < steps.Length; i++)
{
var step = steps[i];
@ -56,20 +56,9 @@ namespace Microsoft.Tye
{
public abstract string DisplayText { get; }
public abstract Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service);
public abstract Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service);
protected bool SkipForEnvironment(OutputContext output, ServiceEntry service, string environment)
{
if (!service.AppliesToEnvironment(environment))
{
output.WriteDebugLine($"Service '{service.FriendlyName}' is not part of environment '{environment}'. Skipping.");
return true;
}
return false;
}
protected bool SkipWithoutProject(OutputContext output, ServiceEntry service, [MaybeNullWhen(returnValue: true)] out Project project)
protected bool SkipWithoutProject(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out ProjectServiceBuilder project)
{
if (output is null)
{
@ -81,18 +70,18 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(service));
}
if (service.Service.Source is Project p)
if (service is ProjectServiceBuilder p)
{
project = p;
return false;
}
output.WriteInfoLine($"Service '{service.FriendlyName}' does not have a project associated. Skipping.");
output.WriteInfoLine($"Service '{service.Name}' does not have a project associated. Skipping.");
project = default!;
return true;
}
protected bool SkipWithoutContainerInfo(OutputContext output, ServiceEntry service, [MaybeNullWhen(returnValue: true)] out ContainerInfo container)
protected bool SkipWithoutContainerInfo(OutputContext output, ServiceBuilder service, [MaybeNullWhen(returnValue: true)] out ContainerInfo container)
{
if (output is null)
{
@ -104,18 +93,18 @@ namespace Microsoft.Tye
throw new ArgumentNullException(nameof(service));
}
if (service.Service.GeneratedAssets.Container is ContainerInfo c)
if (service is ProjectServiceBuilder project && project.ContainerInfo is ContainerInfo c)
{
container = c;
return false;
}
output.WriteInfoLine($"Service '{service.FriendlyName}' does not produce a container. Skipping.");
output.WriteInfoLine($"Service '{service.Name}' does not produce a container. Skipping.");
container = default!;
return true;
}
protected bool SkipWithoutContainerOutput(OutputContext output, ServiceEntry service)
protected bool SkipWithoutContainerOutput(OutputContext output, ServiceBuilder service)
{
if (output is null)
{
@ -132,7 +121,7 @@ namespace Microsoft.Tye
return false;
}
output.WriteInfoLine($"Service '{service.FriendlyName}' does not have a container. Skipping.");
output.WriteInfoLine($"Service '{service.Name}' does not have a container. Skipping.");
return true;
}
}

10
src/Microsoft.Tye.Core/ServiceOutput.cs

@ -0,0 +1,10 @@
// 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.
namespace Microsoft.Tye
{
public abstract class ServiceOutput
{
}
}

12
src/Microsoft.Tye.Core/ValidateSecretStep.cs

@ -31,7 +31,7 @@ namespace Microsoft.Tye
// Keep track of secrets we've seen so we don't validate them twice.
public HashSet<string> Secrets { get; } = new HashSet<string>();
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
public override async Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
var bindings = service.Outputs.OfType<ComputedBindings>().FirstOrDefault();
if (bindings is null)
@ -86,17 +86,17 @@ namespace Microsoft.Tye
if (!Interactive)
{
throw new CommandException(
$"The secret '{secretInputBinding.Name}' used for service '{secretInputBinding.Service.Service.Name}' is missing from the deployment environment. " +
$"The secret '{secretInputBinding.Name}' used for service '{secretInputBinding.Service.Name}' is missing from the deployment environment. " +
$"Rerun the command with --interactive to specify the value interactively, or with --force to skip validation. Alternatively " +
$"use the following command to manually create the secret." + System.Environment.NewLine +
$"kubectl create secret generic {secretInputBinding.Name} --from-literal=connectionstring=<value>");
}
// If we get here then we should create the sceret.
var text = output.Prompt($"Enter the connection string to use for service '{secretInputBinding.Service.Service.Name}'", allowEmpty: true);
var text = output.Prompt($"Enter the connection string to use for service '{secretInputBinding.Service.Name}'", allowEmpty: true);
if (string.IsNullOrWhiteSpace(text))
{
output.WriteAlways($"Skipping creation of secret for '{secretInputBinding.Service.Service.Name}'. This may prevent creation of pods until secrets are created.");
output.WriteAlways($"Skipping creation of secret for '{secretInputBinding.Service.Name}'. This may prevent creation of pods until secrets are created.");
output.WriteAlways($"Manually create a secret with:");
output.WriteAlways($"kubectl create secret generic {secretInputBinding.Name} --from-literal=connectionstring=<value>");
continue;
@ -128,7 +128,7 @@ namespace Microsoft.Tye
var yaml = service.Outputs.OfType<IYamlManifestOutput>().ToArray();
if (yaml.Length == 0)
{
output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping.");
output.WriteDebugLine($"No yaml manifests found for service '{service.Name}'. Skipping.");
return;
}
@ -163,7 +163,7 @@ namespace Microsoft.Tye
throw new CommandException("'kubectl apply' failed.");
}
output.WriteInfoLine($"Deployed service '{service.FriendlyName}'.");
output.WriteInfoLine($"Deployed service '{service.Name}'.");
}
}
}

19
src/Microsoft.Tye.Core/VolumeBuilder.cs

@ -0,0 +1,19 @@
// 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.
namespace Microsoft.Tye
{
public sealed class VolumeBuilder
{
public VolumeBuilder(string source, string target)
{
Source = source;
Target = target;
}
public string Source { get; set; }
public string Target { get; set; }
}
}

8
src/Microsoft.Tye.Core/WriteServiceYamlStep.cs

@ -18,20 +18,20 @@ namespace Microsoft.Tye
public DirectoryInfo OutputDirectory { get; set; } = default!;
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
public override Task ExecuteAsync(OutputContext output, ApplicationBuilder application, ServiceBuilder service)
{
var yaml = service.Outputs.OfType<IYamlManifestOutput>().ToArray();
if (yaml.Length == 0)
{
output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping.");
output.WriteDebugLine($"No yaml manifests found for service '{service.Name}'. Skipping.");
return Task.CompletedTask;
}
var outputFilePath = Path.Combine(OutputDirectory.FullName, $"{service.Service.Name}.yaml");
var outputFilePath = Path.Combine(OutputDirectory.FullName, $"{service.Name}.yaml");
output.WriteInfoLine($"Writing output to '{outputFilePath}'.");
if (File.Exists(outputFilePath) && !Force)
{
throw new CommandException($"'{service.Service.Name}.yaml' already exists for project. use '--force' to overwrite.");
throw new CommandException($"'{service.Name}.yaml' already exists for project. use '--force' to overwrite.");
}
File.Delete(outputFilePath);

78
src/tye/ConfigModel/ConfigApplication.cs → src/tye/ApplicationBuilderExtensions.cs

@ -4,61 +4,68 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Tye.Hosting.Model;
using YamlDotNet.Serialization;
namespace Microsoft.Tye.ConfigModel
{
public class ConfigApplication
public static class ApplicationBuilderExtensions
{
// This gets set by all of the code paths that read the application
[YamlIgnore]
public FileInfo Source { get; set; } = default!;
public string? Name { get; set; }
public string? Registry { get; set; }
public List<ConfigIngress> Ingress { get; set; } = new List<ConfigIngress>();
public List<ConfigService> Services { get; set; } = new List<ConfigService>();
public Tye.Hosting.Model.Application ToHostingApplication()
public static Tye.Hosting.Model.Application ToHostingApplication(this ApplicationBuilder application)
{
var services = new Dictionary<string, Tye.Hosting.Model.Service>();
foreach (var service in Services)
foreach (var service in application.Services)
{
RunInfo? runInfo;
if (service.External)
int replicas;
var env = new List<ConfigurationSource>();
if (service is ExternalServiceBuilder)
{
runInfo = null;
replicas = 1;
}
else if (service.Image is object)
else if (service is ContainerServiceBuilder container)
{
var dockerRunInfo = new DockerRunInfo(service.Image, service.Args);
var dockerRunInfo = new DockerRunInfo(container.Image, container.Args);
foreach (var mapping in service.Volumes)
foreach (var mapping in container.Volumes)
{
dockerRunInfo.VolumeMappings[mapping.Source!] = mapping.Target!;
}
runInfo = dockerRunInfo;
replicas = container.Replicas;
foreach (var entry in container.EnvironmentVariables)
{
env.Add(new ConfigurationSource(entry.Name, entry.Value));
}
}
else if (service.Executable is object)
else if (service is ExecutableServiceBuilder executable)
{
runInfo = new ExecutableRunInfo(service.Executable, service.WorkingDirectory, service.Args);
runInfo = new ExecutableRunInfo(executable.Executable, executable.WorkingDirectory, executable.Args);
replicas = executable.Replicas;
foreach (var entry in executable.EnvironmentVariables)
{
env.Add(new ConfigurationSource(entry.Name, entry.Value));
}
}
else if (service.Project is object)
else if (service is ProjectServiceBuilder project)
{
var projectInfo = new ProjectRunInfo(service.Project, service.Args, service.Build ?? true);
var projectInfo = new ProjectRunInfo(project.ProjectFile.FullName, project.Args, project.Build);
foreach (var mapping in service.Volumes)
foreach (var mapping in project.Volumes)
{
projectInfo.VolumeMappings[mapping.Source!] = mapping.Target!;
}
runInfo = projectInfo;
replicas = project.Replicas;
foreach (var entry in project.EnvironmentVariables)
{
env.Add(new ConfigurationSource(entry.Name, entry.Value));
}
}
else
{
@ -67,8 +74,9 @@ namespace Microsoft.Tye.ConfigModel
var description = new Tye.Hosting.Model.ServiceDescription(service.Name, runInfo)
{
Replicas = service.Replicas ?? 1,
Replicas = replicas,
};
description.Configuration.AddRange(env);
foreach (var binding in service.Bindings)
{
@ -84,28 +92,24 @@ namespace Microsoft.Tye.ConfigModel
});
}
foreach (var entry in service.Configuration)
{
description.Configuration.Add(new ConfigurationSource(entry.Name, entry.Value));
}
services.Add(service.Name, new Tye.Hosting.Model.Service(description));
}
foreach (var ingress in Ingress)
// Ingress get turned into services for hosting
foreach (var ingress in application.Ingress)
{
var rules = new List<IngressRule>();
var rules = new List<Tye.Hosting.Model.IngressRule>();
foreach (var rule in ingress.Rules)
{
rules.Add(new IngressRule(rule.Host, rule.Path, rule.Service!));
rules.Add(new Tye.Hosting.Model.IngressRule(rule.Host, rule.Path, rule.Service!));
}
var runInfo = new IngressRunInfo(rules);
var description = new Tye.Hosting.Model.ServiceDescription(ingress.Name, runInfo)
{
Replicas = ingress.Replicas ?? 1,
Replicas = ingress.Replicas,
};
foreach (var binding in ingress.Bindings)
@ -122,7 +126,7 @@ namespace Microsoft.Tye.ConfigModel
services.Add(ingress.Name, new Tye.Hosting.Model.Service(description));
}
return new Tye.Hosting.Model.Application(Source, services);
return new Tye.Hosting.Model.Application(application.Source, services);
}
}
}

18
src/tye/BuildHost.cs

@ -5,23 +5,21 @@
using System.Collections.Generic;
using System.CommandLine;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Tye.ConfigModel;
namespace Microsoft.Tye
{
public static class BuildHost
{
public static Task BuildAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive)
public static async Task BuildAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive)
{
var application = ConfigFactory.FromFile(path);
return ExecuteBuildAsync(new OutputContext(console, verbosity), application, environment: "production", interactive);
var output = new OutputContext(console, verbosity);
var application = await ApplicationFactory.CreateAsync(output, path);
await ExecuteBuildAsync(output, application, environment: "production", interactive);
}
public static async Task ExecuteBuildAsync(OutputContext output, ConfigApplication application, string environment, bool interactive)
public static async Task ExecuteBuildAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive)
{
var temporaryApplication = await Program.CreateApplicationAdapterAsync(output, application, interactive, requireRegistry: false);
var steps = new List<ServiceExecutor.Step>()
{
new CombineStep() { Environment = environment, },
@ -29,8 +27,10 @@ namespace Microsoft.Tye
new BuildDockerImageStep() { Environment = environment, },
};
var executor = new ServiceExecutor(output, temporaryApplication, steps);
foreach (var service in temporaryApplication.Services)
Program.ApplyRegistryAndDefaults(output, application, interactive, requireRegistry: false);
var executor = new ServiceExecutor(output, application, steps);
foreach (var service in application.Services)
{
await executor.ExecuteAsync(service);
}

23
src/tye/GenerateHost.cs

@ -7,21 +7,20 @@ using System.CommandLine;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Tye.ConfigModel;
namespace Microsoft.Tye
{
public static class GenerateHost
{
public static Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive)
public static async Task GenerateAsync(IConsole console, FileInfo path, Verbosity verbosity, bool interactive)
{
var application = ConfigFactory.FromFile(path);
return ExecuteGenerateAsync(new OutputContext(console, verbosity), application, environment: "production", interactive);
var output = new OutputContext(console, verbosity);
var application = await ApplicationFactory.CreateAsync(output, path);
await ExecuteGenerateAsync(output, application, environment: "production", interactive);
}
public static async Task ExecuteGenerateAsync(OutputContext output, ConfigApplication application, string environment, bool interactive)
public static async Task ExecuteGenerateAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive)
{
var temporaryApplication = await Program.CreateApplicationAdapterAsync(output, application, interactive, requireRegistry: false);
var steps = new List<ServiceExecutor.Step>()
{
new CombineStep() { Environment = environment, },
@ -31,20 +30,22 @@ namespace Microsoft.Tye
steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, });
var executor = new ServiceExecutor(output, temporaryApplication, steps);
foreach (var service in temporaryApplication.Services)
Program.ApplyRegistryAndDefaults(output, application, interactive, requireRegistry: false);
var executor = new ServiceExecutor(output, application, steps);
foreach (var service in application.Services)
{
await executor.ExecuteAsync(service);
}
await GenerateApplicationManifestAsync(output, temporaryApplication, application.Name ?? application.Source.Directory.Name, environment);
await GenerateApplicationManifestAsync(output, application, environment);
}
private static async Task GenerateApplicationManifestAsync(OutputContext output, Tye.Application application, string applicationName, string environment)
private static async Task GenerateApplicationManifestAsync(OutputContext output, ApplicationBuilder application, string environment)
{
using var step = output.BeginStep("Generating Application Manifests...");
var outputFilePath = Path.GetFullPath(Path.Combine(application.RootDirectory, $"{applicationName}-generate-{environment}.yaml"));
var outputFilePath = Path.GetFullPath(Path.Combine(application.Source.DirectoryName, $"{application.Name}-generate-{environment}.yaml"));
output.WriteInfoLine($"Writing output to '{outputFilePath}'.");
{
File.Delete(outputFilePath);

1
src/tye/InitHost.cs

@ -71,7 +71,6 @@ services:
{
// If the input file is a project or solution then use that as the name
application.Name = Path.GetFileNameWithoutExtension(path.Name).ToLowerInvariant();
application.Ingress = null!;
foreach (var service in application.Services)

157
src/tye/Program.DeployCommand.cs

@ -29,7 +29,7 @@ namespace Microsoft.Tye
Required = false
});
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, bool>((console, path, verbosity, interactive, force) =>
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, bool, bool>(async (console, path, verbosity, interactive, force) =>
{
// Workaround for https://github.com/dotnet/command-line-api/issues/723#issuecomment-593062654
if (path is null)
@ -37,14 +37,15 @@ namespace Microsoft.Tye
throw new CommandException("No project or solution file was found.");
}
var application = ConfigFactory.FromFile(path);
return ExecuteDeployAsync(new OutputContext(console, verbosity), application, environment: "production", interactive, force);
var output = new OutputContext(console, verbosity);
var application = await ApplicationFactory.CreateAsync(output, path);
await ExecuteDeployAsync(new OutputContext(console, verbosity), application, environment: "production", interactive, force);
});
return command;
}
private static async Task ExecuteDeployAsync(OutputContext output, ConfigApplication application, string environment, bool interactive, bool force)
private static async Task ExecuteDeployAsync(OutputContext output, ApplicationBuilder application, string environment, bool interactive, bool force)
{
if (!await KubectlDetector.Instance.IsKubectlInstalled.Value)
{
@ -56,7 +57,6 @@ namespace Microsoft.Tye
throw new CommandException($"Cannot apply manifests because kubectl is not connected to a cluster.");
}
var temporaryApplication = await CreateApplicationAdapterAsync(output, application, interactive, requireRegistry: true);
var steps = new List<ServiceExecutor.Step>()
{
new CombineStep() { Environment = environment, },
@ -69,125 +69,18 @@ namespace Microsoft.Tye
steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, });
steps.Add(new DeployServiceYamlStep() { Environment = environment, });
var executor = new ServiceExecutor(output, temporaryApplication, steps);
foreach (var service in temporaryApplication.Services)
{
await executor.ExecuteAsync(service);
}
await DeployApplicationManifestAsync(output, temporaryApplication, application.Source.Directory.Name, environment);
}
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!
}
ApplyRegistryAndDefaults(output, application, interactive, requireRegistry: true);
foreach (var service in temporaryApplication.Services)
var executor = new ServiceExecutor(output, application, steps);
foreach (var service in application.Services)
{
if (service.Service.Source is Project project && service.Service.GeneratedAssets.Container is ContainerInfo container)
{
DockerfileGenerator.ApplyContainerDefaults(temporaryApplication, service, project, container);
}
await executor.ExecuteAsync(service);
}
return temporaryApplication;
await DeployApplicationManifestAsync(output, application, application.Source.Directory.Name, environment);
}
private static async Task DeployApplicationManifestAsync(OutputContext output, Tye.Application application, string applicationName, string environment)
private static async Task DeployApplicationManifestAsync(OutputContext output, ApplicationBuilder application, string applicationName, string environment)
{
using var step = output.BeginStep("Deploying Application Manifests...");
@ -221,5 +114,33 @@ namespace Microsoft.Tye
step.MarkComplete();
}
internal static void ApplyRegistryAndDefaults(OutputContext output, ApplicationBuilder application, bool interactive, bool requireRegistry)
{
if (application.Registry is 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))
{
application.Registry = new ContainerRegistry(registry.Trim());
}
}
else if (application.Registry is 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 application.Services)
{
if (service is ProjectServiceBuilder project && project.ContainerInfo is ContainerInfo container)
{
DockerfileGenerator.ApplyContainerDefaults(application, project, container);
}
}
}
}
}

3
src/tye/Program.RunCommand.cs

@ -70,7 +70,8 @@ namespace Microsoft.Tye
throw new CommandException("No project or solution file was found.");
}
var application = ConfigFactory.FromFile(path);
var output = new OutputContext(console, Verbosity.Quiet);
var application = await ApplicationFactory.CreateAsync(output, path);
var serviceCount = application.Services.Count;
InitializeThreadPoolSettings(serviceCount);

30
src/tye/TemporaryApplicationAdapter.cs

@ -1,30 +0,0 @@
// 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.Collections.Generic;
using Microsoft.Tye.ConfigModel;
namespace Microsoft.Tye
{
internal class TemporaryApplicationAdapter : Tye.Application
{
private readonly ConfigApplication application;
public TemporaryApplicationAdapter(
ConfigApplication application,
ApplicationGlobals globals,
IReadOnlyList<ServiceEntry> services)
{
this.application = application;
Globals = globals;
Services = services;
}
public override ApplicationGlobals Globals { get; }
public override string RootDirectory => application.Source.DirectoryName;
public override IReadOnlyList<ServiceEntry> Services { get; }
}
}

29
test/E2ETest/TyeBuildTests.cs

@ -5,8 +5,6 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.Tye;
using Microsoft.Tye.ConfigModel;
using Xunit;
using Xunit.Abstractions;
namespace E2ETest
@ -37,13 +35,14 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = "test";
application.Registry = new ContainerRegistry("test");
try
{
await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false);
await DockerAssert.AssertImageExistsAsync(output, "test/test-project");
}
@ -69,13 +68,14 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = "test";
application.Registry = new ContainerRegistry("test");
try
{
await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false);
await DockerAssert.AssertImageExistsAsync(output, "test/backend");
await DockerAssert.AssertImageExistsAsync(output, "test/frontend");
@ -104,13 +104,14 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
application.Registry = "test";
application.Registry = new ContainerRegistry("test");
try
{
await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false);
await DockerAssert.AssertImageExistsAsync(output, "test/backend");
await DockerAssert.AssertImageExistsAsync(output, "test/frontend");
@ -138,12 +139,12 @@ namespace E2ETest
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
try
{
await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await BuildHost.ExecuteBuildAsync(outputContext, application, environment, interactive: false);
await DockerAssert.AssertImageExistsAsync(output, "test-project");
}

30
test/E2ETest/TyeGenerateTests.cs

@ -37,14 +37,15 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry for generate
application.Registry = "test";
application.Registry = new ContainerRegistry("test");
try
{
await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false);
// name of application is the folder
var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml"));
@ -75,14 +76,15 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry for generate
application.Registry = "test";
application.Registry = new ContainerRegistry("test");
try
{
await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false);
// name of application is the folder
var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml"));
@ -90,8 +92,6 @@ namespace E2ETest
Assert.Equal(expectedContent, content);
await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await DockerAssert.AssertImageExistsAsync(output, "test/backend");
await DockerAssert.AssertImageExistsAsync(output, "test/frontend");
}
@ -119,14 +119,15 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
// Need to add docker registry for generate
application.Registry = "test";
application.Registry = new ContainerRegistry("test");
try
{
await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false);
// name of application is the folder
var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml"));
@ -161,11 +162,12 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
var application = ConfigFactory.FromFile(projectFile);
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
try
{
await GenerateHost.ExecuteGenerateAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await GenerateHost.ExecuteGenerateAsync(outputContext, application, environment, interactive: false);
// name of application is the folder
var content = File.ReadAllText(Path.Combine(tempDirectory.DirectoryPath, $"{projectName}-generate-{environment}.yaml"));
@ -173,8 +175,6 @@ namespace E2ETest
Assert.Equal(expectedContent, content);
await BuildHost.ExecuteBuildAsync(new OutputContext(sink, Verbosity.Debug), application, environment, interactive: false);
await DockerAssert.AssertImageExistsAsync(output, "test-project");
}
finally

21
test/E2ETest/TyeRunTests.cs

@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
@ -50,7 +49,9 @@ namespace E2ETest
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "test-project.csproj"));
using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), Array.Empty<string>())
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
using var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>())
{
Sink = sink,
};
@ -108,7 +109,9 @@ namespace E2ETest
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), Array.Empty<string>())
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
using var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>())
{
Sink = sink,
};
@ -143,7 +146,9 @@ namespace E2ETest
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), Array.Empty<string>())
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
using var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>())
{
Sink = sink,
};
@ -211,7 +216,9 @@ namespace E2ETest
DirectoryCopy.Copy(projectDirectory.FullName, tempDirectory.DirectoryPath);
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "tye.yaml"));
using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), new[] { "--docker" })
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
using var host = new TyeHost(application.ToHostingApplication(), new[] { "--docker" })
{
Sink = sink,
};
@ -251,7 +258,9 @@ namespace E2ETest
var projectFile = new FileInfo(Path.Combine(tempDirectory.DirectoryPath, "test-project.csproj"));
// Debug targets can be null if not specified, so make sure calling host.Start does not throw.
using var host = new TyeHost(ConfigFactory.FromFile(projectFile).ToHostingApplication(), Array.Empty<string>(), null!)
var outputContext = new OutputContext(sink, Verbosity.Debug);
var application = await ApplicationFactory.CreateAsync(outputContext, projectFile);
using var host = new TyeHost(application.ToHostingApplication(), Array.Empty<string>())
{
Sink = sink,
};

4
test/E2ETest/testassets/generate/frontend-backend.yaml

@ -47,7 +47,7 @@ spec:
app.kubernetes.io/name: backend
type: ClusterIP
ports:
- name: backend
- name: http
protocol: TCP
port: 80
targetPort: 80
@ -102,7 +102,7 @@ spec:
app.kubernetes.io/name: frontend
type: ClusterIP
ports:
- name: frontend
- name: http
protocol: TCP
port: 80
targetPort: 80

4
test/E2ETest/testassets/generate/multi-project.yaml

@ -58,7 +58,7 @@ spec:
app.kubernetes.io/name: backend
type: ClusterIP
ports:
- name: backend
- name: http
protocol: TCP
port: 7000
targetPort: 7000
@ -124,7 +124,7 @@ spec:
app.kubernetes.io/name: frontend
type: ClusterIP
ports:
- name: frontend
- name: http
protocol: TCP
port: 8000
targetPort: 8000

2
test/E2ETest/testassets/generate/single-project-noregistry.yaml

@ -41,7 +41,7 @@ spec:
app.kubernetes.io/name: test-project
type: ClusterIP
ports:
- name: test-project
- name: http
protocol: TCP
port: 80
targetPort: 80

2
test/E2ETest/testassets/generate/single-project.yaml

@ -41,7 +41,7 @@ spec:
app.kubernetes.io/name: test-project
type: ClusterIP
ports:
- name: test-project
- name: http
protocol: TCP
port: 80
targetPort: 80

Loading…
Cancel
Save