5 changed files with 6 additions and 408 deletions
@ -1,137 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.CommandLine; |
|
||||
using System.CommandLine.Invocation; |
|
||||
using System.IO; |
|
||||
using System.Text; |
|
||||
using System.Threading.Tasks; |
|
||||
using Tye; |
|
||||
|
|
||||
namespace Opulence |
|
||||
{ |
|
||||
internal static 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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,193 +0,0 @@ |
|||||
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($"}});"); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,82 +1,9 @@ |
|||||
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 |
namespace Opulence |
||||
{ |
{ |
||||
class Program |
static class Program |
||||
{ |
{ |
||||
static async Task<int> Main(string[] args) |
public static void Main() |
||||
{ |
{ |
||||
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; |
|
||||
} |
} |
||||
} |
} |
||||
} |
} |
||||
Loading…
Reference in new issue