Browse Source

code dump of opulence

davidfowl/dependencies
Ryan Nowak 7 years ago
parent
commit
ab5946c687
  1. 11
      src/opulence/Opulence/ApplicationGlobals.cs
  2. 46
      src/opulence/Opulence/ContainerInfo.cs
  3. 19
      src/opulence/Opulence/ContainerRegistry.cs
  4. 9
      src/opulence/Opulence/DeploymentKind.cs
  5. 19
      src/opulence/Opulence/DockerImage.cs
  6. 19
      src/opulence/Opulence/EnvironmentAttribute.cs
  7. 9
      src/opulence/Opulence/Environments.cs
  8. 19
      src/opulence/Opulence/Framework.cs
  9. 22
      src/opulence/Opulence/FrameworkCollection.cs
  10. 7
      src/opulence/Opulence/GeneratedAssets.cs
  11. 9
      src/opulence/Opulence/HelmChartStep.cs
  12. 13
      src/opulence/Opulence/Opulence.csproj
  13. 53
      src/opulence/Opulence/Pipeline.cs
  14. 19
      src/opulence/Opulence/PipelineHolder.cs
  15. 25
      src/opulence/Opulence/Project.cs
  16. 7
      src/opulence/Opulence/Secret.cs
  17. 34
      src/opulence/Opulence/Service.cs
  18. 35
      src/opulence/Opulence/ServiceBinding.cs
  19. 9
      src/opulence/Opulence/Source.cs
  20. 7
      src/opulence/Opulence/Step.cs
  21. 227
      src/opulence/dotnet-opulence/Application.cs
  22. 142
      src/opulence/dotnet-opulence/ApplicationFactory.cs
  23. 26
      src/opulence/dotnet-opulence/ApplicationYamlWriter.cs
  24. 31
      src/opulence/dotnet-opulence/BuildDockerImageStep.cs
  25. 97
      src/opulence/dotnet-opulence/CombineStep.cs
  26. 15
      src/opulence/dotnet-opulence/CommandException.cs
  27. 67
      src/opulence/dotnet-opulence/ConsoleExtensions.cs
  28. 159
      src/opulence/dotnet-opulence/CustomizationPipeline.cs
  29. 136
      src/opulence/dotnet-opulence/DeployCommand.cs
  30. 62
      src/opulence/dotnet-opulence/DeployServiceYamlStep.cs
  31. 28
      src/opulence/dotnet-opulence/DirectoryCopy.cs
  32. 81
      src/opulence/dotnet-opulence/DirectorySearch.cs
  33. 70
      src/opulence/dotnet-opulence/DockerContainerBuilder.cs
  34. 42
      src/opulence/dotnet-opulence/DockerPush.cs
  35. 142
      src/opulence/dotnet-opulence/DockerfileGenerator.cs
  36. 59
      src/opulence/dotnet-opulence/GenerateCommand.cs
  37. 43
      src/opulence/dotnet-opulence/GenerateDockerfileStep.cs
  38. 47
      src/opulence/dotnet-opulence/GenerateHelmChartStep.cs
  39. 29
      src/opulence/dotnet-opulence/GenerateKubernetesManifestStep.cs
  40. 29
      src/opulence/dotnet-opulence/GenerateOamComponentStep.cs
  41. 83
      src/opulence/dotnet-opulence/HelmChartBuilder.cs
  42. 111
      src/opulence/dotnet-opulence/HelmChartGenerator.cs
  43. 52
      src/opulence/dotnet-opulence/HelmPush.cs
  44. 9
      src/opulence/dotnet-opulence/IYamlManifestOutput.cs
  45. 193
      src/opulence/dotnet-opulence/InitCommand.cs
  46. 27
      src/opulence/dotnet-opulence/KubernetesDeploymentOutput.cs
  47. 196
      src/opulence/dotnet-opulence/KubernetesManifestGenerator.cs
  48. 27
      src/opulence/dotnet-opulence/KubernetesServiceOutput.cs
  49. 32
      src/opulence/dotnet-opulence/Names.cs
  50. 84
      src/opulence/dotnet-opulence/OamApplicationGenerator.cs
  51. 54
      src/opulence/dotnet-opulence/OamComponentGenerator.cs
  52. 27
      src/opulence/dotnet-opulence/OamComponentOutput.cs
  53. 256
      src/opulence/dotnet-opulence/OutputContext.cs
  54. 119
      src/opulence/dotnet-opulence/PackageCommand.cs
  55. 82
      src/opulence/dotnet-opulence/Program.cs
  56. 118
      src/opulence/dotnet-opulence/ProjectReader.cs
  57. 8
      src/opulence/dotnet-opulence/Properties/launchSettings.json
  58. 55
      src/opulence/dotnet-opulence/PushCommand.cs
  59. 36
      src/opulence/dotnet-opulence/PushDockerImageStep.cs
  60. 8
      src/opulence/dotnet-opulence/Resources/Imports.targets
  61. 166
      src/opulence/dotnet-opulence/ScriptRunner.cs
  62. 137
      src/opulence/dotnet-opulence/ServiceExecutor.cs
  63. 184
      src/opulence/dotnet-opulence/StandardOptions.cs
  64. 7
      src/opulence/dotnet-opulence/StepView.cs
  65. 47
      src/opulence/dotnet-opulence/TargetInstaller.cs
  66. 27
      src/opulence/dotnet-opulence/TempDirectory.cs
  67. 26
      src/opulence/dotnet-opulence/TempFile.cs
  68. 25
      src/opulence/dotnet-opulence/Templates.cs
  69. 45
      src/opulence/dotnet-opulence/Templates/Helm/templates/_helpers.tpl
  70. 30
      src/opulence/dotnet-opulence/Templates/Helm/templates/deployment.yaml
  71. 16
      src/opulence/dotnet-opulence/Templates/Helm/templates/service.yaml
  72. 9
      src/opulence/dotnet-opulence/Verbosity.cs
  73. 43
      src/opulence/dotnet-opulence/WriteServiceYamlStep.cs
  74. 28
      src/opulence/dotnet-opulence/build/Opulence.targets
  75. 20
      src/opulence/dotnet-opulence/buildMultiTargeting/Opulence.targets
  76. 39
      src/opulence/dotnet-opulence/dotnet-opulence.csproj
  77. 33
      tye.sln

11
src/opulence/Opulence/ApplicationGlobals.cs

@ -0,0 +1,11 @@
namespace Opulence
{
public sealed class ApplicationGlobals
{
public DeploymentKind DeploymentKind { get; set; } = DeploymentKind.Kubernetes;
public string? Name { get; set; }
public ContainerRegistry? Registry { get; set; }
}
}

46
src/opulence/Opulence/ContainerInfo.cs

@ -0,0 +1,46 @@
namespace Opulence
{
public sealed class ContainerInfo
{
/// <summary>
/// Gets or sets the name of the base image. If null, the base image will be chosen
/// based on the project configuration.
/// </summary>
public string? BaseImageName { get; set; }
/// <summary>
/// Gets or sets the name of the base image tag. If null, the base image tag will be chosen
/// based on the project configuration.
/// </summary>
public string? BaseImageTag { get; set; }
/// <summary>
/// Gets or sets the name of the build image. If null, the build image will be chosen
/// based on the project configuration.
/// </summary>
public string? BuildImageName { get; set; }
/// <summary>
/// Gets or sets the name of the base image tag. If null, the build image tag will be chosen
/// based on the project configuration.
/// </summary>
public string? BuildImageTag { get; set; }
/// <summary>
/// Gets or sets the name of the image. If null, the image name will be chosen
/// based on the project name.
/// </summary>
public string? ImageName { get; set; }
/// <summary>
/// Gets or sets the tag of the image. If null, the image tag will be chosen
/// based on the project version.
/// </summary>
public string? ImageTag { get; set; }
/// <summary>
/// Gets or a sets value which determines whether a multiphase dockerfile is used.
/// </summary>
public bool? UseMultiphaseDockerfile { get; set; }
}
}

19
src/opulence/Opulence/ContainerRegistry.cs

@ -0,0 +1,19 @@
using System;
namespace Opulence
{
public sealed class ContainerRegistry
{
public ContainerRegistry(string hostname)
{
if (hostname is null)
{
throw new ArgumentNullException(nameof(hostname));
}
Hostname = hostname;
}
public string Hostname { get; }
}
}

9
src/opulence/Opulence/DeploymentKind.cs

@ -0,0 +1,9 @@
namespace Opulence
{
public enum DeploymentKind
{
None,
Kubernetes,
Oam,
}
}

19
src/opulence/Opulence/DockerImage.cs

@ -0,0 +1,19 @@
using System;
namespace Opulence
{
public sealed class DockerImage : Source
{
public DockerImage(string image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
Image = image;
}
public string Image { get; }
}
}

19
src/opulence/Opulence/EnvironmentAttribute.cs

@ -0,0 +1,19 @@
using System;
namespace Opulence
{
public class EnvironmentAttribute : Attribute
{
public EnvironmentAttribute(string environmentName)
{
if (environmentName is null)
{
throw new ArgumentNullException(nameof(environmentName));
}
EnvironmentName = environmentName;
}
public string EnvironmentName { get; }
}
}

9
src/opulence/Opulence/Environments.cs

@ -0,0 +1,9 @@
namespace Opulence
{
public static class Environments
{
public const string Development = "Development";
public const string Production = "Production";
}
}

19
src/opulence/Opulence/Framework.cs

@ -0,0 +1,19 @@
using System;
namespace Opulence
{
public sealed class Framework
{
public Framework(string name)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
}
public string Name { get; }
}
}

22
src/opulence/Opulence/FrameworkCollection.cs

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Opulence
{
public sealed class FrameworkCollection : Collection<Framework>
{
public void AddRange(IEnumerable<Framework> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
foreach (var item in items)
{
Add(item);
}
}
}
}

7
src/opulence/Opulence/GeneratedAssets.cs

@ -0,0 +1,7 @@
namespace Opulence
{
public sealed class GeneratedAssets
{
public ContainerInfo? Container { get; set; }
}
}

9
src/opulence/Opulence/HelmChartStep.cs

@ -0,0 +1,9 @@
namespace Opulence
{
public sealed class HelmChartStep : Step
{
public override string DisplayName => "Building Helm Chart...";
public string ChartName { get; set; } = default!;
}
}

13
src/opulence/Opulence/Opulence.csproj

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
<Nullable>Enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="4.7.0" />
</ItemGroup>
</Project>

53
src/opulence/Opulence/Pipeline.cs

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Opulence
{
public abstract class Pipeline
{
private static readonly Dictionary<Type, List<MulticastDelegate>> callbacks = new Dictionary<Type, List<MulticastDelegate>>();
public static void Configure<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);
}
public static Task<object?> ExecuteAsync(Pipeline pipeline)
{
if (pipeline is null)
{
throw new ArgumentNullException(nameof(pipeline));
}
foreach (var kvp in callbacks)
{
var type = kvp.Key;
var delegates = kvp.Value;
var method = pipeline.GetType().GetMethod("Register").MakeGenericMethod(type);
for (var i = 0; i < delegates.Count; i++)
{
method.Invoke(pipeline, new[]{ delegates[i], });
}
}
return pipeline.ExecuteAsync();
}
public abstract void Register<TApplication>(Action<TApplication> callback);
public abstract Task<object?> ExecuteAsync();
}
}

19
src/opulence/Opulence/PipelineHolder.cs

@ -0,0 +1,19 @@
using System;
namespace Opulence
{
public sealed class PipelineHolder
{
public PipelineHolder(Pipeline pipeline)
{
if (pipeline is null)
{
throw new ArgumentNullException(nameof(pipeline));
}
__Pipeline = pipeline;
}
public Pipeline __Pipeline { get; }
}
}

25
src/opulence/Opulence/Project.cs

@ -0,0 +1,25 @@
using System;
namespace Opulence
{
public sealed class Project : Source
{
public Project(string relativeFilePath)
{
if (relativeFilePath is null)
{
throw new ArgumentNullException(nameof(relativeFilePath));
}
RelativeFilePath = relativeFilePath;
}
public string RelativeFilePath { get; }
public FrameworkCollection Frameworks { get; } = new FrameworkCollection();
public string TargetFramework { get; set; } = default!;
public string Version { get; set; } = default!;
}
}

7
src/opulence/Opulence/Secret.cs

@ -0,0 +1,7 @@
namespace Opulence
{
public class Secret
{
public string? Name { get; set; }
}
}

34
src/opulence/Opulence/Service.cs

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
namespace Opulence
{
public sealed class Service
{
public Service(string name)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
}
public string Name { get; }
public GeneratedAssets GeneratedAssets { get; } = new GeneratedAssets();
public int? Port { get; set; }
public string? Protocol { get; set; }
public Source? Source { get; set; }
public Dictionary<string, object> Environment { get; set; } = new Dictionary<string, object>();
public int Replicas { get; set; } = 1;
public List<ServiceBinding> Bindings { get; } = new List<ServiceBinding>();
}
}

35
src/opulence/Opulence/ServiceBinding.cs

