From 3cd81839e0be8344cea8f3a07057e713ae43e8c5 Mon Sep 17 00:00:00 2001 From: Dave Sekula Date: Sun, 26 Nov 2023 04:26:58 +0000 Subject: [PATCH] NonInteractive mode for Apply and Destroy commands --- Directory.Build.props | 1 + .../InitializeConfigurationAction.cs | 19 +++----- .../ApplyManifestsToClusterAction.cs | 42 +++++++++++++----- .../RemoveManifestsFromClusterAction.cs | 44 ++++++++++++++----- .../Commands/Apply/ApplyCommand.cs | 6 ++- .../Commands/Apply/ApplyOptions.cs | 2 + .../Commands/Destroy/DestroyCommand.cs | 6 ++- .../Commands/Destroy/DestroyOptions.cs | 2 + src/Aspirate.Cli/Commands/SharedOptions.cs | 7 +++ src/Aspirate.Shared/Actions/ActionExecutor.cs | 15 ++++++- .../BaseActionWithNonInteractiveSupport.cs | 8 +++- src/Aspirate.Shared/Aspirate.Shared.csproj | 1 + .../Models/State/AspirateState.cs | 4 +- 13 files changed, 112 insertions(+), 45 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index bd9484f..0320464 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,6 +2,7 @@ true + $(NoWarn);NU5104 true true true diff --git a/src/Aspirate.Cli/Actions/Configuration/InitializeConfigurationAction.cs b/src/Aspirate.Cli/Actions/Configuration/InitializeConfigurationAction.cs index 9c718ef..3da6868 100644 --- a/src/Aspirate.Cli/Actions/Configuration/InitializeConfigurationAction.cs +++ b/src/Aspirate.Cli/Actions/Configuration/InitializeConfigurationAction.cs @@ -9,11 +9,6 @@ public class InitializeConfigurationAction( public override Task ExecuteAsync() { - if (CurrentState.NonInteractive) - { - ValidateNonInteractiveState(); - } - configurationService.HandleExistingConfiguration(CurrentState.ProjectPath, CurrentState.NonInteractive); var aspirateSettings = PerformConfigurationBootstrapping(); @@ -171,30 +166,26 @@ private void HandleCreateTemplateFiles(AspirateSettings aspirateConfiguration) } } - protected override void ValidateNonInteractiveState() + public override void ValidateNonInteractiveState() { if (string.IsNullOrEmpty(CurrentState.ProjectPath)) { - Logger.MarkupLine("\r\n[red](!)[/] Project path must be supplied when running in non-interactive mode."); - throw new ActionCausesExitException(9999); + NonInteractiveValidationFailed("Project path must be supplied when running in non-interactive mode."); } if (string.IsNullOrEmpty(CurrentState.ContainerRegistry)) { - Logger.MarkupLine("\r\n[red](!)[/] Container registry must be supplied when running in non-interactive mode."); - throw new ActionCausesExitException(9999); + NonInteractiveValidationFailed("Container Registry must be supplied when running in non-interactive mode."); } if (string.IsNullOrEmpty(CurrentState.ContainerImageTag)) { - Logger.MarkupLine("\r\n[red](!)[/] Container image tag must be supplied when running in non-interactive mode."); - throw new ActionCausesExitException(9999); + NonInteractiveValidationFailed("Container image tag must be supplied when running in non-interactive mode."); } if (string.IsNullOrEmpty(CurrentState.TemplatePath)) { - Logger.MarkupLine("\r\n[red](!)[/] Template path must be supplied when running in non-interactive mode."); - throw new ActionCausesExitException(9999); + NonInteractiveValidationFailed("Template path must be supplied when running in non-interactive mode."); } } } diff --git a/src/Aspirate.Cli/Actions/Manifests/ApplyManifestsToClusterAction.cs b/src/Aspirate.Cli/Actions/Manifests/ApplyManifestsToClusterAction.cs index c4bb996..e2bb7e0 100644 --- a/src/Aspirate.Cli/Actions/Manifests/ApplyManifestsToClusterAction.cs +++ b/src/Aspirate.Cli/Actions/Manifests/ApplyManifestsToClusterAction.cs @@ -1,30 +1,48 @@ namespace Aspirate.Cli.Actions.Manifests; -public sealed class ApplyManifestsToClusterAction(IKubeCtlService kubeCtlService, IServiceProvider serviceProvider) : BaseAction(serviceProvider) +public sealed class ApplyManifestsToClusterAction(IKubeCtlService kubeCtlService, IServiceProvider serviceProvider) : BaseActionWithNonInteractiveSupport(serviceProvider) { public const string ActionKey = "ApplyManifestsToClusterAction"; public override async Task ExecuteAsync() { - Logger.WriteLine(); - var shouldDeploy = Logger.Confirm("[bold]Would you like to deploy the generated manifests to a kubernetes cluster defined in your kubeconfig file?[/]"); - if (!shouldDeploy) + if (!CurrentState.NonInteractive) { - Logger.MarkupLine("[yellow]Cancelled![/]"); + Logger.WriteLine(); + var shouldDeploy = Logger.Confirm( + "[bold]Would you like to deploy the generated manifests to a kubernetes cluster defined in your kubeconfig file?[/]"); - return true; + if (!shouldDeploy) + { + Logger.MarkupLine("[yellow]Cancelled![/]"); + + return true; + } + + CurrentState.KubeContext = await kubeCtlService.SelectKubernetesContextForDeployment(); + + if (!CurrentState.ActiveKubernetesContextIsSet) + { + return false; + } } - CurrentState.ActiveKubernetesContext = await kubeCtlService.SelectKubernetesContextForDeployment(); + await kubeCtlService.ApplyManifests(CurrentState.KubeContext, CurrentState.InputPath); + Logger.MarkupLine($"\r\n\t[green]({EmojiLiterals.CheckMark}) Done:[/] Deployments successfully applied to cluster [blue]'{CurrentState.KubeContext}'[/]"); + + return true; + } + public override void ValidateNonInteractiveState() + { if (!CurrentState.ActiveKubernetesContextIsSet) { - return false; + NonInteractiveValidationFailed("Cannot apply manifests to cluster without specifying the kubernetes context to use."); } - await kubeCtlService.ApplyManifests(CurrentState.ActiveKubernetesContext, CurrentState.InputPath); - Logger.MarkupLine($"\r\n\t[green]({EmojiLiterals.CheckMark}) Done:[/] Deployments successfully applied to cluster [blue]'{CurrentState.ActiveKubernetesContext}'[/]"); - - return true; + if (string.IsNullOrEmpty(CurrentState.InputPath)) + { + NonInteractiveValidationFailed("Cannot apply manifests to cluster without specifying the input path to use for manifests."); + } } } diff --git a/src/Aspirate.Cli/Actions/Manifests/RemoveManifestsFromClusterAction.cs b/src/Aspirate.Cli/Actions/Manifests/RemoveManifestsFromClusterAction.cs index fff68e5..08d56fa 100644 --- a/src/Aspirate.Cli/Actions/Manifests/RemoveManifestsFromClusterAction.cs +++ b/src/Aspirate.Cli/Actions/Manifests/RemoveManifestsFromClusterAction.cs @@ -1,30 +1,50 @@ namespace Aspirate.Cli.Actions.Manifests; -public sealed class RemoveManifestsFromClusterAction(IKubeCtlService kubeCtlService, IServiceProvider serviceProvider) : BaseAction(serviceProvider) +public sealed class RemoveManifestsFromClusterAction(IKubeCtlService kubeCtlService, IServiceProvider serviceProvider) : + BaseActionWithNonInteractiveSupport(serviceProvider) { public const string ActionKey = "RemoveManifestsFromClusterAction"; public override async Task ExecuteAsync() { - Logger.WriteLine(); - var shouldDeploy = Logger.Confirm("[bold]Would you like to remove the deployed manifests from a kubernetes cluster defined in your kubeconfig file?[/]"); - if (!shouldDeploy) + if (!CurrentState.NonInteractive) { - Logger.MarkupLine("[yellow]Cancelled![/]"); - return true; + Logger.WriteLine(); + var shouldDeploy = Logger.Confirm( + "[bold]Would you like to remove the deployed manifests from a kubernetes cluster defined in your kubeconfig file?[/]"); + + if (!shouldDeploy) + { + Logger.MarkupLine("[yellow]Cancelled![/]"); + + return true; + } + + CurrentState.KubeContext = await kubeCtlService.SelectKubernetesContextForDeployment(); + + if (!CurrentState.ActiveKubernetesContextIsSet) + { + return false; + } } - CurrentState.ActiveKubernetesContext = await kubeCtlService.SelectKubernetesContextForDeployment(); + await kubeCtlService.RemoveManifests(CurrentState.KubeContext, CurrentState.InputPath); + Logger.MarkupLine($"\r\n\t[green]({EmojiLiterals.CheckMark}) Done:[/] Deployments removed from cluster [blue]'{CurrentState.KubeContext}'[/]"); + + return true; + } + public override void ValidateNonInteractiveState() + { if (!CurrentState.ActiveKubernetesContextIsSet) { - return false; + NonInteractiveValidationFailed("Cannot remove manifests from a cluster without specifying the kubernetes context to use."); } - await kubeCtlService.RemoveManifests(CurrentState.ActiveKubernetesContext, CurrentState.InputPath); - Logger.MarkupLine($"\r\n\t[green]({EmojiLiterals.CheckMark}) Done:[/] Deployments removed from cluster [blue]'{CurrentState.ActiveKubernetesContext}'[/]"); - - return true; + if (string.IsNullOrEmpty(CurrentState.InputPath)) + { + NonInteractiveValidationFailed("Cannot remove manifests from a cluster without specifying the input path to use for manifests."); + } } } diff --git a/src/Aspirate.Cli/Commands/Apply/ApplyCommand.cs b/src/Aspirate.Cli/Commands/Apply/ApplyCommand.cs index 5eb7e78..d950a61 100644 --- a/src/Aspirate.Cli/Commands/Apply/ApplyCommand.cs +++ b/src/Aspirate.Cli/Commands/Apply/ApplyCommand.cs @@ -2,6 +2,10 @@ namespace Aspirate.Cli.Commands.Apply; public sealed class ApplyCommand : BaseCommand { - public ApplyCommand() : base("apply", "Apply the generated kustomize manifest to the cluster.") => + public ApplyCommand() : base("apply", "Apply the generated kustomize manifest to the cluster.") + { AddOption(SharedOptions.ManifestDirectoryPath); + AddOption(SharedOptions.KubernetesContext); + AddOption(SharedOptions.NonInteractive); + } } diff --git a/src/Aspirate.Cli/Commands/Apply/ApplyOptions.cs b/src/Aspirate.Cli/Commands/Apply/ApplyOptions.cs index 042b88e..72de848 100644 --- a/src/Aspirate.Cli/Commands/Apply/ApplyOptions.cs +++ b/src/Aspirate.Cli/Commands/Apply/ApplyOptions.cs @@ -3,4 +3,6 @@ namespace Aspirate.Cli.Commands.Apply; public sealed class ApplyOptions : ICommandOptions { public string InputPath { get; init; } = AspirateLiterals.DefaultOutputPath; + public string? KubeContext { get; set; } + public bool NonInteractive { get; set; } = false; } diff --git a/src/Aspirate.Cli/Commands/Destroy/DestroyCommand.cs b/src/Aspirate.Cli/Commands/Destroy/DestroyCommand.cs index e9c6a91..c7a5c4d 100644 --- a/src/Aspirate.Cli/Commands/Destroy/DestroyCommand.cs +++ b/src/Aspirate.Cli/Commands/Destroy/DestroyCommand.cs @@ -2,6 +2,10 @@ namespace Aspirate.Cli.Commands.Destroy; public sealed class DestroyCommand : BaseCommand { - public DestroyCommand() : base("destroy", "Removes the manifests from your cluster..") => + public DestroyCommand() : base("destroy", "Removes the manifests from your cluster..") + { AddOption(SharedOptions.ManifestDirectoryPath); + AddOption(SharedOptions.KubernetesContext); + AddOption(SharedOptions.NonInteractive); + } } diff --git a/src/Aspirate.Cli/Commands/Destroy/DestroyOptions.cs b/src/Aspirate.Cli/Commands/Destroy/DestroyOptions.cs index 1eef558..f3ad2a4 100644 --- a/src/Aspirate.Cli/Commands/Destroy/DestroyOptions.cs +++ b/src/Aspirate.Cli/Commands/Destroy/DestroyOptions.cs @@ -3,4 +3,6 @@ namespace Aspirate.Cli.Commands.Destroy; public sealed class DestroyOptions : ICommandOptions { public string InputPath { get; init; } = AspirateLiterals.DefaultOutputPath; + public string? KubeContext { get; set; } + public bool NonInteractive { get; set; } = false; } diff --git a/src/Aspirate.Cli/Commands/SharedOptions.cs b/src/Aspirate.Cli/Commands/SharedOptions.cs index 92208b4..4a15036 100644 --- a/src/Aspirate.Cli/Commands/SharedOptions.cs +++ b/src/Aspirate.Cli/Commands/SharedOptions.cs @@ -23,6 +23,13 @@ public static class SharedOptions IsRequired = false, }; + public static Option KubernetesContext => new(new[] { "-k", "--kube-context" }) + { + Description = "The name of the kubernetes context to use", + Arity = ArgumentArity.ExactlyOne, + IsRequired = false, + }; + public static Option NonInteractive => new(new[] { "--non-interactive" }) { Description = "Disables interactive mode for the command", diff --git a/src/Aspirate.Shared/Actions/ActionExecutor.cs b/src/Aspirate.Shared/Actions/ActionExecutor.cs index 2d8b46f..48417ff 100644 --- a/src/Aspirate.Shared/Actions/ActionExecutor.cs +++ b/src/Aspirate.Shared/Actions/ActionExecutor.cs @@ -1,8 +1,9 @@ namespace Aspirate.Shared.Actions; -public class ActionExecutor(IAnsiConsole console, IServiceProvider serviceProvider) +public class ActionExecutor(IAnsiConsole console, IServiceProvider serviceProvider, AspirateState state) { - public static ActionExecutor CreateInstance(IServiceProvider serviceProvider) => new(serviceProvider.GetRequiredService(), serviceProvider); + public static ActionExecutor CreateInstance(IServiceProvider serviceProvider) => + new(serviceProvider.GetRequiredService(), serviceProvider, serviceProvider.GetRequiredService()); private readonly Queue _actionQueue = new(); @@ -16,6 +17,11 @@ public ActionExecutor QueueAction(string actionKey, Func? onFailure = null public async Task ExecuteCommandsAsync() { + if (state.NonInteractive) + { + console.MarkupLine("[blue]Non-interactive mode enabled.[/]"); + } + while (_actionQueue.Count > 0) { var executionAction = _actionQueue.Dequeue(); @@ -23,6 +29,11 @@ public async Task ExecuteCommandsAsync() try { + if (state.NonInteractive && action is BaseActionWithNonInteractiveSupport nonInteractiveAction) + { + nonInteractiveAction.ValidateNonInteractiveState(); + } + var successfullyCompleted = await action.ExecuteAsync(); if (successfullyCompleted) diff --git a/src/Aspirate.Shared/Actions/BaseActionWithNonInteractiveSupport.cs b/src/Aspirate.Shared/Actions/BaseActionWithNonInteractiveSupport.cs index 5ecc681..80a9698 100644 --- a/src/Aspirate.Shared/Actions/BaseActionWithNonInteractiveSupport.cs +++ b/src/Aspirate.Shared/Actions/BaseActionWithNonInteractiveSupport.cs @@ -2,5 +2,11 @@ namespace Aspirate.Shared.Actions; public abstract class BaseActionWithNonInteractiveSupport(IServiceProvider serviceProvider) : BaseAction(serviceProvider) { - protected abstract void ValidateNonInteractiveState(); + public abstract void ValidateNonInteractiveState(); + + protected void NonInteractiveValidationFailed(string message) + { + Logger.MarkupLine($"\r\n[red](!)[/] {message}"); + throw new ActionCausesExitException(9999); + } } diff --git a/src/Aspirate.Shared/Aspirate.Shared.csproj b/src/Aspirate.Shared/Aspirate.Shared.csproj index 038ea56..bbf2a80 100644 --- a/src/Aspirate.Shared/Aspirate.Shared.csproj +++ b/src/Aspirate.Shared/Aspirate.Shared.csproj @@ -2,6 +2,7 @@ net8.0 + false diff --git a/src/Aspirate.Shared/Models/State/AspirateState.cs b/src/Aspirate.Shared/Models/State/AspirateState.cs index 2297567..3fdbb1a 100644 --- a/src/Aspirate.Shared/Models/State/AspirateState.cs +++ b/src/Aspirate.Shared/Models/State/AspirateState.cs @@ -9,12 +9,12 @@ public class AspirateState public string? ContainerRegistry { get; set; } public string? ContainerImageTag { get; set; } public string? TemplatePath { get; set; } - public string? ActiveKubernetesContext { get; set; } + public string? KubeContext { get; set; } public bool NonInteractive { get; set; } public List AspireComponentsToProcess { get; set; } = []; public Dictionary LoadedAspireManifestResources { get; set; } = []; public Dictionary FinalResources { get; } = []; - public bool ActiveKubernetesContextIsSet => !string.IsNullOrEmpty(ActiveKubernetesContext); + public bool ActiveKubernetesContextIsSet => !string.IsNullOrEmpty(KubeContext); public List> SelectedProjectComponents => LoadedAspireManifestResources .Where(x => x.Value is Project && AspireComponentsToProcess.Contains(x.Key))