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