@ -0,0 +1,35 @@
using System;
namespace Opulence
{
public class ServiceBinding
{
public ServiceBinding(string name)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
}
public ServiceBinding(Service service)
{
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
Name = service.Name;
// We don't copy other properties here because a "hardcoded" value of Protocol/Port is treated differently
// from just binding the name when considering environments.
}
public string Name { get; set; }
public string? Protocol { get; set; }
public int? Port { get; set; }
public Secret? ConnectionString { get; set; }
}
}

9
src/opulence/Opulence/Source.cs

@ -0,0 +1,9 @@
namespace Opulence
{
public abstract class Source
{
internal Source()
{
}
}
}

7
src/opulence/Opulence/Step.cs

@ -0,0 +1,7 @@
namespace Opulence
{
public abstract class Step
{
public abstract string DisplayName { get; }
}
}

227
src/opulence/dotnet-opulence/Application.cs

@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace Opulence
{
internal 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))!;
}
}
internal 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);
}
internal 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; }
}
}

142
src/opulence/dotnet-opulence/ApplicationFactory.cs

@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
namespace Opulence
{
internal static class ApplicationFactory
{
public static async Task<Application> CreateApplicationAsync(OutputContext output, FileInfo projectFile)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (projectFile is null)
{
throw new ArgumentNullException(nameof(projectFile));
}
if (string.Equals(projectFile.Extension, ".sln", StringComparison.Ordinal))
{
output.WriteDebugLine($"Solution '{projectFile.FullName}' was provided as input.");
return await CreateApplicationForSolutionAsync(output, projectFile);
}
else
{
output.WriteDebugLine($"Project '{projectFile.FullName}' was provided as input.");
return await CreateApplicationForProjectAsync(output, projectFile);
}
}
private static async Task<Application> CreateApplicationForProjectAsync(OutputContext output, FileInfo projectFile)
{
// Project workflow:
//
// 1. Determine if there's an 'Opulence.csx' - use that to initialize the set of services.
// 2. If there's not an 'Opulence.csx' then move on with just the project.
var application = await ScriptRunner.RunCustomizationScriptAsync(output, projectFile);
if (application != null)
{
return application;
}
return await InferApplicationForProject(output, projectFile);
}
private static async Task<Application> InferApplicationForProject(OutputContext output, FileInfo projectFile)
{
var globals = new ApplicationGlobals();
globals.Name = Names.NormalizeToDns(Path.GetFileNameWithoutExtension(projectFile.Name));
var services = new List<ServiceEntry>();
var name = Path.GetFileNameWithoutExtension(projectFile.Name);
var project = new Project(projectFile.FullName);
var service = new ServiceEntry(new Service(name), name);
service.Service.Source = project;
services.Add(service);
await ProjectReader.ReadProjectDetailsAsync(output, projectFile, project);
var container = new ContainerInfo();
service.Service.GeneratedAssets.Container = container;
var application = new GroveledApplication(globals, projectFile.DirectoryName, services);
DockerfileGenerator.ApplyContainerDefaults(application, service, project, container);
return application;
}
private static async Task<Application> CreateApplicationForSolutionAsync(OutputContext output, FileInfo solutionFile)
{
// Solution workflow:
//
// 1. If there's an 'Opulence.csx' - use that that to initialize the set of services.
// 2. If there's not an 'Opulence.csx' then grovel all of the projects in the solution looking
// for executable projects.
SolutionFile solution;
try
{
solution = SolutionFile.Parse(solutionFile.FullName);
}
catch (Exception ex)
{
throw new CommandException($"Parsing solution file '{solutionFile.FullName}' failed.", ex);
}
var application = await ScriptRunner.RunCustomizationScriptAsync(output, solutionFile, solution);
if (application != null)
{
return application;
}
return await InferApplicationForSolution(output, solutionFile, solution);
}
private static async Task<Application> InferApplicationForSolution(OutputContext output, FileInfo solutionFile, SolutionFile solution)
{
var globals = new ApplicationGlobals();
globals.Name = Names.NormalizeToDns(Path.GetFileNameWithoutExtension(solutionFile.Name));
var services = new List<ServiceEntry>();
for (var i = 0; i < solution.ProjectsInOrder.Count; i++)
{
// The library we're using doesn't translate Windows style paths automatically.
var solutionProject = solution.ProjectsInOrder[i];
if (solutionProject.AbsolutePath.EndsWith(".csproj", StringComparison.Ordinal))
{
var projectFilePath = solutionProject.RelativePath.Replace('\\', Path.DirectorySeparatorChar);
var projectFile = new FileInfo(projectFilePath);
var name = Path.GetFileNameWithoutExtension(projectFile.Name);
var project = new Project(projectFile.FullName);
var service = new ServiceEntry(new Service(name), name);
service.Service.Source = project;
services.Add(service);
await ProjectReader.ReadProjectDetailsAsync(output, projectFile, project);
var container = new ContainerInfo();
service.Service.GeneratedAssets.Container = container;
}
}
var application = new GroveledApplication(globals, solutionFile.DirectoryName, services);
foreach (var service in application.Services)
{
DockerfileGenerator.ApplyContainerDefaults(application, service, (Project)service.Service.Source!, service.Service.GeneratedAssets.Container!);
}
return application;
}
}
}

26
src/opulence/dotnet-opulence/ApplicationYamlWriter.cs

@ -0,0 +1,26 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal sealed class ApplicationYamlWriter
{
public static Task WriteAsync(OutputContext output, StreamWriter writer, Application application)
{
var yaml = application.Services.SelectMany(s => s.Outputs.OfType<IYamlManifestOutput>()).ToArray();
if (yaml.Length == 0)
{
output.WriteDebugLine($"No yaml manifests found. Skipping.");
return Task.CompletedTask;
}
var yamlStream = new YamlStream(yaml.Select(y => y.Yaml));
yamlStream.Save(writer, assignAnchors: false);
return Task.CompletedTask;
}
}
}

31
src/opulence/dotnet-opulence/BuildDockerImageStep.cs

@ -0,0 +1,31 @@
using System.Threading.Tasks;
namespace Opulence
{
internal sealed class BuildDockerImageStep : ServiceExecutor.Step
{
public override string DisplayText => "Building Docker Image...";
public string Environment { get; set; } = "production";
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{
if (SkipWithoutProject(output, service, out var project))
{
return;
}
if (SkipWithoutContainerInfo(output, service, out var container))
{
return;
}
if (SkipForEnvironment(output, service, Environment))
{
return;
}
await DockerContainerBuilder.BuildContainerImageAsync(output, application, service, project, container);
}
}
}

97
src/opulence/dotnet-opulence/CombineStep.cs

@ -0,0 +1,97 @@
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal sealed class CombineStep : ServiceExecutor.Step
{
public override string DisplayText => "Compiling Services...";
public string Environment { get; set; } = "production";
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{
// Process bindings and turn them into environment variables.
foreach (var binding in service.Service.Bindings)
{
// Try to see if the other project is a service in this application. This can help with
// heuristics if the binding doesn't have all possible info specified.
var other = application.Services.FirstOrDefault(s => s.Service.Name == binding.Name);
var key = $"SERVICES__{binding.Name}";
// Find the value that needs to go in this env-var (in priority order).
string value;
if (binding.ConnectionString != null)
{
binding.ConnectionString.Name ??= $"binding-{binding.Name}";
// It's a secret! We don't use env-vars for this.
continue;
}
else if (binding.Protocol != null)
{
// This is fully specified as a URL.
value = ResolveUri(binding.Protocol, binding.Name, binding.Port);
}
else if (other?.Service.Source != null && other.AppliesToEnvironment(Environment))
{
// If we get here it means that the other service is built from source.
// In this case we'll assume that it's http on 80 as a reasonable guess.
value = ResolveUri(other.Service.Protocol ?? "http", binding.Name, other.Service.Port);
}
else if (other?.Service.Source != null)
{
// The other service is built from source, but doesn't apply to this environment.
// This is likely user-error.
throw new CommandException($"Unable to resolve the uri for binding '{binding.Name}'.");
}
else if (other?.Service.Source == null)
{
// The service isn't built from source.
binding.ConnectionString = new Secret() { Name = $"binding-{binding.Name}", };
// It's a secret! We don't use env-vars for this.
continue;
}
else
{
// Generic catch all case. We don't expect this to get hit because the three blocks
// above cover all possibilities, but the compiler doesn't agree.
throw new CommandException($"Unable to resolve the uri for binding '{binding.Name}'.");
}
service.Service.Environment[key] = value;
}
return Task.CompletedTask;
}
private static string ResolveUri(string protocol, string name, int? port)
{
if (protocol == "http" && (port == 80 || port == null))
{
return $"{protocol}://{name}";
}
else if (protocol == "https" && (port == 443 || port == null))
{
return $"{protocol}://{name}";
}
else if (port == null)
{
return $"{protocol}://{name}";
}
else
{
return $"{protocol}://{name}:{port}";
}
}
}
}

15
src/opulence/dotnet-opulence/CommandException.cs

@ -0,0 +1,15 @@
using System;
namespace Opulence
{
internal class CommandException : Exception
{
public CommandException(string message)
: base(message)
{ }
public CommandException(string message, Exception inner)
: base(message, inner)
{ }
}
}

67
src/opulence/dotnet-opulence/ConsoleExtensions.cs

@ -0,0 +1,67 @@
using System.Linq;
namespace System.CommandLine
{
internal static class ConsoleExtensions
{
private static bool? _isConsoleRedirectionCheckSupported;
private static bool IsConsoleRedirectionCheckSupported
{
get
{
if (_isConsoleRedirectionCheckSupported == null)
{
try
{
var check = Console.IsOutputRedirected;
_isConsoleRedirectionCheckSupported = true;
}
catch (PlatformNotSupportedException)
{
_isConsoleRedirectionCheckSupported = false;
}
}
return _isConsoleRedirectionCheckSupported.Value;
}
}
public static void SetTerminalForegroundColor(this IConsole console, ConsoleColor color)
{
if (console.GetType().GetInterfaces().Any(i => i.Name == "ITerminal"))
{
((dynamic)console).ForegroundColor = color;
}
if (IsConsoleRedirectionCheckSupported &&
!Console.IsOutputRedirected)
{
Console.ForegroundColor = color;
}
else if (IsConsoleRedirectionCheckSupported)
{
Console.ForegroundColor = color;
}
}
public static void ResetTerminalForegroundColor(this IConsole console)
{
if (console.GetType().GetInterfaces().Any(i => i.Name == "ITerminal"))
{
((dynamic)console).ForegroundColor = ConsoleColor.Red;
}
if (IsConsoleRedirectionCheckSupported &&
!Console.IsOutputRedirected)
{
Console.ResetColor();
}
else if (IsConsoleRedirectionCheckSupported)
{
Console.ResetColor();
}
}
}
}

159
src/opulence/dotnet-opulence/CustomizationPipeline.cs

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
namespace Opulence
{
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;
}
}
}

136
src/opulence/dotnet-opulence/DeployCommand.cs

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace Opulence
{
public class DeployCommand
{
public static Command Create()
{
var command = new Command("deploy", "Deploy the application to Kubernetes")
{
StandardOptions.Project,
StandardOptions.Verbosity,
StandardOptions.Environment,
};
command.Handler = CommandHandler.Create<IConsole, FileInfo, string, Verbosity>((console, project, environment, verbosity) =>
{
return ExecuteAsync(new OutputContext(console, verbosity), project, environment);
});
return command;
}
private static async Task ExecuteAsync(OutputContext output, FileInfo projectFile, string environment)
{
output.WriteBanner();
var application = await ApplicationFactory.CreateApplicationAsync(output, projectFile);
if (application.Globals.Registry?.Hostname == null)
{
throw new CommandException("A registry is required for deploy operations. run 'dotnet-opulence init'.");
}
var steps = new List<ServiceExecutor.Step>()
{
new CombineStep() { Environment = environment, },
new BuildDockerImageStep() { Environment = environment, },
new PushDockerImageStep() { Environment = environment, },
};
if (application.Globals.DeploymentKind == DeploymentKind.None)
{
// No extra steps
}
else if (application.Globals.DeploymentKind == DeploymentKind.Kubernetes)
{
steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, });
}
else if (application.Globals.DeploymentKind == DeploymentKind.Oam)
{
steps.Add(new GenerateOamComponentStep() { Environment = environment, });
}
else
{
throw new InvalidOperationException($"Unknown DeploymentKind: " + application.Globals.DeploymentKind);
}
// If this is command is for a project, then deploy the component manifest
// for just the project. We won't run the "application deploy" part.
if (!string.Equals(".sln", projectFile.Extension, StringComparison.Ordinal))
{
steps.Add(new DeployServiceYamlStep() { Environment = environment, });
}
var executor = new ServiceExecutor(output, application, steps);
foreach (var service in application.Services)
{
if (service.IsMatchForProject(application, projectFile))
{
await executor.ExecuteAsync(service);
}
}
if (string.Equals(".sln", projectFile.Extension, StringComparison.Ordinal))
{
await PackageApplicationAsync(output, application, Path.GetFileNameWithoutExtension(projectFile.Name), environment);
}
}
private static async Task PackageApplicationAsync(OutputContext output, Application application, string applicationName, string environment)
{
using var step = output.BeginStep("Deploying Application Manifests...");
using var tempFile = TempFile.Create();
output.WriteInfoLine($"Writing output to '{tempFile.FilePath}'.");
{
using var stream = File.OpenWrite(tempFile.FilePath);
using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
if (application.Globals.DeploymentKind == DeploymentKind.None)
{
// No extra steps
}
else if (application.Globals.DeploymentKind == DeploymentKind.Kubernetes)
{
await ApplicationYamlWriter.WriteAsync(output, writer, application);
}
else if (application.Globals.DeploymentKind == DeploymentKind.Oam)
{
await OamApplicationGenerator.WriteOamApplicationAsync(writer, output, application, applicationName, environment);
}
else
{
throw new InvalidOperationException($"Unknown DeploymentKind: " + application.Globals.DeploymentKind);
}
}
output.WriteDebugLine("Running 'kubectl apply'.");
output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
$"kubectl",
$"apply -f \"{tempFile.FilePath}\"",
System.Environment.CurrentDirectory,
stdOut: capture.StdOut,
stdErr: capture.StdErr);
output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}");
if (exitCode != 0)
{
throw new CommandException("'kubectl apply' failed.");
}
output.WriteInfoLine($"Deployed application '{applicationName}'.");
step.MarkComplete();
}
}
}

