77 changed files with 4382 additions and 0 deletions
@ -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; } |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Opulence |
|||
{ |
|||
public enum DeploymentKind |
|||
{ |
|||
None, |
|||
Kubernetes, |
|||
Oam, |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Opulence |
|||
{ |
|||
public static class Environments |
|||
{ |
|||
public const string Development = "Development"; |
|||
|
|||
public const string Production = "Production"; |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Opulence |
|||
{ |
|||
public sealed class GeneratedAssets |
|||
{ |
|||
public ContainerInfo? Container { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Opulence |
|||
{ |
|||
public sealed class HelmChartStep : Step |
|||
{ |
|||
public override string DisplayName => "Building Helm Chart..."; |
|||
|
|||
public string ChartName { get; set; } = default!; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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!; |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Opulence |
|||
{ |
|||
public class Secret |
|||
{ |
|||
public string? Name { get; set; } |
|||
} |
|||
} |
|||
@ -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>(); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace Opulence |
|||
{ |
|||
public abstract class Source |
|||
{ |
|||
internal Source() |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Opulence |
|||
{ |
|||
public abstract class Step |
|||
{ |
|||
public abstract string DisplayName { get; } |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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}"; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
@ -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) |
|||
{ } |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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}'."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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!)); |
|||
} |
|||
} |
|||
} |
|||
@ -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."); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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("+", "-"); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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}'."); |
|||
} |
|||
} |
|||
} |
|||
@ -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)}'."); |
|||
|
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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")}"); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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."); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using YamlDotNet.RepresentationModel; |
|||
|
|||
namespace Opulence |
|||
{ |
|||
internal interface IYamlManifestOutput |
|||
{ |
|||
YamlDocument Yaml { get; } |
|||
} |
|||
} |
|||
@ -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($"}});"); |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"profiles": { |
|||
"dotnet-opulence": { |
|||
"commandName": "Project", |
|||
"commandLineArgs": "" |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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}'"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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)!); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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, |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Opulence |
|||
{ |
|||
internal class StepView |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
|
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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 -}} |
|||
@ -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 |
|||
@ -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 |
|||
@ -0,0 +1,9 @@ |
|||
namespace Opulence |
|||
{ |
|||
internal enum Verbosity |
|||
{ |
|||
Quiet, |
|||
Info, |
|||
Debug, |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
Loading…
Reference in new issue