From a3740e7664b48d44d0e133285572045e8b67adff Mon Sep 17 00:00:00 2001 From: prom3theu5 Date: Sun, 26 Nov 2023 04:49:46 +0000 Subject: [PATCH] Adds noninteractive mode for generate command, as well as --skip-build which skips build and push (#50) --- .../BuildAndPushContainersAction.cs | 14 ++++- .../Manifests/LoadAspireManifestAction.cs | 22 ++++++- .../Commands/Generate/GenerateCommand.cs | 2 + .../Commands/Generate/GenerateOptions.cs | 7 ++- src/Aspirate.Cli/Commands/SharedOptions.cs | 7 +++ .../Components/Project/ProjectProcessor.cs | 4 +- .../Services/ContainerCompositionService.cs | 60 ++++++++++++++----- .../Models/State/AspirateState.cs | 1 + .../Services/IContainerCompositionService.cs | 2 +- 9 files changed, 94 insertions(+), 25 deletions(-) diff --git a/src/Aspirate.Cli/Actions/Containers/BuildAndPushContainersAction.cs b/src/Aspirate.Cli/Actions/Containers/BuildAndPushContainersAction.cs index 885f34d..90abb29 100644 --- a/src/Aspirate.Cli/Actions/Containers/BuildAndPushContainersAction.cs +++ b/src/Aspirate.Cli/Actions/Containers/BuildAndPushContainersAction.cs @@ -1,12 +1,18 @@ namespace Aspirate.Cli.Actions.Containers; public sealed class BuildAndPushContainersAction( - IServiceProvider serviceProvider) : BaseAction(serviceProvider) + IServiceProvider serviceProvider) : BaseActionWithNonInteractiveSupport(serviceProvider) { public const string ActionKey = "BuildAndPushContainersAction"; public override async Task ExecuteAsync() { + if (CurrentState.SkipBuild) + { + Logger.MarkupLine("\r\n[bold]Skipping build and push action as requested.[/]"); + return true; + } + if (NoSelectedProjectComponents()) { return true; @@ -18,7 +24,7 @@ public override async Task ExecuteAsync() foreach (var resource in CurrentState.SelectedProjectComponents) { - await projectProcessor.BuildAndPushProjectContainer(resource); + await projectProcessor.BuildAndPushProjectContainer(resource, CurrentState.NonInteractive); } Logger.MarkupLine("\r\n[bold]Building and push completed for all selected project components.[/]"); @@ -36,4 +42,8 @@ private bool NoSelectedProjectComponents() Logger.MarkupLine("\r\n[bold]No project components selected. Skipping build and publish action.[/]"); return true; } + + public override void ValidateNonInteractiveState() + { + } } diff --git a/src/Aspirate.Cli/Actions/Manifests/LoadAspireManifestAction.cs b/src/Aspirate.Cli/Actions/Manifests/LoadAspireManifestAction.cs index 2f1e22a..6d1e8cb 100644 --- a/src/Aspirate.Cli/Actions/Manifests/LoadAspireManifestAction.cs +++ b/src/Aspirate.Cli/Actions/Manifests/LoadAspireManifestAction.cs @@ -2,7 +2,7 @@ namespace Aspirate.Cli.Actions.Manifests; public class LoadAspireManifestAction( IManifestFileParserService manifestFileParserService, - IServiceProvider serviceProvider) : BaseAction(serviceProvider) + IServiceProvider serviceProvider) : BaseActionWithNonInteractiveSupport(serviceProvider) { public const string ActionKey = "LoadAspireManifestAction"; @@ -17,8 +17,15 @@ public override Task ExecuteAsync() return Task.FromResult(true); } - private List SelectManifestItemsToProcess() => - Logger.Prompt( + private List SelectManifestItemsToProcess() + { + if (CurrentState.NonInteractive) + { + Logger.MarkupLine("\r\n[blue]Non-Interactive Mode: Processing all components in the loaded file.[/]\r\n"); + return CurrentState.LoadedAspireManifestResources.Keys.ToList(); + } + + return Logger.Prompt( new MultiSelectionPrompt() .Title("Select [green]components[/] to process from the loaded file") .PageSize(10) @@ -28,4 +35,13 @@ private List SelectManifestItemsToProcess() => "[grey](Press [blue][/] to toggle a component, " + "[green][/] to accept)[/]") .AddChoiceGroup("All Components", CurrentState.LoadedAspireManifestResources.Keys.ToList())); + } + + public override void ValidateNonInteractiveState() + { + if (string.IsNullOrEmpty(CurrentState.ProjectManifest)) + { + NonInteractiveValidationFailed("No Aspire Manifest file was supplied."); + } + } } diff --git a/src/Aspirate.Cli/Commands/Generate/GenerateCommand.cs b/src/Aspirate.Cli/Commands/Generate/GenerateCommand.cs index 9f8e6fb..36008bd 100644 --- a/src/Aspirate.Cli/Commands/Generate/GenerateCommand.cs +++ b/src/Aspirate.Cli/Commands/Generate/GenerateCommand.cs @@ -6,5 +6,7 @@ public GenerateCommand() : base("generate", "Builds, pushes containers, generate { AddOption(SharedOptions.AspireProjectPath); AddOption(SharedOptions.OutputPath); + AddOption(SharedOptions.NonInteractive); + AddOption(SharedOptions.SkipBuild); } } diff --git a/src/Aspirate.Cli/Commands/Generate/GenerateOptions.cs b/src/Aspirate.Cli/Commands/Generate/GenerateOptions.cs index 504fe4c..5a816d8 100644 --- a/src/Aspirate.Cli/Commands/Generate/GenerateOptions.cs +++ b/src/Aspirate.Cli/Commands/Generate/GenerateOptions.cs @@ -2,6 +2,9 @@ namespace Aspirate.Cli.Commands.Generate; public sealed class GenerateOptions : ICommandOptions { - public string ProjectPath { get; init; } = AspirateLiterals.DefaultAspireProjectPath; - public string OutputPath { get; init; } = AspirateLiterals.DefaultOutputPath; + public string ProjectPath { get; set; } = AspirateLiterals.DefaultAspireProjectPath; + public string OutputPath { get; set; } = AspirateLiterals.DefaultOutputPath; + + public bool SkipBuild { get; set; } = false; + public bool NonInteractive { get; set; } = false; } diff --git a/src/Aspirate.Cli/Commands/SharedOptions.cs b/src/Aspirate.Cli/Commands/SharedOptions.cs index 4a15036..644db7e 100644 --- a/src/Aspirate.Cli/Commands/SharedOptions.cs +++ b/src/Aspirate.Cli/Commands/SharedOptions.cs @@ -30,6 +30,13 @@ public static class SharedOptions IsRequired = false, }; + public static Option SkipBuild => new(new[] { "--skip-build" }) + { + Description = "Skips build and Push of containers", + Arity = ArgumentArity.ZeroOrOne, + IsRequired = false, + }; + public static Option NonInteractive => new(new[] { "--non-interactive" }) { Description = "Disables interactive mode for the command", diff --git a/src/Aspirate.Cli/Processors/Components/Project/ProjectProcessor.cs b/src/Aspirate.Cli/Processors/Components/Project/ProjectProcessor.cs index 8d472b5..66595ca 100644 --- a/src/Aspirate.Cli/Processors/Components/Project/ProjectProcessor.cs +++ b/src/Aspirate.Cli/Processors/Components/Project/ProjectProcessor.cs @@ -55,7 +55,7 @@ public override Task CreateManifests(KeyValuePair resour return Task.FromResult(true); } - public async Task BuildAndPushProjectContainer(KeyValuePair resource) + public async Task BuildAndPushProjectContainer(KeyValuePair resource, bool nonInteractive) { var project = resource.Value as AspireProject; @@ -64,7 +64,7 @@ public async Task BuildAndPushProjectContainer(KeyValuePair re throw new InvalidOperationException($"Container details for project {resource.Key} not found."); } - await containerCompositionService.BuildAndPushContainerForProject(project, containerDetails); + await containerCompositionService.BuildAndPushContainerForProject(project, containerDetails, nonInteractive); _console.MarkupLine($"\t[green]({EmojiLiterals.CheckMark}) Done: [/] Building and Pushing container for project [blue]{resource.Key}[/]"); } diff --git a/src/Aspirate.Cli/Services/ContainerCompositionService.cs b/src/Aspirate.Cli/Services/ContainerCompositionService.cs index 11880cd..69580a9 100644 --- a/src/Aspirate.Cli/Services/ContainerCompositionService.cs +++ b/src/Aspirate.Cli/Services/ContainerCompositionService.cs @@ -5,7 +5,10 @@ public sealed class ContainerCompositionService(IFileSystem filesystem, IAnsiCon private readonly StringBuilder _stdOutBuffer = new(); private readonly StringBuilder _stdErrBuffer = new(); - public async Task BuildAndPushContainerForProject(Project project, MsBuildContainerProperties containerDetails) + public async Task BuildAndPushContainerForProject( + Project project, + MsBuildContainerProperties containerDetails, + bool nonInteractive) { _stdErrBuffer.Clear(); _stdOutBuffer.Clear(); @@ -17,12 +20,15 @@ public async Task BuildAndPushContainerForProject(Project project, MsBuild await AddProjectPublishArguments(argumentsBuilder, fullProjectPath); AddContainerDetailsToArguments(argumentsBuilder, containerDetails); - await ExecuteCommand(argumentsBuilder, onFailed: HandleBuildErrors); + await ExecuteCommand(argumentsBuilder, nonInteractive, onFailed: HandleBuildErrors); return true; } - private async Task ExecuteCommand(ArgumentsBuilder argumentsBuilder, Func? onFailed = default) + private async Task ExecuteCommand( + ArgumentsBuilder argumentsBuilder, + bool nonInteractive, + Func? onFailed = default) { var arguments = argumentsBuilder.RenderArguments(); @@ -51,6 +57,7 @@ private async Task ExecuteCommand(ArgumentsBuilder argumentsBuilder, Func ExecuteCommandNoOutput(string command, IReadOnly return commandResult.ExitCode != 0; } - private Task HandleBuildErrors(ArgumentsBuilder argumentsBuilder, string errors) + private Task HandleBuildErrors(ArgumentsBuilder argumentsBuilder, bool nonInteractive, string errors) { if (errors.Contains(DotNetSdkLiterals.DuplicateFileOutputError, StringComparison.OrdinalIgnoreCase)) { - return HandleDuplicateFilesInOutput(argumentsBuilder); + return HandleDuplicateFilesInOutput(argumentsBuilder, nonInteractive); } if (errors.Contains(DotNetSdkLiterals.NoContainerRegistryAccess, StringComparison.OrdinalIgnoreCase)) { - return HandleNoDockerRegistryAccess(argumentsBuilder); + return HandleNoDockerRegistryAccess(argumentsBuilder, nonInteractive); } if (errors.Contains(DotNetSdkLiterals.UnknownContainerRegistryAddress, StringComparison.OrdinalIgnoreCase)) @@ -96,9 +103,9 @@ private Task HandleBuildErrors(ArgumentsBuilder argumentsBuilder, string errors) throw new ActionCausesExitException(9999); } - private Task HandleDuplicateFilesInOutput(ArgumentsBuilder argumentsBuilder) + private Task HandleDuplicateFilesInOutput(ArgumentsBuilder argumentsBuilder, bool nonInteractive) { - var shouldRetry = AskIfShouldRetryHandlingDuplicateFiles(); + var shouldRetry = AskIfShouldRetryHandlingDuplicateFiles(nonInteractive); if (shouldRetry) { argumentsBuilder.AppendArgument(DotNetSdkLiterals.ErrorOnDuplicatePublishOutputFilesArgument, "false"); @@ -106,15 +113,21 @@ private Task HandleDuplicateFilesInOutput(ArgumentsBuilder argumentsBuilder) _stdErrBuffer.Clear(); _stdOutBuffer.Clear(); - return ExecuteCommand(argumentsBuilder, HandleBuildErrors); + return ExecuteCommand(argumentsBuilder, nonInteractive, HandleBuildErrors); } throw new ActionCausesExitException(9999); } - private async Task HandleNoDockerRegistryAccess(ArgumentsBuilder argumentsBuilder) + private async Task HandleNoDockerRegistryAccess(ArgumentsBuilder argumentsBuilder, bool nonInteractive) { - var shouldLogin = AskIfShouldLoginToDocker(); + if (nonInteractive) + { + console.MarkupLine($"\r\n[red bold]{DotNetSdkLiterals.NoContainerRegistryAccess}: No access to container registry. Cannot attempt login in non interactive mode.[/]"); + throw new ActionCausesExitException(1000); + } + + var shouldLogin = AskIfShouldLoginToDocker(nonInteractive); if (shouldLogin) { var credentials = GatherDockerCredentials(); @@ -125,16 +138,33 @@ private async Task HandleNoDockerRegistryAccess(ArgumentsBuilder argumentsBuilde { await ExecuteCommand( argumentsBuilder, + nonInteractive, onFailed: HandleBuildErrors); } } } - private bool AskIfShouldRetryHandlingDuplicateFiles() => - console.Confirm("\r\n[red bold]Implicitly, dotnet publish does not allow duplicate filenames to be output to the artefact directory at build time.\r\nWould you like to retry the build explicitly allowing them?[/]\r\n"); + private bool AskIfShouldRetryHandlingDuplicateFiles(bool nonInteractive) + { + if (nonInteractive) + { + return true; + } - private bool AskIfShouldLoginToDocker() => - console.Confirm("\r\nWe could not access the container registry during build. Do you want to login to the registry and retry?\r\n"); + return console.Confirm( + "\r\n[red bold]Implicitly, dotnet publish does not allow duplicate filenames to be output to the artefact directory at build time.\r\nWould you like to retry the build explicitly allowing them?[/]\r\n"); + } + + private bool AskIfShouldLoginToDocker(bool nonInteractive) + { + if (nonInteractive) + { + return false; + } + + return console.Confirm( + "\r\nWe could not access the container registry during build. Do you want to login to the registry and retry?\r\n"); + } private Dictionary GatherDockerCredentials() { diff --git a/src/Aspirate.Shared/Models/State/AspirateState.cs b/src/Aspirate.Shared/Models/State/AspirateState.cs index 3fdbb1a..b1d9f1f 100644 --- a/src/Aspirate.Shared/Models/State/AspirateState.cs +++ b/src/Aspirate.Shared/Models/State/AspirateState.cs @@ -11,6 +11,7 @@ public class AspirateState public string? TemplatePath { get; set; } public string? KubeContext { get; set; } public bool NonInteractive { get; set; } + public bool SkipBuild { get; set; } public List AspireComponentsToProcess { get; set; } = []; public Dictionary LoadedAspireManifestResources { get; set; } = []; public Dictionary FinalResources { get; } = []; diff --git a/src/Aspirate.Shared/Services/IContainerCompositionService.cs b/src/Aspirate.Shared/Services/IContainerCompositionService.cs index 8202a84..3eb79db 100644 --- a/src/Aspirate.Shared/Services/IContainerCompositionService.cs +++ b/src/Aspirate.Shared/Services/IContainerCompositionService.cs @@ -2,5 +2,5 @@ namespace Aspirate.Shared.Services; public interface IContainerCompositionService { - Task BuildAndPushContainerForProject(Project project, MsBuildContainerProperties containerDetails); + Task BuildAndPushContainerForProject(Project project, MsBuildContainerProperties containerDetails, bool nonInteractive); }