62
src/opulence/dotnet-opulence/DeployServiceYamlStep.cs

@ -0,0 +1,62 @@
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal sealed class DeployServiceYamlStep : ServiceExecutor.Step
{
public override string DisplayText => "Deploying Manifests...";
public string Environment { get; set; } = "production";
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{
var yaml = service.Outputs.OfType<IYamlManifestOutput>().ToArray();
if (yaml.Length == 0)
{
output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping.");
return;
}
using var tempFile = TempFile.Create();
output.WriteDebugLine($"Writing output to '{tempFile.FilePath}'.");
{
using var stream = File.OpenWrite(tempFile.FilePath);
using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
var yamlStream = new YamlStream(yaml.Select(y => y.Yaml));
yamlStream.Save(writer, assignAnchors: false);
}
// kubectl apply logic is implemented in the client in older versions of k8s. The capability
// to get the same behavior in the server isn't present in every version that's relevant.
//
// https://kubernetes.io/docs/reference/using-api/api-concepts/#server-side-apply
//
output.WriteDebugLine("Running 'kubectl apply'.");
output.WriteCommandLine("kubectl", $"apply -f \"{tempFile.FilePath}\"");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
$"kubectl",
$"apply -f \"{tempFile.FilePath}\"",
System.Environment.CurrentDirectory,
stdOut: capture.StdOut,
stdErr: capture.StdErr);
output.WriteDebugLine($"Done running 'kubectl apply' exit code: {exitCode}");
if (exitCode != 0)
{
throw new CommandException("'kubectl apply' failed.");
}
output.WriteInfoLine($"Deployed service '{service.FriendlyName}'.");
}
}
}

28
src/opulence/dotnet-opulence/DirectoryCopy.cs

@ -0,0 +1,28 @@
using System.IO;
namespace Opulence
{
public static class DirectoryCopy
{
public static void Copy(string source, string destination)
{
CopyCore(new DirectoryInfo(source), new DirectoryInfo(destination));
}
private static void CopyCore(DirectoryInfo source, DirectoryInfo destination)
{
Directory.CreateDirectory(destination.FullName);
foreach (var file in source.GetFiles())
{
file.CopyTo(Path.Combine(destination.FullName, file.Name), true);
}
foreach (var nextSource in source.GetDirectories())
{
var nextDestination = destination.CreateSubdirectory(nextSource.Name);
CopyCore(nextSource, nextDestination);
}
}
}
}

81
src/opulence/dotnet-opulence/DirectorySearch.cs

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Opulence
{
internal static class DirectorySearch
{
public static string? AscendingSearch(string directoryPath, string fileName)
{
if (directoryPath is null)
{
throw new ArgumentNullException(nameof(directoryPath));
}
if (fileName is null)
{
throw new ArgumentNullException(nameof(fileName));
}
var directory = new DirectoryInfo(directoryPath);
while (true)
{
var filePath = Path.Combine(directory.FullName, fileName);
if (File.Exists(filePath))
{
return filePath;
}
if (directory == directory.Root)
{
return null;
}
if (directory.Parent == null)
{
return null;
}
directory = directory.Parent;
}
}
public static IEnumerable<FileInfo> AscendingWildcardSearch(string directoryPath, string searchPattern)
{
if (directoryPath is null)
{
throw new ArgumentNullException(nameof(directoryPath));
}
if (searchPattern is null)
{
throw new ArgumentNullException(nameof(searchPattern));
}
var directory = new DirectoryInfo(directoryPath);
while (true)
{
if (directory.EnumerateFiles(searchPattern).Any())
{
return directory.EnumerateFiles(searchPattern);
}
if (directory == directory.Root)
{
return Array.Empty<FileInfo>();
}
if (directory.Parent == null)
{
return Array.Empty<FileInfo>();
}
directory = directory.Parent;
}
}
}
}

70
src/opulence/dotnet-opulence/DockerContainerBuilder.cs

@ -0,0 +1,70 @@
using System;
using System.CommandLine.Invocation;
using System.IO;
using System.Threading.Tasks;
namespace Opulence
{
internal static class DockerContainerBuilder
{
public static async Task BuildContainerImageAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
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));
}
if (container is null)
{
throw new ArgumentNullException(nameof(container));
}
using var tempFile = TempFile.Create();
var dockerFilePath = Path.Combine(application.GetProjectDirectory(project), Path.GetDirectoryName(project.RelativeFilePath)!, "Dockerfile");
if (File.Exists(dockerFilePath))
{
output.WriteDebugLine($"Using existing dockerfile '{dockerFilePath}'.");
}
else
{
await DockerfileGenerator.WriteDockerfileAsync(output, application, service, project, container, tempFile.FilePath);
dockerFilePath = tempFile.FilePath;
}
output.WriteDebugLine("Running 'docker build'.");
output.WriteCommandLine("docker", $"build . -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
$"docker",
$"build . -t {container.ImageName}:{container.ImageTag} -f \"{dockerFilePath}\"",
application.GetProjectDirectory(project),
stdOut: capture.StdOut,
stdErr: capture.StdErr);
output.WriteDebugLine($"Done running 'docker build' exit code: {exitCode}");
if (exitCode != 0)
{
throw new CommandException("'docker build' failed.");
}
output.WriteInfoLine($"Created Docker Image: {container.ImageName}:{container.ImageTag}");
service.Outputs.Add(new DockerImageOutput(container.ImageName!, container.ImageTag!));
}
}
}

42
src/opulence/dotnet-opulence/DockerPush.cs

@ -0,0 +1,42 @@
using System;
using System.CommandLine.Invocation;
using System.Threading.Tasks;
namespace Opulence
{
internal static class DockerPush
{
public static async Task ExecuteAsync(OutputContext output, string imageName, string imageTag)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (imageName is null)
{
throw new ArgumentNullException(nameof(imageName));
}
if (imageTag is null)
{
throw new ArgumentNullException(nameof(imageTag));
}
output.WriteDebugLine("Running 'docker push'.");
output.WriteCommandLine("docker", $"push {imageName}:{imageTag}");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
$"docker",
$"push {imageName}:{imageTag}",
stdOut: capture.StdOut,
stdErr: capture.StdErr);
output.WriteDebugLine($"Done running 'docker push' exit code: {exitCode}");
if (exitCode != 0)
{
throw new CommandException("'docker push' failed.");
}
}
}
}

142
src/opulence/dotnet-opulence/DockerfileGenerator.cs

@ -0,0 +1,142 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Opulence
{
internal static class DockerfileGenerator
{
public static async Task WriteDockerfileAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, string filePath)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
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));
}
if (container is null)
{
throw new ArgumentNullException(nameof(container));
}
if (filePath is null)
{
throw new ArgumentNullException(nameof(filePath));
}
using var stream = File.OpenWrite(filePath);
using var writer = new StreamWriter(stream, encoding: Encoding.UTF8, leaveOpen: true);
var entryPoint = Path.GetFileNameWithoutExtension(project.RelativeFilePath);
output.WriteDebugLine($"Writing dockerfile to '{filePath}'.");
if (container.UseMultiphaseDockerfile ?? true)
{
await WriteMultiphaseDockerfileAsync(writer, entryPoint, container);
}
else
{
await WriteLocalPublishDockerfileAsync(writer, entryPoint, container);
}
output.WriteDebugLine("Done writing dockerfile.");
}
private static async Task WriteMultiphaseDockerfileAsync(StreamWriter writer, string applicationEntryPoint, ContainerInfo container)
{
await writer.WriteLineAsync($"FROM {container.BuildImageName}:{container.BuildImageTag} as SDK");
await writer.WriteLineAsync($"WORKDIR /src");
await writer.WriteLineAsync($"COPY . .");
await writer.WriteLineAsync($"RUN dotnet publish -c Release -o /out");
await writer.WriteLineAsync($"FROM {container.BaseImageName}:{container.BaseImageTag} as RUNTIME");
await writer.WriteLineAsync($"WORKDIR /app");
await writer.WriteLineAsync($"COPY --from=SDK /out .");
await writer.WriteLineAsync($"ENTRYPOINT [\"dotnet\", \"{applicationEntryPoint}.dll\"]");
}
private static async Task WriteLocalPublishDockerfileAsync(StreamWriter writer, string applicationEntryPoint, ContainerInfo container)
{
await writer.WriteLineAsync($"FROM {container.BaseImageName}:{container.BaseImageTag}");
await writer.WriteLineAsync($"WORKDIR /app");
await writer.WriteLineAsync($"COPY . /app");
await writer.WriteLineAsync($"ENTRYPOINT [\"dotnet\", \"{applicationEntryPoint}.dll\"]");
}
public static void ApplyContainerDefaults(Application application, ServiceEntry service, Project 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));
}
if (container is null)
{
throw new ArgumentNullException(nameof(container));
}
if (container.BaseImageName == null &&
project.Frameworks.Any(f => f.Name == "Microsoft.AspNetCore.App"))
{
container.BaseImageName = "mcr.microsoft.com/dotnet/core/aspnet";
}
else if (container.BaseImageName == null)
{
container.BaseImageName = "mcr.microsoft.com/dotnet/core/runtime";
}
if (container.BaseImageTag == null &&
project.TargetFramework == "netcoreapp3.1")
{
container.BaseImageTag = "3.1";
}
else if (container.BaseImageTag == null &&
project.TargetFramework == "netcoreapp3.0")
{
container.BaseImageTag = "3.0";
}
if (container.BaseImageTag == null)
{
throw new CommandException($"Unsupported TFM {project.TargetFramework}.");
}
container.BuildImageName ??= "mcr.microsoft.com/dotnet/core/sdk";
container.BuildImageTag ??= "3.1";
if (container.ImageName == null && application.Globals.Registry?.Hostname == null)
{
container.ImageName ??= service.Service.Name.ToLowerInvariant();
}
else if (container.ImageName == null && application.Globals.Registry?.Hostname != null)
{
container.ImageName ??= $"{application.Globals.Registry?.Hostname}/{service.Service.Name.ToLowerInvariant()}";
}
container.ImageTag ??= project.Version.Replace("+", "-");
}
}
}

59
src/opulence/dotnet-opulence/GenerateCommand.cs

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Threading.Tasks;
namespace Opulence
{
internal static class GenerateCommand
{
public static Command Create()
{
var command = new Command("generate", "Generate assets")
{
StandardOptions.Project,
StandardOptions.Verbosity,
StandardOptions.Outputs,
StandardOptions.Force,
};
command.Handler = CommandHandler.Create<IConsole, FileInfo, Verbosity, List<string>, bool>((console, project, verbosity, outputs, force) =>
{
var output = new OutputContext(console, verbosity);
return ExecuteAsync(output, project, outputs, force);
});
return command;
}
private static async Task ExecuteAsync(OutputContext output, FileInfo projectFile, List<string> outputs, bool force)
{
output.WriteBanner();
var application = await ApplicationFactory.CreateApplicationAsync(output, projectFile);
var steps = new List<ServiceExecutor.Step>();
if (outputs.Count == 0 || outputs.Contains("container"))
{
steps.Add(new GenerateDockerfileStep(){ Force = force, });
}
if (outputs.Count == 0 || outputs.Contains("chart"))
{
steps.Add(new GenerateHelmChartStep());
}
var executor = new ServiceExecutor(output, application, steps);
foreach (var service in application.Services)
{
if (service.IsMatchForProject(application, projectFile))
{
await executor.ExecuteAsync(service);
}
}
}
}
}

43
src/opulence/dotnet-opulence/GenerateDockerfileStep.cs

@ -0,0 +1,43 @@
using System.IO;
using System.Threading.Tasks;
namespace Opulence
{
internal sealed class GenerateDockerfileStep : ServiceExecutor.Step
{
public override string DisplayText => "Generating Dockerfile...";
public bool Force { get; set; }
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{
if (SkipWithoutProject(output, service, out var project))
{
return;
}
if (SkipWithoutContainerInfo(output, service, out var container))
{
return;
}
if (container.UseMultiphaseDockerfile == false)
{
throw new CommandException("Generated Dockerfile workflow does not support single-phase Dockerfiles.");
}
container.UseMultiphaseDockerfile ??= true;
var dockerFilePath = Path.Combine(application.GetProjectDirectory(project), "Dockerfile");
if (File.Exists(dockerFilePath) && !Force)
{
throw new CommandException("'Dockerfile' already exists for project. use '--force' to overwrite.");
}
File.Delete(dockerFilePath);
await DockerfileGenerator.WriteDockerfileAsync(output, application, service, project, container, dockerFilePath);
output.WriteInfoLine($"Generated Dockerfile at '{dockerFilePath}'.");
}
}
}

47
src/opulence/dotnet-opulence/GenerateHelmChartStep.cs

@ -0,0 +1,47 @@
using System.IO;
using System.Threading.Tasks;
namespace Opulence
{
internal sealed class GenerateHelmChartStep : ServiceExecutor.Step
{
public override string DisplayText => "Generating Helm Chart...";
public bool Force { get; set; }
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{
if (SkipWithoutProject(output, service, out var project))
{
return;
}
if (SkipWithoutContainerInfo(output, service, out var container))
{
return;
}
var chartDirectory = Path.Combine(application.GetProjectDirectory(project), "charts");
if (Directory.Exists(chartDirectory) && !Force)
{
throw new CommandException("'charts' directory already exists for project. use '--force' to overwrite.");
}
else if (Directory.Exists(chartDirectory))
{
Directory.Delete(chartDirectory, recursive: true);
}
var chart = new HelmChartStep();
await HelmChartGenerator.GenerateAsync(
output,
application,
service,
project,
container,
chart,
new DirectoryInfo(chartDirectory));
output.WriteInfoLine($"Generated Helm Chart at '{Path.Combine(chartDirectory, chart.ChartName)}'.");
}
}
}

29
src/opulence/dotnet-opulence/GenerateKubernetesManifestStep.cs

@ -0,0 +1,29 @@
using System.Threading.Tasks;
namespace Opulence
{
internal sealed class GenerateKubernetesManifestStep : ServiceExecutor.Step
{
public override string DisplayText => "Generating Manifests...";
public string Environment { get; set; } = "production";
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{
if (SkipWithoutContainerOutput(output, service))
{
return Task.CompletedTask;
}
if (SkipForEnvironment(output, service, Environment))
{
return Task.CompletedTask;
}
service.Outputs.Add(KubernetesManifestGenerator.CreateDeployment(output, application, service));
service.Outputs.Add(KubernetesManifestGenerator.CreateService(output, application, service));
return Task.CompletedTask;
}
}
}

29
src/opulence/dotnet-opulence/GenerateOamComponentStep.cs

@ -0,0 +1,29 @@
using System.Threading.Tasks;
namespace Opulence
{
internal sealed class GenerateOamComponentStep : ServiceExecutor.Step
{
public override string DisplayText => "Generating Manifests...";
public string Environment { get; set; } = "production";
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{
if (SkipWithoutContainerOutput(output, service))
{
return Task.CompletedTask;
}
if (SkipForEnvironment(output, service, Environment))
{
return Task.CompletedTask;
}
var component = OamComponentGenerator.CreateOamComponent(output, application, service);
service.Outputs.Add(component);
return Task.CompletedTask;
}
}
}

83
src/opulence/dotnet-opulence/HelmChartBuilder.cs

@ -0,0 +1,83 @@
using System;
using System.CommandLine.Invocation;
using System.IO;
using System.Threading.Tasks;
namespace Opulence
{
internal static class HelmChartBuilder
{
public static async Task BuildHelmChartAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, HelmChartStep chart)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
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));
}
if (container is null)
{
throw new ArgumentNullException(nameof(container));
}
if (chart is null)
{
throw new ArgumentNullException(nameof(chart));
}
var projectDirectory = Path.Combine(application.RootDirectory, Path.GetDirectoryName(project.RelativeFilePath)!);
var outputDirectoryPath = Path.Combine(projectDirectory, "bin");
using var tempDirectory = TempDirectory.Create();
HelmChartGenerator.ApplyHelmChartDefaults(application, service, container, chart);
var chartRoot = Path.Combine(projectDirectory, "charts");
var chartPath = Path.Combine(chartRoot, chart.ChartName);
output.WriteDebugLine($"Looking for existing chart in '{chartPath}'.");
if (Directory.Exists(chartPath))
{
output.WriteDebugLine($"Found existing chart in '{chartPath}'.");
}
else
{
chartRoot = tempDirectory.DirectoryPath;
chartPath = Path.Combine(chartRoot, chart.ChartName);
output.WriteDebugLine($"Generating chart in '{chartPath}'.");
await HelmChartGenerator.GenerateAsync(output, application, service, project, container, chart, new DirectoryInfo(tempDirectory.DirectoryPath));
}
output.WriteDebugLine("Running 'helm package'.");
output.WriteCommandLine("helm", $"package -d \"{outputDirectoryPath}\" --version {project.Version.Replace('+', '-')} --app-version {project.Version.Replace('+', '-')}");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
"helm",
$"package . -d \"{outputDirectoryPath}\" --version {project.Version.Replace('+', '-')} --app-version {project.Version.Replace('+', '-')}",
workingDir: chartPath,
stdOut: capture.StdOut,
stdErr: capture.StdErr);
output.WriteDebugLine($"Running 'helm package' exit code: {exitCode}");
if (exitCode != 0)
{
throw new CommandException("Running 'helm package' failed.");
}
output.WriteInfoLine($"Created Helm Chart: {Path.Combine(outputDirectoryPath, chart.ChartName + "-" + project.Version.Replace('+', '-') + ".tgz")}");
}
}
}

111
src/opulence/dotnet-opulence/HelmChartGenerator.cs

@ -0,0 +1,111 @@
using System;
using System.IO;
using System.Threading.Tasks;
namespace Opulence
{
internal static class HelmChartGenerator
{
public static async Task GenerateAsync(OutputContext output, Application application, ServiceEntry service, Project project, ContainerInfo container, HelmChartStep chart, DirectoryInfo outputDirectory)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (project is null)
{
throw new ArgumentNullException(nameof(project));
}
if (container is null)
{
throw new ArgumentNullException(nameof(container));
}
if (chart is null)
{
throw new ArgumentNullException(nameof(chart));
}
if (outputDirectory is null)
{
throw new ArgumentNullException(nameof(outputDirectory));
}
ApplyHelmChartDefaults(application, service, container, chart);
// The directory with the charts needs to be the same as the chart name
var chartDirectoryPath = Path.Combine(outputDirectory.FullName, chart.ChartName);
Directory.CreateDirectory(chartDirectoryPath);
var templateDirectoryPath = Path.Combine(
Path.GetDirectoryName(typeof(HelmChartGenerator).Assembly.Location)!,
"Templates",
"Helm");
DirectoryCopy.Copy(templateDirectoryPath, chartDirectoryPath);
// Write Chart.yaml
//
// apiVersion: v1
// name: <appname>
// version: <version>
// appVersion: <version>
await File.WriteAllLinesAsync(Path.Combine(chartDirectoryPath, "Chart.yaml"), new[]
{
$"apiVersion: v1",
$"name: {chart.ChartName}",
$"# helm requires the version and appVersion to specified in Chart.yaml",
$"# opulence will override these values when packaging the chart",
$"version: {project.Version.Replace('+', '-')}",
$"appVersion: {project.Version.Replace('+', '-')}"
});
// Write values.yaml
//
// image:
// repository: rynowak.azurecr.io/rochambot/gamemaster
await File.WriteAllLinesAsync(Path.Combine(chartDirectoryPath, "values.yaml"), new[]
{
$"image:",
$" repository: {container.ImageName}",
});
}
public static void ApplyHelmChartDefaults(Application application, ServiceEntry service, ContainerInfo container, HelmChartStep chart)
{
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (container is null)
{
throw new ArgumentNullException(nameof(container));
}
if (chart is null)
{
throw new ArgumentNullException(nameof(chart));
}
chart.ChartName ??= service.Service.Name.ToLowerInvariant();
}
}
}

52
src/opulence/dotnet-opulence/HelmPush.cs

@ -0,0 +1,52 @@
using System;
using System.CommandLine.Invocation;
using System.Threading.Tasks;
namespace Opulence
{
internal static class HelmPush
{
public static async Task ExecuteAsync(OutputContext output, string registryHostname, string chartFilePath)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (registryHostname is null)
{
throw new ArgumentNullException(nameof(registryHostname));
}
if (chartFilePath is null)
{
throw new ArgumentNullException(nameof(chartFilePath));
}
// NOTE: this is currently hardcoded to use ACR and uses the azure CLI to do
// the push operation. Helm 3 has support for pushing to OCI repositories,
// but it's not documented that it works yet for azure.
if (!registryHostname.EndsWith(".azurecr.io"))
{
throw new CommandException("Currently pushing of helm charts is limited to '*.azurecr.io'.");
}
var registryName = registryHostname.Substring(0, registryHostname.Length - ".azurecr.io".Length);
output.WriteDebugLine("Running 'az acr helm push'.");
output.WriteCommandLine("az", $"acr helm push --name {registryName} \"{chartFilePath}\"");
var capture = output.Capture();
var exitCode = await Process.ExecuteAsync(
$"az",
$"acr helm push --name {registryName} \"{chartFilePath}\"",
stdOut: capture.StdOut,
stdErr: capture.StdErr);
output.WriteDebugLine($"Done running 'az acr helm push' exit code: {exitCode}");
if (exitCode != 0)
{
throw new CommandException("'az acr helm push' failed.");
}
}
}
}

9
src/opulence/dotnet-opulence/IYamlManifestOutput.cs

@ -0,0 +1,9 @@
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal interface IYamlManifestOutput
{
YamlDocument Yaml { get; }
}
}

193
src/opulence/dotnet-opulence/InitCommand.cs

@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
namespace Opulence
{
internal static class InitCommand
{
public static Command Create()
{
var command = new Command("init", "Initialize repo")
{
StandardOptions.Verbosity,
new Option(new[] { "-d", "--directory", }, "Directory to Initialize")
{
Argument = new Argument<DirectoryInfo>(() =>
{
return new DirectoryInfo(Environment.CurrentDirectory);
}).ExistingOnly(),
},
};
command.Handler = CommandHandler.Create<IConsole, Verbosity, DirectoryInfo>((console, verbosity, directory) =>
{
var output = new OutputContext(console, verbosity);
return ExecuteAsync(output, directory);
});
return command;
}
private static async Task ExecuteAsync(OutputContext output, DirectoryInfo directory)
{
output.WriteBanner();
string? solutionFilePath = null;
string? opulenceFilePath = null;
using (var step = output.BeginStep("Looking For Existing Config..."))
{
opulenceFilePath = DirectorySearch.AscendingSearch(directory.FullName, "Opulence.csx");
if (opulenceFilePath != null)
{
output.WriteInfoLine($"Found 'Opulence.csx' at '{opulenceFilePath}'");
step.MarkComplete();
return;
}
else
{
output.WriteInfoLine("Not Found");
step.MarkComplete();
}
}
using (var step = output.BeginStep("Looking For .sln File..."))
{
solutionFilePath = DirectorySearch.AscendingWildcardSearch(directory.FullName, "*.sln").FirstOrDefault()?.FullName;
if (opulenceFilePath == null &&
solutionFilePath != null &&
output.Confirm($"Use '{Path.GetDirectoryName(solutionFilePath)}' as Root?"))
{
opulenceFilePath = Path.Combine(Path.GetDirectoryName(solutionFilePath)!, "Opulence.csx");
step.MarkComplete();
}
else
{
output.WriteInfoLine("Not Found.");
step.MarkComplete();
}
}
if (opulenceFilePath == null &&
Path.GetFullPath(directory.FullName) != Path.GetFullPath(Environment.CurrentDirectory))
{
// User specified a directory other than the current one
using (var step = output.BeginStep("Trying Project Directory..."))
{
if (output.Confirm("Use Project Directory as Root?"))
{
opulenceFilePath = Path.Combine(directory.FullName, "Opulence.csx");
}
step.MarkComplete();
}
}
if (opulenceFilePath == null)
{
using (var step = output.BeginStep("Trying Current Directory..."))
{
if (output.Confirm("Use Current Directory as Root?"))
{
opulenceFilePath = Path.Combine(directory.FullName, "Opulence.csx");
}
step.MarkComplete();
}
}
if (opulenceFilePath == null)
{
throw new CommandException("Cannot Determine Root Directory.");
}
using (var step = output.BeginStep("Writing Opulence.csx ..."))
{
var hostname = output.Prompt("Enter the Container Registry (ex: 'example.azurecr.io' for Azure or 'example' for dockerhub)");
var services = new List<(string, string)>();
if (solutionFilePath != null && output.Confirm($"Use solution file '{solutionFilePath}' to initialize services?"))
{
services.AddRange(ReadServicesFromSolution(solutionFilePath));
services.Sort((a, b) => a.Item1.CompareTo(b.Item1));
}
using var stream = File.OpenWrite(opulenceFilePath);
using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
await WriteOpulenceConfigAsync(writer, hostname, services);
output.WriteInfoLine($"Initialized Opulence Config at '{opulenceFilePath}'.");
step.MarkComplete();
}
}
private static IEnumerable<(string, string)> ReadServicesFromSolution(string solutionFilePath)
{
SolutionFile solution;
try
{
solution = SolutionFile.Parse(solutionFilePath);
}
catch (Exception ex)
{
throw new CommandException($"Parsing solution file '{solutionFilePath}' failed.", ex);
}
for (var i = 0; i < solution.ProjectsInOrder.Count; i++)
{
var project = solution.ProjectsInOrder[i];
if (string.Equals(Path.GetExtension(project.RelativePath), ".csproj", StringComparison.Ordinal))
{
var fileName = Path.GetFileNameWithoutExtension(project.RelativePath.Replace('\\', Path.DirectorySeparatorChar));
yield return (Names.NormalizeToFriendly(fileName), Names.NormalizeToDns(fileName));
}
}
}
private static async Task WriteOpulenceConfigAsync(TextWriter writer, string hostname, List<(string, string)> services)
{
await writer.WriteLineAsync("#r \"Opulence\"");
await writer.WriteLineAsync();
await writer.WriteLineAsync($"using Opulence;");
await writer.WriteLineAsync();
await writer.WriteLineAsync($"public class Application");
await writer.WriteLineAsync($"{{");
await writer.WriteLineAsync($" public ApplicationGlobals Globals {{ get; }} = new ApplicationGlobals()");
await writer.WriteLineAsync($" {{");
await writer.WriteLineAsync($" Registry = new ContainerRegistry(\"{hostname}\"),");
await writer.WriteLineAsync($" }};");
await writer.WriteLineAsync();
await writer.WriteLineAsync($" // Define more services and dependencies here as your application grows.");
for (var i = 0; i < services.Count; i++)
{
await writer.WriteLineAsync($" public Service {services[i].Item1} {{ get; }} = new Service(\"{services[i].Item2}\");");
if (i + 1 < services.Count)
{
await writer.WriteLineAsync();
}
}
await writer.WriteLineAsync($"}}");
await writer.WriteLineAsync();
await writer.WriteLineAsync($"Pipeline.Configure<Application>(app =>");
await writer.WriteLineAsync($"{{");
await writer.WriteLineAsync($" // Configure your service bindings here with code.");
await writer.WriteLineAsync($"}});");
}
}
}

27
src/opulence/dotnet-opulence/KubernetesDeploymentOutput.cs

@ -0,0 +1,27 @@
using System;
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal sealed class KubernetesDeploymentOutput : ServiceOutput, IYamlManifestOutput
{
public KubernetesDeploymentOutput(string name, YamlDocument yaml)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
if (yaml is null)
{
throw new ArgumentNullException(nameof(yaml));
}
Name = name;
Yaml = yaml;
}
public string Name { get; }
public YamlDocument Yaml { get; }
}
}

196
src/opulence/dotnet-opulence/KubernetesManifestGenerator.cs

@ -0,0 +1,196 @@
using System;
using System.Linq;
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal static class KubernetesManifestGenerator
{
public static ServiceOutput CreateService(OutputContext output, Application application, ServiceEntry service)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
var root = new YamlMappingNode();
root.Add("kind", "Service");
root.Add("apiVersion", "v1");
var metadata = new YamlMappingNode();
root.Add("metadata", metadata);
metadata.Add("name", service.Service.Name);
var labels = new YamlMappingNode();
metadata.Add("labels", labels);
labels.Add("app.kubernetes.io/name", service.Service.Name);
labels.Add("app.kubernetes.io/part-of", application.Globals.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);
spec.Add("type", "ClusterIP");
var ports = new YamlSequenceNode();
spec.Add("ports", ports);
var port = new YamlMappingNode();
ports.Add(port);
port.Add("name", "web");
port.Add("protocol", "TCP");
port.Add("port", "80");
port.Add("targetPort", "80");
return new KubernetesServiceOutput(service.Service.Name, new YamlDocument(root));
}
public static ServiceOutput CreateDeployment(OutputContext output, Application application, ServiceEntry service)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
var root = new YamlMappingNode();
root.Add("kind", "Deployment");
root.Add("apiVersion", "apps/v1");
var metadata = new YamlMappingNode();
root.Add("metadata", metadata);
metadata.Add("name", service.Service.Name);
var labels = new YamlMappingNode();
metadata.Add("labels", labels);
labels.Add("app.kubernetes.io/name", service.Service.Name);
labels.Add("app.kubernetes.io/part-of", application.Globals.Name);
var spec = new YamlMappingNode();
root.Add("spec", spec);
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);
var template = new YamlMappingNode();
spec.Add("template", template);
metadata = new YamlMappingNode();
template.Add("metadata", metadata);
labels = new YamlMappingNode();
metadata.Add("labels", labels);
labels.Add("app.kubernetes.io/name", service.Service.Name);
labels.Add("app.kubernetes.io/part-of", application.Globals.Name);
spec = new YamlMappingNode();
template.Add("spec", spec);
var containers = new YamlSequenceNode();
spec.Add("containers", containers);
var images = service.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("image", $"{image.ImageName}:{image.ImageTag}");
if (service.Service.Environment.Count > 0)
{
var env = new YamlSequenceNode();
container.Add("env", env);
foreach (var kvp in service.Service.Environment)
{
env.Add(new YamlMappingNode()
{
{ "name", kvp.Key },
{ "value", kvp.Value.ToString() },
});
}
}
if (service.Service.Bindings.Any(b => b.ConnectionString != null))
{
var volumeMounts = new YamlSequenceNode();
container.Add("volumeMounts", volumeMounts);
foreach (var binding in service.Service.Bindings.Where(b => b.ConnectionString != null))
{
var volumeMount = new YamlMappingNode();
volumeMounts.Add(volumeMount);
volumeMount.Add("name", $"{binding.Name}-secret");
volumeMount.Add("mountPath", $"/var/bindings/{binding.Name}");
volumeMount.Add("readOnly", "true");
}
}
var ports = new YamlSequenceNode();
container.Add("ports", ports);
var containerPort = new YamlMappingNode();
ports.Add(containerPort);
containerPort.Add("containerPort", "80");
}
if (service.Service.Bindings.Any(b => b.ConnectionString != null))
{
var volumes = new YamlSequenceNode();
spec.Add("volumes", volumes);
foreach (var binding in service.Service.Bindings.Where(b => b.ConnectionString != null))
{
var volume = new YamlMappingNode();
volumes.Add(volume);
volume.Add("name", $"{binding.Name}-secret");
var secret = new YamlMappingNode();
volume.Add("secret", secret);
secret.Add("secretName", binding.ConnectionString!.Name!);
var items = new YamlSequenceNode();
secret.Add("items", items);
var item = new YamlMappingNode();
items.Add(item);
item.Add("key", "uri");
item.Add("path", $"SERVICES__{binding.Name}");
}
}
return new KubernetesDeploymentOutput(service.Service.Name, new YamlDocument(root));
}
}
}

27
src/opulence/dotnet-opulence/KubernetesServiceOutput.cs

@ -0,0 +1,27 @@
using System;
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal sealed class KubernetesServiceOutput : ServiceOutput, IYamlManifestOutput
{
public KubernetesServiceOutput(string name, YamlDocument yaml)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
if (yaml is null)
{
throw new ArgumentNullException(nameof(yaml));
}
Name = name;
Yaml = yaml;
}
public string Name { get; }
public YamlDocument Yaml { get; }
}
}

32
src/opulence/dotnet-opulence/Names.cs

@ -0,0 +1,32 @@
using System;
namespace Opulence
{
internal static class Names
{
public static string NormalizeToDns(string name)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
name = name.ToLowerInvariant();
name = name.Replace('.', '-');
name = name.Replace('_', '-');
return name;
}
public static string NormalizeToFriendly(string name)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
name = name.Replace('.', '_');
name = name.Replace('-', '_');
return name;
}
}
}

84
src/opulence/dotnet-opulence/OamApplicationGenerator.cs

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal static class OamApplicationGenerator
{
public static Task WriteOamApplicationAsync(TextWriter writer, OutputContext output, Application application, string applicationName, string environment)
{
if (writer is null)
{
throw new ArgumentNullException(nameof(writer));
}
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (applicationName is null)
{
throw new ArgumentNullException(nameof(applicationName));
}
if (environment is null)
{
throw new ArgumentNullException(nameof(environment));
}
var componentManifests = new List<OamComponentOutput>();
var documents = new List<YamlDocument>();
foreach (var service in application.Services)
{
if (service.AppliesToEnvironment(environment))
{
componentManifests.AddRange(service.Outputs.OfType<OamComponentOutput>());
}
}
var root = new YamlMappingNode();
root.Add("kind", "ApplicationConfiguration");
root.Add("apiVersion", "core.oam.dev/v1alpha1");
var metadata = new YamlMappingNode();
root.Add("metadata", metadata);
metadata.Add("name", applicationName.ToLowerInvariant());
var spec = new YamlMappingNode();
root.Add("spec", spec);
var components = new YamlSequenceNode();
spec.Add("components", components);
foreach (var manifest in componentManifests)
{
documents.Add(manifest.Yaml);
var component = new YamlMappingNode();
components.Add(component);
component.Add("componentName", manifest.Name);
component.Add("instanceName", $"{environment.ToLowerInvariant()}-{manifest.Name}");
}
documents.Add(new YamlDocument(root));
var stream = new YamlStream(documents.ToArray());
stream.Save(writer, assignAnchors: false);
return Task.CompletedTask;
}
}
}

54
src/opulence/dotnet-opulence/OamComponentGenerator.cs

@ -0,0 +1,54 @@
using System;
using System.Linq;
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal static class OamComponentGenerator
{
public static OamComponentOutput CreateOamComponent(OutputContext output, Application application, ServiceEntry service)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
var root = new YamlMappingNode();
root.Add("kind", "ComponentSchematic");
root.Add("apiVersion", "core.oam.dev/v1alpha1");
var metadata = new YamlMappingNode();
root.Add("metadata", metadata);
metadata.Add("name", service.Service.Name);
var spec = new YamlMappingNode();
root.Add("spec", spec);
spec.Add("workloadType", "core.oam.dev/v1alpha1.Server");
var containers = new YamlSequenceNode();
spec.Add("containers", containers);
var images = service.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("image", $"{image.ImageName}:{image.ImageTag}");
}
return new OamComponentOutput(service.Service.Name, new YamlDocument(root));
}
}
}

27
src/opulence/dotnet-opulence/OamComponentOutput.cs

@ -0,0 +1,27 @@
using System;
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal sealed class OamComponentOutput : ServiceOutput, IYamlManifestOutput
{
public OamComponentOutput(string name, YamlDocument yaml)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
if (yaml is null)
{
throw new ArgumentNullException(nameof(yaml));
}
Name = name;
Yaml = yaml;
}
public string Name { get; }
public YamlDocument Yaml { get; }
}
}

256
src/opulence/dotnet-opulence/OutputContext.cs

@ -0,0 +1,256 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.IO;
namespace Opulence
{
internal sealed class OutputContext
{
private const int IndentAmount = 4;
private readonly Stack<StepTracker> steps;
private int indent;
public OutputContext(IConsole console, Verbosity verbosity)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
Console = console;
Verbosity = verbosity;
steps = new Stack<StepTracker>();
}
private IConsole Console { get; }
public Verbosity Verbosity { get; }
public StepTracker BeginStep(string title)
{
if (title is null)
{
throw new ArgumentNullException(nameof(title));
}
WriteInfoLine("💰 " + title);
indent += IndentAmount;
var currentStep = new StepTracker(this, title);
steps.Push(currentStep);
return currentStep;
}
private void EndStep(StepTracker step)
{
if (!object.ReferenceEquals(step, steps.Peek()))
{
throw new InvalidOperationException($"Attempting to end a step that isn't running. Currently executing step: {steps.Peek()?.Title}");
}
indent -= IndentAmount;
steps.Pop();
if (step.Message != null)
{
WriteDebugLine(step.Message);
}
}
private void Write(Verbosity verbosity, string message)
{
if (Verbosity >= verbosity)
{
if (indent > 0)
{
Console.Out.Write(new string(' ', indent));
}
Console.Out.Write(message);
}
}
private void WriteLine(Verbosity verbosity, string message)
{
if (Verbosity >= verbosity)
{
if (indent > 0)
{
Console.Out.Write(new string(' ', indent));
}
Console.Out.WriteLine(message);
}
}
public void WriteAlways(string message)
{
Write(Verbosity.Info, message);
}
public void WriteAlwaysLine(string message)
{
WriteLine(Verbosity.Info, message);
}
public void WriteInfoLine(string message)
{
WriteLine(Verbosity.Info, message);
}
public void WriteDebugLine(string message)
{
WriteLine(Verbosity.Debug, message);
}
public void WriteCommandLine(string process, string args)
{
WriteDebugLine($"> {process} {args}");
}
public bool Confirm(string prompt)
{
while (true)
{
WriteAlways(prompt + " (y/n): ");
var key = System.Console.ReadKey();
WriteAlwaysLine(string.Empty);
if (key.KeyChar == 'y' || key.KeyChar == 'Y')
{
return true;
}
else if (key.KeyChar == 'n' || key.KeyChar == 'N')
{
return false;
}
else
{
WriteAlwaysLine("Invalid Input.");
}
}
}
public string Prompt(string prompt, bool allowEmpty = false)
{
while (true)
{
WriteAlways(prompt + ": ");
var line = System.Console.ReadLine();
if (allowEmpty || !string.IsNullOrEmpty(line))
{
return line.Trim();
}
}
}
//
public void WriteBanner()
{
// From: http://patorjk.com/software/taag/#p=display&f=Big%20Money-nw&t=Opulence
WriteInfoLine(string.Empty);
WriteInfoLine(@" $$$$$$\ $$\ ");
WriteInfoLine(@"$$ __$$\ $$ | ");
WriteInfoLine(@"$$ / $$ | $$$$$$\ $$\ $$\ $$ | $$$$$$\ $$$$$$$\ $$$$$$$\ $$$$$$\ ");
WriteInfoLine(@"$$ | $$ |$$ __$$\ $$ | $$ |$$ |$$ __$$\ $$ __$$\ $$ _____|$$ __$$\ ");
WriteInfoLine(@"$$ | $$ |$$ / $$ |$$ | $$ |$$ |$$$$$$$$ |$$ | $$ |$$ / $$$$$$$$ |");
WriteInfoLine(@"$$ | $$ |$$ | $$ |$$ | $$ |$$ |$$ ____|$$ | $$ |$$ | $$ ____|");
WriteInfoLine(@" $$$$$$ |$$$$$$$ |\$$$$$$ |$$ |\$$$$$$$\ $$ | $$ |\$$$$$$$\ \$$$$$$$\ ");
WriteInfoLine(@" \______/ $$ ____/ \______/ \__| \_______|\__| \__| \_______| \_______|");
WriteInfoLine(@" $$ | ");
WriteInfoLine(@" $$ | ");
WriteInfoLine(@" \__| ");
WriteInfoLine(@"--------------------------------------------------------------------------");
WriteInfoLine(string.Empty);
WriteInfoLine("White-Glove service for .NET and Kubernetes...");
WriteInfoLine("Someone will be right with you!");
WriteInfoLine(string.Empty);
}
public CapturedCommandOutput Capture()
{
return new CapturedCommandOutput(this);
}
public sealed class CapturedCommandOutput
{
private readonly OutputContext output;
public CapturedCommandOutput(OutputContext output)
{
this.output = output;
}
public void StdOut(string line)
{
if (output.Verbosity >= Verbosity.Debug)
{
output.Console.SetTerminalForegroundColor(ConsoleColor.Gray);
output.Console.Out.WriteLine(new string(' ', output.indent + IndentAmount) + line);
output.Console.ResetTerminalForegroundColor();
}
}
public void StdErr(string line)
{
if (output.Verbosity >= Verbosity.Info)
{
output.Console.SetTerminalForegroundColor(ConsoleColor.Red);
output.Console.Out.WriteLine(new string(' ', output.indent + IndentAmount)+ line);
output.Console.ResetTerminalForegroundColor();
}
}
}
public class StepTracker : IDisposable
{
private readonly OutputContext output;
private readonly string title;
private string? message;
private bool disposed;
public StepTracker(OutputContext output, string title)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (title is null)
{
throw new ArgumentNullException(nameof(title));
}
this.output = output;
this.title = title;
}
public bool Completed => message != null;
public string? Message => message;
public string Title => title;
public void MarkComplete(string? message = null)
{
this.message = message ?? ("Done " + title);
}
public void Dispose()
{
if (disposed)
{
return;
}
output.EndStep(this);
disposed = true;
}
}
}
}

119
src/opulence/dotnet-opulence/PackageCommand.cs

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace Opulence
{
public class PackageCommand
{
public static Command Create()
{
var command = new Command("package", "Package the project")
{
StandardOptions.Project,
StandardOptions.Verbosity,
StandardOptions.Force,
StandardOptions.Environment,
new Option(new []{ "-o", "--output" }, "Output directory")
{
Argument = new Argument<DirectoryInfo>("output", () => new DirectoryInfo(Environment.CurrentDirectory))
{
Arity = ArgumentArity.ExactlyOne,
},
Required = false,
},
};
command.Handler = CommandHandler.Create<IConsole, FileInfo, DirectoryInfo, string, bool, Verbosity>((console, project, output, environment, force, verbosity) =>
{
return ExecuteAsync(new OutputContext(console, verbosity), project, output, environment, force);
});
return command;
}
private static async Task ExecuteAsync(OutputContext output, FileInfo projectFile, DirectoryInfo outputDirectory, string environment, bool force)
{
output.WriteBanner();
var application = await ApplicationFactory.CreateApplicationAsync(output, projectFile);
var steps = new List<ServiceExecutor.Step>
{
new CombineStep() { Environment = environment, },
new BuildDockerImageStep() { Environment = environment, },
};
if (application.Globals.DeploymentKind == DeploymentKind.None)
{
// No extra steps
}
else if (application.Globals.DeploymentKind == DeploymentKind.Kubernetes)
{
steps.Add(new GenerateKubernetesManifestStep() { Environment = environment, });
}
else if (application.Globals.DeploymentKind == DeploymentKind.Oam)
{
steps.Add(new GenerateOamComponentStep() { Environment = environment, });
}
else
{
throw new InvalidOperationException($"Unknown DeploymentKind: " + application.Globals.DeploymentKind);
}
// If this is command is for a project, then write out the component manifest
// for just the project. We won't run the "application package" part.
if (!string.Equals(".sln", projectFile.Extension, StringComparison.Ordinal))
{
steps.Add(new WriteServiceYamlStep(){ OutputDirectory = outputDirectory, Force = force, });
}
var executor = new ServiceExecutor(output, application, steps);
foreach (var service in application.Services)
{
if (service.IsMatchForProject(application, projectFile))
{
await executor.ExecuteAsync(service);
}
}
if (string.Equals(".sln", projectFile.Extension, StringComparison.Ordinal))
{
await PackageApplicationAsync(output, application, outputDirectory, Path.GetFileNameWithoutExtension(projectFile.Name), environment);
}
}
private static async Task PackageApplicationAsync(OutputContext output, Application application, DirectoryInfo outputDirectory, string applicationName, string environment)
{
using var step = output.BeginStep("Writing Application Manifests...");
var outputFile = Path.Combine(outputDirectory.FullName, $"{applicationName}-{environment}.yaml");
output.WriteInfoLine($"Writing output to '{outputFile}'.");
File.Delete(outputFile);
using var stream = File.OpenWrite(outputFile);
using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
if (application.Globals.DeploymentKind == DeploymentKind.None)
{
// No extra steps
}
else if (application.Globals.DeploymentKind == DeploymentKind.Kubernetes)
{
await ApplicationYamlWriter.WriteAsync(output, writer, application);
}
else if (application.Globals.DeploymentKind == DeploymentKind.Oam)
{
await OamApplicationGenerator.WriteOamApplicationAsync(writer, output, application, applicationName, environment);
}
else
{
throw new InvalidOperationException($"Unknown DeploymentKind: " + application.Globals.DeploymentKind);
}
step.MarkComplete();
}
}
}

82
src/opulence/dotnet-opulence/Program.cs

@ -0,0 +1,82 @@
using System;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Help;
using System.CommandLine.Invocation;
using System.CommandLine.IO;
using System.CommandLine.Parsing;
using System.CommandLine.Rendering;
using System.Threading.Tasks;
namespace Opulence
{
class Program
{
static async Task<int> Main(string[] args)
{
var command = new RootCommand();
command.AddCommand(InitCommand.Create());
command.AddCommand(GenerateCommand.Create());
command.AddCommand(PackageCommand.Create());
command.AddCommand(PushCommand.Create());
command.AddCommand(DeployCommand.Create());
command.Description = "White-Glove Service for .NET and Kubernetes";
command.Handler = CommandHandler.Create<IHelpBuilder>(help =>
{
help.Write(command);
return 1;
});
var builder = new CommandLineBuilder(command);
// Parsing behavior
builder.UseHelp();
builder.UseVersionOption();
builder.UseDebugDirective();
builder.UseParseErrorReporting();
builder.ParseResponseFileAs(ResponseFileHandling.ParseArgsAsSpaceSeparated);
builder.UsePrefixes(new []{ "-", "--", }); // disable garbage windows conventions
builder.CancelOnProcessTermination();
builder.UseExceptionHandler(HandleException);
// Allow fancy drawing.
builder.UseAnsiTerminalWhenAvailable();
var parser = builder.Build();
return await parser.InvokeAsync(args);
}
private static void HandleException(Exception exception, InvocationContext context)
{
context.Console.ResetTerminalForegroundColor();
context.Console.SetTerminalForegroundColor(ConsoleColor.Red);
if (exception is OperationCanceledException)
{
context.Console.Error.WriteLine("Oh dear! Operation canceled.");
}
else if (exception is CommandException command)
{
context.Console.Error.WriteLine($"Drats! '{context.ParseResult.CommandResult.Command.Name}' failed:");
context.Console.Error.WriteLine($"\t{command.Message}");
if (command.InnerException != null)
{
context.Console.Error.WriteLine();
context.Console.Error.WriteLine(command.InnerException.ToString());
}
}
else
{
context.Console.Error.WriteLine("An unhandled exception has occurred, how unseemly: ");
context.Console.Error.WriteLine(exception.ToString());
}
context.Console.ResetTerminalForegroundColor();
context.ResultCode = 1;
}
}
}

118
src/opulence/dotnet-opulence/ProjectReader.cs

@ -0,0 +1,118 @@
using System;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Semver;
namespace Opulence
{
internal static class ProjectReader
{
public static async Task ReadProjectDetailsAsync(OutputContext output, FileInfo projectFile, Project project)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (projectFile is null)
{
throw new ArgumentNullException(nameof(projectFile));
}
if (project is null)
{
throw new ArgumentNullException(nameof(project));
}
using (var step = output.BeginStep("Reading Project Details..."))
{
await EvaluateMSBuildAsync(output, projectFile, project);
if (!SemVersion.TryParse(project.Version, out var version))
{
output.WriteInfoLine($"No version or invalid version 'application.Version' found, using default.");
version = new SemVersion(0, 1, 0);
project.Version = version.ToString();
}
step.MarkComplete();
}
}
private static async Task EvaluateMSBuildAsync(OutputContext output, FileInfo projectFile, Project project)
{
try
{
output.WriteDebugLine("Installing msbuild targets.");
TargetInstaller.Install(projectFile.FullName);
output.WriteDebugLine("Installed msbuild targets.");
}
catch (Exception ex)
{
throw new CommandException("Failed to install targets.", ex);
}
var outputFilePath = Path.GetTempFileName();
try
{
var capture = output.Capture();
var opulenceRoot = Path.GetDirectoryName(typeof(Program).Assembly.Location);
var restore = string.Empty;
if (!File.Exists(Path.Combine(projectFile.DirectoryName, "obj", "project.assets.json")))
{
restore = "/restore";
}
output.WriteDebugLine("Running 'dotnet msbuild'.");
output.WriteCommandLine("dotnet", $"msbuild {restore} /t:EvaluateOpulenceProjectInfo \"/p:OpulenceTargetLocation={opulenceRoot}\" \"/p:OpulenceOutputFilePath={outputFilePath}\"");
var exitCode = await Process.ExecuteAsync(
$"dotnet",
$"msbuild {restore} /t:EvaluateOpulenceProjectInfo \"/p:OpulenceTargetLocation={opulenceRoot}\" \"/p:OpulenceOutputFilePath={outputFilePath}\"",
workingDir: projectFile.DirectoryName,
stdOut: capture.StdOut,
stdErr: capture.StdErr);
output.WriteDebugLine($"Done running 'dotnet msbuild' exit code: {exitCode}");
if (exitCode != 0)
{
throw new CommandException("'dotnet msbuild' failed.");
}
var lines = await File.ReadAllLinesAsync(outputFilePath);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
if (line.StartsWith("version="))
{
project.Version = line.Substring("version=".Length).Trim();
output.WriteDebugLine($"Found application version: {line}");
continue;
}
if (line.StartsWith("tfm"))
{
project.TargetFramework = line.Substring("tfm=".Length).Trim();
output.WriteDebugLine($"Found target framework: {line}");
continue;
}
if (line.StartsWith("frameworks="))
{
var right = line.Substring("frameworks=".Length).Trim();
project.Frameworks.AddRange(right.Split(",").Select(s => new Framework(s)));
output.WriteDebugLine($"Found shared frameworks: {line}");
continue;
}
}
}
finally
{
File.Delete(outputFilePath);
}
}
}
}

8
src/opulence/dotnet-opulence/Properties/launchSettings.json

@ -0,0 +1,8 @@
{
"profiles": {
"dotnet-opulence": {
"commandName": "Project",
"commandLineArgs": ""
}
}
}

55
src/opulence/dotnet-opulence/PushCommand.cs

@ -0,0 +1,55 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Threading.Tasks;
namespace Opulence
{
public class PushCommand
{
public static Command Create()
{
var command = new Command("push", "Push the application to registry")
{
StandardOptions.Project,
StandardOptions.Verbosity,
StandardOptions.Environment,
};
command.Handler = CommandHandler.Create<IConsole, FileInfo, string, Verbosity>((console, project, environment, verbosity) =>
{
return ExecuteAsync(new OutputContext(console, verbosity), project, environment);
});
return command;
}
private static async Task ExecuteAsync(OutputContext output, FileInfo projectFile, string environment)
{
output.WriteBanner();
var application = await ApplicationFactory.CreateApplicationAsync(output, projectFile);
if (application.Globals.Registry?.Hostname == null)
{
throw new CommandException("A registry is required for push operations. run 'dotnet-opulence init'.");
}
var steps = new ServiceExecutor.Step[]
{
new CombineStep() { Environment = environment, },
new BuildDockerImageStep() { Environment = environment, },
new PushDockerImageStep() { Environment = environment, },
};
var executor = new ServiceExecutor(output, application, steps);
foreach (var service in application.Services)
{
if (service.IsMatchForProject(application, projectFile))
{
await executor.ExecuteAsync(service);
}
}
}
}
}

36
src/opulence/dotnet-opulence/PushDockerImageStep.cs

@ -0,0 +1,36 @@
using System.Linq;
using System.Threading.Tasks;
namespace Opulence
{
internal sealed class PushDockerImageStep : ServiceExecutor.Step
{
public override string DisplayText => "Pushing Docker Image...";
public string Environment { get; set; } = "production";
public override async Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{
if (SkipWithoutProject(output, service, out var _))
{
return;
}
if (SkipWithoutContainerInfo(output, service, out var _))
{
return;
}
if (SkipForEnvironment(output, service, Environment))
{
return;
}
foreach (var image in service.Outputs.OfType<DockerImageOutput>())
{
await DockerPush.ExecuteAsync(output, image.ImageName, image.ImageTag);
output.WriteInfoLine($"Pushed docker image: '{image.ImageName}:{image.ImageTag}'");
}
}
}
}

8
src/opulence/dotnet-opulence/Resources/Imports.targets

@ -0,0 +1,8 @@
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Condition=" '$(TargetFramework)' == '' AND '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(OpulenceTargetLocation)\buildMultiTargeting\Opulence.targets" Condition="'$(OpulenceTargetLocation)'!=''" />
</ImportGroup>
<ImportGroup Condition=" '$(TargetFramework)' != '' AND '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(OpulenceTargetLocation)\build\Opulence.targets" Condition="'$(OpulenceTargetLocation)'!=''" />
</ImportGroup>
</Project>

166
src/opulence/dotnet-opulence/ScriptRunner.cs

@ -0,0 +1,166 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Build.Construction;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
namespace Opulence
{
internal static class ScriptRunner
{
public static async Task<ApplicationWrapper?> RunCustomizationScriptAsync(OutputContext output, FileInfo projectFile)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
using (var step = output.BeginStep("Applying Application Customizations..."))
{
var scriptFilePath = DirectorySearch.AscendingSearch(projectFile.DirectoryName, "Opulence.csx");
output.WriteDebugLine($"Looking for customization script above '{projectFile}'.");
if (!File.Exists(scriptFilePath))
{
output.WriteDebugLine($"No customization script found.");
step.MarkComplete("Skipping...");
return null;
}
output.WriteInfoLine($"Configuring application using '{Path.GetFileName(scriptFilePath)}'.");
var code = File.ReadAllText(scriptFilePath);
var script = CSharpScript.Create<object>(
code,
options: ScriptOptions.Default.WithImports(new []{ "System", "System.Collections.Generic", }),
globalsType: typeof(PipelineHolder),
assemblyLoader: null);
script = script.ContinueWith<object>(@"return await Pipeline.ExecuteAsync(__Pipeline);", options: ScriptOptions.Default);
output.WriteDebugLine($"Compiling {Path.GetFileName(scriptFilePath)}'.");
script.Compile();
var diagnostics = script.Compile();
if (diagnostics.Length > 0)
{
var builder = new StringBuilder();
output.WriteDebugLine($"Script '{scriptFilePath}' had compilation errors.");
builder.AppendLine($"Script '{scriptFilePath}' had compilation errors.");
foreach (var diagnostic in diagnostics)
{
output.WriteDebugLine(CSharpDiagnosticFormatter.Instance.Format(diagnostic));
builder.AppendLine(CSharpDiagnosticFormatter.Instance.Format(diagnostic));
}
throw new CommandException(builder.ToString());
}
output.WriteDebugLine($"Done compiling {Path.GetFileName(scriptFilePath)}'.");
var pipeline = new CustomizationPipeline(
output,
rootDirectory: Path.GetDirectoryName(scriptFilePath)!,
name: Names.NormalizeToDns(Path.GetFileNameWithoutExtension(projectFile.Name)),
solution: null,
projectFile);
var holder = new PipelineHolder(pipeline);
output.WriteDebugLine($"Running {Path.GetFileName(scriptFilePath)}'.");
object obj;
try
{
var result = await script.RunAsync(holder);
obj = result.ReturnValue;
}
catch (Exception ex)
{
throw new CommandException($"Failed executing {Path.GetFileName(scriptFilePath)}'.", ex);
}
step.MarkComplete();
return new ApplicationWrapper(obj, Path.GetDirectoryName(scriptFilePath)!);
}
}
public static async Task<ApplicationWrapper?> RunCustomizationScriptAsync(OutputContext output, FileInfo solutionFile, SolutionFile solution)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (solutionFile is null)
{
throw new ArgumentNullException(nameof(solutionFile));
}
if (solution is null)
{
throw new ArgumentNullException(nameof(solution));
}
using (var step = output.BeginStep("Applying Application Customizations..."))
{
var scriptFilePath = Path.Combine(solutionFile.DirectoryName, "Opulence.csx");
output.WriteDebugLine($"Looking for customization script at '{scriptFilePath}'.");
if (!File.Exists(scriptFilePath))
{
output.WriteDebugLine($"No customization script found.");
step.MarkComplete("Skipping...");
return null;
}
output.WriteInfoLine($"Configuring application using '{Path.GetFileName(scriptFilePath)}'.");
var code = File.ReadAllText(scriptFilePath);
var script = CSharpScript.Create<object>(
code,
options: ScriptOptions.Default.WithImports(new []{ "System", "System.Collections.Generic", }),
globalsType: typeof(PipelineHolder),
assemblyLoader: null);
script = script.ContinueWith<object>(@"return await Pipeline.ExecuteAsync(__Pipeline);", options: ScriptOptions.Default);
output.WriteDebugLine($"Compiling {Path.GetFileName(scriptFilePath)}'.");
script.Compile();
var diagnostics = script.Compile();
if (diagnostics.Length > 0)
{
var builder = new StringBuilder();
output.WriteDebugLine($"Script '{scriptFilePath}' had compilation errors.");
builder.AppendLine($"Script '{scriptFilePath}' had compilation errors.");
foreach (var diagnostic in diagnostics)
{
output.WriteDebugLine(CSharpDiagnosticFormatter.Instance.Format(diagnostic));
builder.AppendLine(CSharpDiagnosticFormatter.Instance.Format(diagnostic));
}
throw new CommandException(builder.ToString());
}
output.WriteDebugLine($"Done compiling {Path.GetFileName(scriptFilePath)}'.");
var pipeline = new CustomizationPipeline(
output,
rootDirectory: Path.GetDirectoryName(scriptFilePath)!,
name: Names.NormalizeToDns(Path.GetFileNameWithoutExtension(solutionFile.Name)),
solution,
projectFile: null);
var holder = new PipelineHolder(pipeline);
output.WriteDebugLine($"Running {Path.GetFileName(scriptFilePath)}'.");
object obj;
try
{
var result = await script.RunAsync(holder);
obj = result.ReturnValue;
}
catch (Exception ex)
{
throw new CommandException($"Failed executing {Path.GetFileName(scriptFilePath)}'.", ex);
}
step.MarkComplete();
return new ApplicationWrapper(obj, Path.GetDirectoryName(scriptFilePath)!);
}
}
}
}

137
src/opulence/dotnet-opulence/ServiceExecutor.cs

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Opulence
{
internal sealed class ServiceExecutor
{
private readonly OutputContext output;
private readonly Application application;
private readonly Step[] steps;
public ServiceExecutor(OutputContext output, Application application, IEnumerable<Step> steps)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (application is null)
{
throw new ArgumentNullException(nameof(application));
}
if (steps is null)
{
throw new ArgumentNullException(nameof(steps));
}
this.output = output;
this.application = application;
this.steps = steps.ToArray();
}
public async Task ExecuteAsync(ServiceEntry service)
{
using var tracker = output.BeginStep($"Processing Service '{service.FriendlyName}'...");
for (var i = 0; i < steps.Length; i++)
{
var step = steps[i];
using var stepTracker = output.BeginStep(step.DisplayText);
await step.ExecuteAsync(output, application, service);
stepTracker.MarkComplete();
}
tracker.MarkComplete();
}
public abstract class Step
{
public abstract string DisplayText { get; }
public abstract Task ExecuteAsync(OutputContext output, Application application, ServiceEntry 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)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (service.Service.Source is Project p)
{
project = p;
return false;
}
output.WriteInfoLine($"Service '{service.FriendlyName}' does not have a project associated. Skipping.");
project = default!;
return true;
}
protected bool SkipWithoutContainerInfo(OutputContext output, ServiceEntry service, [MaybeNullWhen(returnValue: true)] out ContainerInfo container)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (service.Service.GeneratedAssets.Container is ContainerInfo c)
{
container = c;
return false;
}
output.WriteInfoLine($"Service '{service.FriendlyName}' does not produce a container. Skipping.");
container = default!;
return true;
}
protected bool SkipWithoutContainerOutput(OutputContext output, ServiceEntry service)
{
if (output is null)
{
throw new ArgumentNullException(nameof(output));
}
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (service.Outputs.OfType<DockerImageOutput>().Any())
{
return false;
}
output.WriteInfoLine($"Service '{service.FriendlyName}' does not have a container. Skipping.");
return true;
}
}
}
}

184
src/opulence/dotnet-opulence/StandardOptions.cs

@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.IO;
using System.Linq;
namespace Opulence
{
internal static class StandardOptions
{
private static readonly string[] AllOutputs = new string[] { "container", "chart", };
public static Option Environment
{
get
{
return new Option(new []{ "-e", "--environment" }, "Environemnt")
{
Argument = new Argument<string>("environment", () => "production")
{
Arity = ArgumentArity.ExactlyOne,
},
Required = false,
};
}
}
public static Option Force
{
get
{
return new Option("--force", "Force overwrite of existing files")
{
Argument = new Argument<bool>(),
};
}
}
public static Option Outputs
{
get
{
var argument = new Argument<List<string>>(TryConvert)
{
Arity = ArgumentArity.ZeroOrMore,
};
argument.AddSuggestions(AllOutputs);
argument.SetDefaultValue(new List<string>(AllOutputs));
return new Option(new[]{ "-o", "--outputs" }, "Outputs to generate")
{
Argument = argument,
};
static bool TryConvert(SymbolResult symbol, out List<string> outputs)
{
outputs = new List<string>();
foreach (var token in symbol.Tokens)
{
if (!AllOutputs.Any(item => string.Equals(item, token.Value, StringComparison.OrdinalIgnoreCase)))
{
symbol.ErrorMessage = $"output '{token.Value}' is not recognized";
outputs = default!;
return false;
}
outputs.Add(token.Value.ToLowerInvariant());
}
return true;
}
}
}
public static Option Project
{
get
{
var argument = new Argument<FileInfo>(TryParse, isDefault: true)
{
Arity = ArgumentArity.ZeroOrOne,
Name = "project-file or solution-file or directory",
};
return new Option(new [] { "-p", "--project", }, "Project file, Solution file or directory")
{
Argument = argument,
};
static bool TryFindProjectFile(string directoryPath, out string? projectFilePath, out string? errorMessage)
{
var matches = new List<string>();
foreach (var candidate in Directory.EnumerateFiles(directoryPath))
{
if (Path.GetExtension(candidate).EndsWith(".sln"))
{
matches.Add(candidate);
}
if (Path.GetExtension(candidate).EndsWith(".csproj"))
{
matches.Add(candidate);
}
}
// Prefer solution if both are in the same directory. This helps
// avoid some conflicts.
if (matches.Any(m => m.EndsWith(".sln")))
{
matches.RemoveAll(m => m.EndsWith(".csproj"));
}
if (matches.Count == 0)
{
errorMessage = $"No project file or solution file was found in directory '{directoryPath}'.";
projectFilePath = default;
return false;
}
else if (matches.Count == 1)
{
errorMessage = null;
projectFilePath = matches[0];
return true;
}
else
{
errorMessage = $"More than one project file or solution file was found in directory '{directoryPath}'.";
projectFilePath = default;
return false;
}
}
static FileInfo TryParse(ArgumentResult result)
{
var token = result.Tokens.Count switch
{
0 => ".",
1 => result.Tokens[0].Value,
_ => throw new InvalidOperationException("Unexpected token count."),
};
if (File.Exists(token))
{
return new FileInfo(token);
}
if (Directory.Exists(token))
{
if (TryFindProjectFile(token, out var filePath, out var errorMessage))
{
return new FileInfo(filePath);
}
else
{
result.ErrorMessage = errorMessage;
return default!;
}
}
result.ErrorMessage = $"The file '{token}' could not be found.";
return default!;
}
}
}
public static Option Verbosity
{
get
{
return new Option(new [] { "-v", "--verbosity" }, "Output verbostiy")
{
Argument = new Argument<Verbosity>("one of: quiet|info|debug", () => Opulence.Verbosity.Info)
{
Arity = ArgumentArity.ExactlyOne,
},
Required = false,
};
}
}
}
}

7
src/opulence/dotnet-opulence/StepView.cs

@ -0,0 +1,7 @@
namespace Opulence
{
internal class StepView
{
}
}

47
src/opulence/dotnet-opulence/TargetInstaller.cs

@ -0,0 +1,47 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Reflection;
namespace Opulence
{
public static class TargetInstaller
{
public static void Install(string projectFilePath)
{
if (projectFilePath is null)
{
throw new ArgumentNullException(nameof(projectFilePath));
}
var projectDirectory = Path.GetDirectoryName(projectFilePath);
var intermediateDirectory = Path.Combine(projectDirectory!, "obj");
Directory.CreateDirectory(intermediateDirectory);
var fileName = $"{Path.GetFileName(projectFilePath)}.opulence.targets";
var targetFilePath = Path.Combine(intermediateDirectory, fileName);
if (File.Exists(targetFilePath))
{
return;
}
var toolType = typeof(TargetInstaller);
var toolAssembly = toolType.GetTypeInfo().Assembly;
var toolImportTargetsResourceName = $"Opulence.Resources.Imports.targets";
using var stream = toolAssembly.GetManifestResourceStream(toolImportTargetsResourceName);
if (stream == null)
{
throw new CommandException("Failed to find resource. Valid names: " + string.Join(", ", toolAssembly.GetManifestResourceNames()));
}
var targetBytes = new byte[stream.Length];
stream.Read(targetBytes, 0, targetBytes.Length);
File.WriteAllBytes(targetFilePath, targetBytes);
}
}
}

27
src/opulence/dotnet-opulence/TempDirectory.cs

@ -0,0 +1,27 @@
using System;
using System.IO;
namespace Opulence
{
internal class TempDirectory : IDisposable
{
public static TempDirectory Create()
{
var directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(directoryPath);
return new TempDirectory(directoryPath);
}
private TempDirectory(string directoryPath)
{
DirectoryPath = directoryPath;
}
public string DirectoryPath { get; }
public void Dispose()
{
Directory.Delete(DirectoryPath, recursive: true);
}
}
}

26
src/opulence/dotnet-opulence/TempFile.cs

@ -0,0 +1,26 @@
using System;
using System.IO;
namespace Opulence
{
internal class TempFile : IDisposable
{
public static TempFile Create()
{
return new TempFile(Path.GetTempFileName());
}
public void Dispose()
{
File.Delete(FilePath);
}
private TempFile(string filePath)
{
FilePath = filePath;
}
public string FilePath { get; }
}
}

25
src/opulence/dotnet-opulence/Templates.cs

@ -0,0 +1,25 @@
using System.IO;
namespace Opulence
{
internal static class Templates
{
public static string RootDirectoryPath
{
get
{
return Path.Combine(
Path.GetDirectoryName(typeof(HelmChartGenerator).Assembly.Location)!,
"Templates");
}
}
public static string HelmChartsDirectoryPath
{
get
{
return Path.Combine(RootDirectoryPath, "Helm");
}
}
}
}

45
src/opulence/dotnet-opulence/Templates/Helm/templates/_helpers.tpl

@ -0,0 +1,45 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "thischart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "thischart.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "thischart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "thischart.labels" -}}
app.kubernetes.io/name: {{ include "thischart.name" . }}
helm.sh/chart: {{ include "thischart.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}

30
src/opulence/dotnet-opulence/Templates/Helm/templates/deployment.yaml

@ -0,0 +1,30 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "thischart.fullname" . }}
labels:
{{ include "thischart.labels" . | indent 4 }}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: {{ include "thischart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "thischart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
containers:
- name: {{ .Chart.Name }}
image: {{ .Values.image.repository }}:{{ .Chart.AppVersion }}
imagePullPolicy: Always
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
periodSeconds: 5
ports:
- containerPort: 80

16
src/opulence/dotnet-opulence/Templates/Helm/templates/service.yaml

@ -0,0 +1,16 @@
kind: Service
apiVersion: v1
metadata:
name: {{ include "thischart.name" . }}
labels:
app.kubernetes.io/name: {{ include "thischart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
selector:
app.kubernetes.io/name: {{ include "thischart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
ports:
- name: http
protocol: TCP
port: 80
type: LoadBalancer

9
src/opulence/dotnet-opulence/Verbosity.cs

@ -0,0 +1,9 @@
namespace Opulence
{
internal enum Verbosity
{
Quiet,
Info,
Debug,
}
}

43
src/opulence/dotnet-opulence/WriteServiceYamlStep.cs

@ -0,0 +1,43 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using YamlDotNet.RepresentationModel;
namespace Opulence
{
internal sealed class WriteServiceYamlStep : ServiceExecutor.Step
{
public override string DisplayText => "Writing Manifests...";
public bool Force { get; set; }
public DirectoryInfo OutputDirectory { get; set; } = default!;
public override Task ExecuteAsync(OutputContext output, Application application, ServiceEntry service)
{
var yaml = service.Outputs.OfType<IYamlManifestOutput>().ToArray();
if (yaml.Length == 0)
{
output.WriteDebugLine($"No yaml manifests found for service '{service.FriendlyName}'. Skipping.");
return Task.CompletedTask;
}
var outputFilePath = Path.Combine(OutputDirectory.FullName, $"{service.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.");
}
File.Delete(outputFilePath);
using var stream = File.OpenWrite(outputFilePath);
using var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
var yamlStream = new YamlStream(yaml.Select(y => y.Yaml));
yamlStream.Save(writer, assignAnchors: false);
return Task.CompletedTask;
}
}
}

28
src/opulence/dotnet-opulence/build/Opulence.targets

@ -0,0 +1,28 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<EvaluateOpulenceProjectInfoDependsOn>
$(EvaluateOpulenceProjectInfoDependsOn);
ResolveReferences;
ResolvePackageDependenciesDesignTime;
PrepareResources;
GetAssemblyAttributes;
</EvaluateOpulenceProjectInfoDependsOn>
</PropertyGroup>
<Target Name="EvaluateOpulenceProjectInfo" DependsOnTargets="$(EvaluateOpulenceProjectInfoDependsOn)">
<Error Message="OpulenceOutputFilePath was not specified." Condition="'$(OpulenceOutputFilePath)'==''" />
<PropertyGroup>
<_Frameworks>@(FrameworkReference, ',')</_Frameworks>
</PropertyGroup>
<ItemGroup>
<_Lines Include="version=$(InformationalVersion)" />
<_Lines Include="tfm=$(TargetFramework)" />
<_Lines Include="frameworks=$(_Frameworks)" />
</ItemGroup>
<WriteLinesToFile File="$(OpulenceOutputFilePath)" Lines="@(_Lines)" Overwrite="true" />
</Target>
</Project>

20
src/opulence/dotnet-opulence/buildMultiTargeting/Opulence.targets

@ -0,0 +1,20 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="GetNearestTargetFramework" AssemblyFile="$(MicrosoftNETBuildTasksAssembly)" />
<Target Name="EvaluateOpulenceProjectInfo" DependsOnTargets="$(EvaluateOpulenceProjectInfoDependsOn)">
<GetNearestTargetFramework
ReferringTargetFramework="netcoreapp3.0"
PossibleTargetFrameworks="$(TargetFrameworks)"
ProjectFilePath="$(MSBuildProjectFullPath)"
ContinueOnError="WarnAndContinue">
<Output PropertyName="NearestTargetFramework" TaskParameter="NearestTargetFramework" />
</GetNearestTargetFramework>
<Error Text="Opulence requires targeting netcoreapp3.0 and newer." Condition=" '$(NearestTargetFramework)' == '' " />
<MSBuild
Projects="$(MSBuildProjectFile)"
Targets="EvaluateOpulenceProjectInfo"
Properties="TargetFramework=$(NearestTargetFramework);OutputFile=$(OutputFile)" />
</Target>
</Project>

39
src/opulence/dotnet-opulence/dotnet-opulence.csproj

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Opulence</RootNamespace>
<PackAsTool>true</PackAsTool>
<PackageId>Opulence.dotnet-opulence</PackageId>
<ToolCommandName>opulence</ToolCommandName>
<Nullable>Enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="KubernetesClient" Version="1.6.11" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="3.4.0" />
<PackageReference Include="semver" Version="2.0.6" />
<PackageReference Include="Sln" Version="0.3.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20071.2" />
<PackageReference Include="System.Commandline.Rendering" Version="0.3.0-alpha.20070.2" />
<PackageReference Include="System.Reflection.Metadata" Version="1.8.0" />
<PackageReference Include="YamlDotNet.Signed" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Opulence\Opulence.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="build\**" CopyToOutputDirectory="PreserveNewest" />
<Content Include="buildMultiTargeting\**" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Templates\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Imports.targets" />
</ItemGroup>
</Project>

33
tye.sln

@ -11,6 +11,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F19B02EB-A
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E2ETest", "test\E2ETest\E2ETest.csproj", "{D15E5FF6-C1E7-4110-A2BE-06ADA7ACA82B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "opulence", "opulence", "{E8219571-549D-4EFE-8E62-54082D707B9A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Opulence", "src\opulence\Opulence\Opulence.csproj", "{0A8A0C69-49CB-462E-A7D4-605F78786EEF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-opulence", "src\opulence\dotnet-opulence\dotnet-opulence.csproj", "{BA03A616-4649-414D-A116-727D72F4BF50}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -48,9 +54,36 @@ Global
{D15E5FF6-C1E7-4110-A2BE-06ADA7ACA82B}.Release|x64.Build.0 = Release|Any CPU
{D15E5FF6-C1E7-4110-A2BE-06ADA7ACA82B}.Release|x86.ActiveCfg = Release|Any CPU
{D15E5FF6-C1E7-4110-A2BE-06ADA7ACA82B}.Release|x86.Build.0 = Release|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Debug|x64.ActiveCfg = Debug|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Debug|x64.Build.0 = Debug|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Debug|x86.ActiveCfg = Debug|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Debug|x86.Build.0 = Debug|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Release|Any CPU.Build.0 = Release|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Release|x64.ActiveCfg = Release|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Release|x64.Build.0 = Release|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Release|x86.ActiveCfg = Release|Any CPU
{0A8A0C69-49CB-462E-A7D4-605F78786EEF}.Release|x86.Build.0 = Release|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Debug|x64.ActiveCfg = Debug|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Debug|x64.Build.0 = Debug|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Debug|x86.ActiveCfg = Debug|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Debug|x86.Build.0 = Debug|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Release|Any CPU.Build.0 = Release|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Release|x64.ActiveCfg = Release|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Release|x64.Build.0 = Release|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Release|x86.ActiveCfg = Release|Any CPU
{BA03A616-4649-414D-A116-727D72F4BF50}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9BFE4070-D6D8-41F8-B92E-F85C3DCD7AB8} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601}
{D15E5FF6-C1E7-4110-A2BE-06ADA7ACA82B} = {F19B02EB-A372-417A-B2C2-EA0D5A3C76D5}
{E8219571-549D-4EFE-8E62-54082D707B9A} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601}
{0A8A0C69-49CB-462E-A7D4-605F78786EEF} = {E8219571-549D-4EFE-8E62-54082D707B9A}
{BA03A616-4649-414D-A116-727D72F4BF50} = {E8219571-549D-4EFE-8E62-54082D707B9A}
EndGlobalSection
EndGlobal

Loading…
Cancel
Save