From 671467590c5348fac641e4369d4c3191e199b6a8 Mon Sep 17 00:00:00 2001 From: Mark Raming Date: Sun, 25 Sep 2022 21:42:24 +0200 Subject: [PATCH 1/4] New List-Runbooks, List-RunbookRuns and Delete-RunbookRuns commands added --- .../RunbookRun/DeleteRunbookRunsCommand.cs | 89 +++++++++++++ .../RunbookRun/ListRunbookRunsCommand.cs | 111 ++++++++++++++++ .../RunbookRun/RunbookRunCommandBase.cs | 119 ++++++++++++++++++ .../Commands/Runbooks/ListRunbooksCommand.cs | 68 ++++++++++ .../Commands/Runbooks/RunbookCommandBase.cs | 59 +++++++++ 5 files changed, 446 insertions(+) create mode 100644 source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs create mode 100644 source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs create mode 100644 source/Octopus.Cli/Commands/RunbookRun/RunbookRunCommandBase.cs create mode 100644 source/Octopus.Cli/Commands/Runbooks/ListRunbooksCommand.cs create mode 100644 source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs diff --git a/source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs b/source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs new file mode 100644 index 0000000000..82b6ec68d6 --- /dev/null +++ b/source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Octopus.Cli.Infrastructure; +using Octopus.Cli.Repositories; +using Octopus.Cli.Util; +using Octopus.Client; +using Octopus.Client.Model; +using Octopus.CommandLine; +using Octopus.CommandLine.Commands; +using Octopus.Versioning.Octopus; + +namespace Octopus.Cli.Commands.RunbooksRun +{ + [Command("delete-runbookruns", Description = "Deletes a range of runbook runs.")] + public class DeleteRunbookRunsCommand : RunbookRunCommandBase, ISupportFormattedOutput + { + List toDelete = new List(); + List wouldDelete = new List(); + DateTime minDate = new DateTime(1900, 1, 1); + DateTime maxDate = DateTime.MaxValue; + + bool whatIf = false; + + public DeleteRunbookRunsCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) + : base(repositoryFactory, fileSystem, clientFactory, commandOutputProvider) + { + var options = Options.For("Deletion"); + options.Add("minCreateDate=", "[Optional] Earliest (inclusive) create date for the range of runbook runs to delete.", v => minDate = v); + options.Add("maxCreateDate=", "[Optional] Latest (inclusive) create date for the range of runbooks to delete.", v => maxDate = v); + options.Add("whatIf", "[Optional, Flag] if specified, releases won't actually be deleted, but will be listed as if simulating the command.", v => whatIf = true); + } + + public override async Task Request() + { + await base.Request(); + commandOutputProvider.Debug("Finding runbook runs..."); + + await Repository.RunbookRuns + .Paginate(projectsFilter, + runbooksFilter, + environmentsFilter, + tenantsFilter, + page => { + foreach(var run in page.Items) { + if(run.Created >= minDate && run.Created <= maxDate) { + if(whatIf) { + commandOutputProvider.Information("[WhatIf] Run {RunId:l} would have been deleted", run.Id); + wouldDelete.Add(run); + } else { + toDelete.Add(run); + commandOutputProvider.Information("Deleting Run {RunId:l}", run.Id); + } + } + } + return true; // We need to check all runs + }) + .ConfigureAwait(false); + + // Don't do anything else for WhatIf + if (whatIf) return; + + foreach (var run in toDelete) + await Repository.Client.Delete(run.Link("Self")).ConfigureAwait(false); + } + + public void PrintDefaultOutput() + { + } + + public void PrintJsonOutput() + { + var affectedRuns = whatIf ? wouldDelete : toDelete; + commandOutputProvider.Json( + affectedRuns.Select(run => new { + Id = run.Id, + Name = run.Name, + Created = run.Created, + Project = new { Id = run.ProjectId, Name = projectsById[run.ProjectId].Name }, + Runbook = new { Id = run.RunbookId, Name = runbooksById[run.RunbookId].Name }, + Environment = new { Id = run.EnvironmentId, Name = environmentsById[run.EnvironmentId].Name }, + Tenant = new { Id = run.TenantId, Name = !string.IsNullOrEmpty(run.TenantId) ? tenantsById[run.TenantId].Name : null }, + FailureEncountered = run.FailureEncountered, + Comments = run.Comments + })); + } + } +} diff --git a/source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs b/source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs new file mode 100644 index 0000000000..7017a61cde --- /dev/null +++ b/source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Octopus.Cli.Model; +using Octopus.Cli.Repositories; +using Octopus.Cli.Util; +using Octopus.Client; +using Octopus.Client.Model; +using Octopus.CommandLine; +using Octopus.CommandLine.Commands; + +namespace Octopus.Cli.Commands.RunbooksRun +{ + [Command("list-runbookruns", Description = "Lists a runbook runs by project and/or runbook")] + public class ListRunbookRunsCommand : RunbookRunCommandBase, ISupportFormattedOutput + { + const int DefaultReturnAmount = 30; + int? numberOfResults; + + List runbookRuns = new List(); + + public ListRunbookRunsCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) + : base(repositoryFactory, fileSystem, clientFactory, commandOutputProvider) + { + var options = Options.For("Listing"); + options.Add("number=", $"[Optional] number of results to return, default is {DefaultReturnAmount}", v => numberOfResults = v); + } + + public override async Task Request() + { + await base.Request(); + + commandOutputProvider.Debug("Loading runbook runs..."); + + var maxResults = numberOfResults ?? DefaultReturnAmount; + await Repository.RunbookRuns + .Paginate(projectsFilter, + runbooksFilter, + environmentsFilter, + tenantsFilter, + delegate(ResourceCollection page) + { + if (runbookRuns.Count < maxResults) + foreach (var dr in page.Items.Take(maxResults - runbookRuns.Count)) + runbookRuns.Add(dr); + + return true; + }) + .ConfigureAwait(false); + } + + public void PrintDefaultOutput() + { + if (!runbookRuns.Any()) + commandOutputProvider.Information("Did not find any runbook runs matching the search criteria."); + + commandOutputProvider.Debug($"Showing {runbookRuns.Count} results..."); + + foreach(var item in runbookRuns) { + LogrunbookRunInfo(commandOutputProvider, item); + } + + if (numberOfResults.HasValue && numberOfResults != runbookRuns.Count) + commandOutputProvider.Debug($"Please note you asked for {numberOfResults} results, but there were only {runbookRuns.Count} that matched your criteria"); + } + + public void PrintJsonOutput() + { + commandOutputProvider.Json( + runbookRuns.Select(run => new + { + Id = run.Id, + Name = run.Name, + Created = run.Created, + Project = new { Id = run.ProjectId, Name = projectsById[run.ProjectId].Name}, + Runbook = new { Id = run.RunbookId, Name = runbooksById[run.RunbookId].Name}, + Environment = new { Id = run.EnvironmentId, Name = environmentsById[run.EnvironmentId].Name}, + Tenant = new { Id = run.TenantId, Name = !string.IsNullOrEmpty(run.TenantId) ? tenantsById[run.TenantId].Name : null }, + FailureEncountered = run.FailureEncountered, + Comments = run.Comments + })); + } + + private void LogrunbookRunInfo(ICommandOutputProvider outputProvider, + RunbookRunResource runbookRunItem) + { + var nameOfrunbookRunEnvironment = environmentsById[runbookRunItem.EnvironmentId].Name; + var nameOfrunbookRunProject = projectsById[runbookRunItem.ProjectId].Name; + var nameOfrunbook = runbooksById[runbookRunItem.RunbookId].Name; + + outputProvider.Information(" - Id: {Name:1}", runbookRunItem.Id); + outputProvider.Information(" - Name: {Name:1}", runbookRunItem.Name); + outputProvider.Information(" - Project: {Project:l}", nameOfrunbookRunProject); + outputProvider.Information(" - Runbook: {Runbook:l}", nameOfrunbook); + outputProvider.Information(" - Environment: {Environment:l}", nameOfrunbookRunEnvironment); + + if (!string.IsNullOrEmpty(runbookRunItem.TenantId)) + { + var nameOfrunbookRunTenant = tenantsById[runbookRunItem.TenantId].Name; + outputProvider.Information(" - Tenant: {Tenant:l}", nameOfrunbookRunTenant); + } + + outputProvider.Information("\tCreated: {$Date:l}", runbookRunItem.Created); + if (!string.IsNullOrWhiteSpace(runbookRunItem.Comments)) outputProvider.Information("\tComments: {$Comments:l}", runbookRunItem.Comments); + outputProvider.Information("\tFaulure Encountered: {FailureEncountered:l}", runbookRunItem.FailureEncountered ? "Yes" : "No"); + + outputProvider.Information(string.Empty); + } + } +} diff --git a/source/Octopus.Cli/Commands/RunbookRun/RunbookRunCommandBase.cs b/source/Octopus.Cli/Commands/RunbookRun/RunbookRunCommandBase.cs new file mode 100644 index 0000000000..1f613cd26a --- /dev/null +++ b/source/Octopus.Cli/Commands/RunbookRun/RunbookRunCommandBase.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Octopus.Cli.Commands.Runbooks; +using Octopus.Cli.Repositories; +using Octopus.Cli.Util; +using Octopus.Client; +using Octopus.Client.Model; +using Octopus.CommandLine; +using Octopus.CommandLine.Commands; + +namespace Octopus.Cli.Commands.RunbooksRun { + + /// + /// Base class for Runbook related commands with shared logic for all Runbook (and RunbookRun) commands + /// + public abstract class RunbookRunCommandBase : RunbookCommandBase { + protected readonly HashSet environments = new HashSet(StringComparer.OrdinalIgnoreCase); + protected readonly HashSet runbooks = new HashSet(StringComparer.OrdinalIgnoreCase); + protected readonly HashSet tenants = new HashSet(StringComparer.OrdinalIgnoreCase); + protected IDictionary runbooksById; + protected IDictionary environmentsById; + protected IDictionary tenantsById; + + protected string[] runbooksFilter; + protected string[] environmentsFilter; + protected string[] tenantsFilter; + + public RunbookRunCommandBase(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) + : base(repositoryFactory, fileSystem, clientFactory, commandOutputProvider) { + var options = Options.For("Listing"); + options.Add("runbook=", "[Optional] Name of a runbook to filter by. Can be specified many times.", v => runbooks.Add(v), allowsMultiple: true); + options.Add("environment=", "[Optional] Name of an environment to filter by. Can be specified many times.", v => environments.Add(v), allowsMultiple: true); + options.Add("tenant=", "[Optional] Name of a tenant to filter by. Can be specified many times.", v => tenants.Add(v), allowsMultiple: true); + } + + public override async Task Request() { + // Need to run the base implementation first to resolve the projects, which is required by Runbook query. + await base.Request(); + + environmentsById = await LoadEnvironments().ConfigureAwait(false); + environmentsFilter = environmentsById.Any() ? environmentsById.Keys.ToArray() : new string[0]; + runbooksById = await LoadRunbooks().ConfigureAwait(false); + runbooksFilter = runbooksById.Any() ? runbooksById.Keys.ToArray() : new string[0]; + tenantsById = await LoadTenants().ConfigureAwait(false); + tenantsFilter = tenants.Any() ? tenantsById.Keys.ToArray() : new string[0]; + } + + private async Task> LoadEnvironments() { + commandOutputProvider.Information("Loading environments..."); + var environmentQuery = environments.Any() + ? Repository.Environments.FindByNames(environments.ToArray()) + : Repository.Environments.FindAll(); + + var environmentResources = await environmentQuery.ConfigureAwait(false); + + var missingEnvironments = + environments.Except(environmentResources.Select(e => e.Name), StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if(missingEnvironments.Any()) + throw new CommandException("Could not find environments: " + string.Join(",", missingEnvironments)); + + return environmentResources.ToDictionary(e => e.Id, e => e); + } + + private async Task> LoadRunbooks() { + commandOutputProvider.Information("Loading runbooks..."); + + Task> runbookQuery; + + if(projectsFilter.Any()) { + runbookQuery = runbooks.Any() + ? Repository.Runbooks.FindMany(rb => projectsFilter.Contains(rb.ProjectId) && runbooks.ToArray().Contains(rb.Name)) + : Repository.Runbooks.FindMany(rb => projectsFilter.Contains(rb.ProjectId)); + } else { + runbookQuery = runbooks.Any() + ? Repository.Runbooks.FindByNames(runbooks.ToArray()) + : Repository.Runbooks.FindAll(); + } + + var runbookResources = await runbookQuery.ConfigureAwait(false); + + var missingRunbooks = + runbooks.Except(runbookResources.Select(e => e.Name), StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if(missingRunbooks.Any()) + throw new CommandException("Could not find runbooks: " + string.Join(",", missingRunbooks)); + + return runbookResources.ToDictionary(rb => rb.Id, rb => rb); + } + + private async Task> LoadTenants() { + commandOutputProvider.Information("Loading tenants..."); + + var multiTenancyStatus = await Repository.Tenants.Status().ConfigureAwait(false); + + if(multiTenancyStatus.Enabled) { + var tenantsQuery = tenants.Any() + ? Repository.Tenants.FindByNames(tenants.ToArray()) + : Repository.Tenants.FindAll(); + + var tenantsResources = await tenantsQuery.ConfigureAwait(false); + + var missingTenants = tenants.Except(tenantsResources.Select(e => e.Name), StringComparer.OrdinalIgnoreCase).ToArray(); + + if(missingTenants.Any()) + throw new CommandException("Could not find tenants: " + string.Join(",", missingTenants)); + + return tenantsResources.ToDictionary(t => t.Id, t => t); + } else { + return new Dictionary(); + } + } + } +} diff --git a/source/Octopus.Cli/Commands/Runbooks/ListRunbooksCommand.cs b/source/Octopus.Cli/Commands/Runbooks/ListRunbooksCommand.cs new file mode 100644 index 0000000000..a6be17a05d --- /dev/null +++ b/source/Octopus.Cli/Commands/Runbooks/ListRunbooksCommand.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Octopus.Cli.Repositories; +using Octopus.Cli.Util; +using Octopus.Client; +using Octopus.Client.Model; +using Octopus.CommandLine; +using Octopus.CommandLine.Commands; + +namespace Octopus.Cli.Commands.Runbooks +{ + [Command("list-runbooks", Description = "Lists runbooks by project.")] + public class ListRunbooksCommand : RunbookCommandBase, ISupportFormattedOutput + { + List runbooks; + + public ListRunbooksCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) + : base(repositoryFactory, fileSystem, clientFactory, commandOutputProvider) + { + } + + public override async Task Request() + { + await base.Request(); + + commandOutputProvider.Debug("Loading runbooks..."); + + runbooks = await Repository.Runbooks + .FindMany(x => projectsFilter.Contains(x.ProjectId)) + .ConfigureAwait(false); + } + + public void PrintDefaultOutput() + { + commandOutputProvider.Information("Runbooks: {Count}", runbooks.Count); + foreach (var project in projectsById.Values) + { + commandOutputProvider.Information(" - Project: {Project:l}", project.Name); + + foreach (var runbook in runbooks.Where(x => x.ProjectId == project.Id)) + { + var propertiesToLog = new List(); + propertiesToLog.AddRange(FormatRunbookPropertiesAsStrings(runbook)); + foreach (var property in propertiesToLog) + commandOutputProvider.Information(" {Property:l}", property); + commandOutputProvider.Information(""); + } + } + } + + public void PrintJsonOutput() + { + commandOutputProvider.Json(projectsById.Values.Select(pr => new + { + Project = new { pr.Id, pr.Name }, + Ruunbooks = runbooks.Where(r => r.ProjectId == pr.Id) + .Select(r => new + { + r.Id, + r.Name, + r.Description + }) + })); + } + } +} diff --git a/source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs b/source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs new file mode 100644 index 0000000000..2c6f68890a --- /dev/null +++ b/source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Octopus.Cli.Repositories; +using Octopus.Cli.Util; +using Octopus.Client; +using Octopus.Client.Model; +using Octopus.CommandLine; +using Octopus.CommandLine.Commands; + +namespace Octopus.Cli.Commands.Runbooks { + + /// + /// Base class for Runbook related commands with shared logic for all Runbook (and RunbookRun) commands + /// + public abstract class RunbookCommandBase : ApiCommand { + protected readonly HashSet projects = new HashSet(StringComparer.OrdinalIgnoreCase); + protected IDictionary projectsById; + protected string[] projectsFilter; + + public RunbookCommandBase(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) + : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) { + var options = Options.For("Listing"); + options.Add("project=", "[Optional] Name of a project to filter by. Can be specified many times.", v => projects.Add(v), allowsMultiple: true); + } + + public virtual async Task Request() { + projectsById = await LoadProjects().ConfigureAwait(false); + projectsFilter = projectsById.Any() ? projectsById.Keys.ToArray() : new string[0]; + } + + protected IEnumerable FormatRunbookPropertiesAsStrings(RunbookResource runbook) { + return new string[] + { + "Id: " + runbook.Id, + "Name: " + runbook.Name, + "Description: " + runbook.Description, + }; + } + + private async Task> LoadProjects() { + commandOutputProvider.Information("Loading projects..."); + var projectQuery = projects.Any() + ? Repository.Projects.FindByNames(projects.ToArray()) + : Repository.Projects.FindAll(); + + var projectResources = await projectQuery.ConfigureAwait(false); + + var missingProjects = projects.Except(projectResources.Select(e => e.Name), StringComparer.OrdinalIgnoreCase).ToArray(); + + if(missingProjects.Any()) + throw new CommandException("Could not find projects: " + string.Join(",", missingProjects)); + + return projectResources.ToDictionary(p => p.Id, p => p); + } + } +} From de9b2f4ffe1389bbf6f8cb1e22335402db8c0fc6 Mon Sep 17 00:00:00 2001 From: Mark Raming Date: Tue, 27 Sep 2022 15:06:29 +0200 Subject: [PATCH 2/4] --ProjectName parameter made required for List-Runbooks, List-RunbookRuns and Delete-RunbookRuns commands --- .../Commands/Runbooks/RunbookCommandBase.cs | 14 +++--- source/OctopusCli.sln | 43 +++++++++++-------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs b/source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs index 2c6f68890a..d20c072f3c 100644 --- a/source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs +++ b/source/Octopus.Cli/Commands/Runbooks/RunbookCommandBase.cs @@ -23,7 +23,14 @@ public abstract class RunbookCommandBase : ApiCommand { public RunbookCommandBase(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider) : base(clientFactory, repositoryFactory, fileSystem, commandOutputProvider) { var options = Options.For("Listing"); - options.Add("project=", "[Optional] Name of a project to filter by. Can be specified many times.", v => projects.Add(v), allowsMultiple: true); + options.Add("project=", "Name of a project to filter by. Can be specified many times.", v => projects.Add(v), allowsMultiple: true); + } + + protected override Task ValidateParameters() { + if(!projects.Any(p => !string.IsNullOrWhiteSpace(p))) + throw new CommandException("Please specify at least one project name or ID using the parameter: --project=XYZ"); + + return base.ValidateParameters(); } public virtual async Task Request() { @@ -42,11 +49,8 @@ protected IEnumerable FormatRunbookPropertiesAsStrings(RunbookResource r private async Task> LoadProjects() { commandOutputProvider.Information("Loading projects..."); - var projectQuery = projects.Any() - ? Repository.Projects.FindByNames(projects.ToArray()) - : Repository.Projects.FindAll(); - var projectResources = await projectQuery.ConfigureAwait(false); + var projectResources = await Repository.Projects.FindByNames(projects.ToArray()).ConfigureAwait(false); var missingProjects = projects.Except(projectResources.Select(e => e.Name), StringComparer.OrdinalIgnoreCase).ToArray(); diff --git a/source/OctopusCli.sln b/source/OctopusCli.sln index d4567f7445..7a31a566f3 100644 --- a/source/OctopusCli.sln +++ b/source/OctopusCli.sln @@ -1,15 +1,16 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28803.202 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32526.322 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{193F466A-BECE-4809-A369-7CC061C31C82}" ProjectSection(SolutionItems) = preProject - NuGet.Config = NuGet.Config - ..\readme.md = ..\readme.md ..\.gitignore = ..\.gitignore + ..\.github\workflows\build.yml = ..\.github\workflows\build.yml ..\BuildAssets\create-octopuscli-linux-packages.sh = ..\BuildAssets\create-octopuscli-linux-packages.sh + NuGet.Config = NuGet.Config + ..\readme.md = ..\readme.md ..\BuildAssets\test-linux-package.sh = ..\BuildAssets\test-linux-package.sh - ..\.github\workflows\build.yml = ..\.github\workflows\build.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Octo", "Octo\Octo.csproj", "{D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}" @@ -21,14 +22,14 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Octopus.Cli", "Octopus.Cli\Octopus.Cli.csproj", "{627462FB-2E70-4F9C-ACC9-8CF84BF6C196}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{994AA337-0E3B-44C3-83F8-0E6C5D3AA007}" -ProjectSection(SolutionItems) = preProject - ..\Dockerfiles\Readme.md = ..\Dockerfiles\Readme.md -EndProjectSection + ProjectSection(SolutionItems) = preProject + ..\Dockerfiles\Readme.md = ..\Dockerfiles\Readme.md + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Alpine", "Alpine", "{32A3C564-207A-43DB-9588-27AFAF846C1A}" -ProjectSection(SolutionItems) = preProject - ..\Dockerfiles\alpine\Dockerfile = ..\Dockerfiles\alpine\Dockerfile -EndProjectSection + ProjectSection(SolutionItems) = preProject + ..\Dockerfiles\alpine\Dockerfile = ..\Dockerfiles\alpine\Dockerfile + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "..\build\_build.csproj", "{0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}" EndProject @@ -42,10 +43,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1BFD88F-FB8E-49EC-968F-F4D9A8A52519}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -92,14 +89,22 @@ Global {627462FB-2E70-4F9C-ACC9-8CF84BF6C196}.Release|Mixed Platforms.Build.0 = Release|Any CPU {627462FB-2E70-4F9C-ACC9-8CF84BF6C196}.Release|x86.ActiveCfg = Release|Any CPU {627462FB-2E70-4F9C-ACC9-8CF84BF6C196}.Release|x86.Build.0 = Release|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Debug|x86.Build.0 = Debug|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|x86.ActiveCfg = Release|Any CPU + {0B7F1B18-1DF1-4B74-A4D4-BED7FC82343E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {FD0E320F-79AC-4489-9508-C62497C629D3} - EndGlobalSection GlobalSection(NestedProjects) = preSolution {32A3C564-207A-43DB-9588-27AFAF846C1A} = {994AA337-0E3B-44C3-83F8-0E6C5D3AA007} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FD0E320F-79AC-4489-9508-C62497C629D3} + EndGlobalSection EndGlobal From b08c73a4b5986339b2931328fbb37a46803f876d Mon Sep 17 00:00:00 2001 From: Mark Raming Date: Tue, 27 Sep 2022 16:47:56 +0200 Subject: [PATCH 3/4] Fixed type in text output of List-RunbookRuns --- .../Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs b/source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs index 7017a61cde..c2780dd112 100644 --- a/source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs +++ b/source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs @@ -103,7 +103,7 @@ private void LogrunbookRunInfo(ICommandOutputProvider outputProvider, outputProvider.Information("\tCreated: {$Date:l}", runbookRunItem.Created); if (!string.IsNullOrWhiteSpace(runbookRunItem.Comments)) outputProvider.Information("\tComments: {$Comments:l}", runbookRunItem.Comments); - outputProvider.Information("\tFaulure Encountered: {FailureEncountered:l}", runbookRunItem.FailureEncountered ? "Yes" : "No"); + outputProvider.Information("\tFailure Encountered: {FailureEncountered:l}", runbookRunItem.FailureEncountered ? "Yes" : "No"); outputProvider.Information(string.Empty); } From e12b5065ce098d2a4506148b662588d2a8f57557 Mon Sep 17 00:00:00 2001 From: Mark Raming Date: Tue, 27 Sep 2022 17:31:18 +0200 Subject: [PATCH 4/4] Delete-RunbookRuns now requires minCreateDate and maxCreateDate arguments to prevent accidental deletion of all runs. Refactored actual delete command for runbook runs. --- .../RunbookRun/DeleteRunbookRunsCommand.cs | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs b/source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs index 82b6ec68d6..2e5db108ac 100644 --- a/source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs +++ b/source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs @@ -2,24 +2,22 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Octopus.Cli.Infrastructure; + using Octopus.Cli.Repositories; using Octopus.Cli.Util; using Octopus.Client; using Octopus.Client.Model; using Octopus.CommandLine; using Octopus.CommandLine.Commands; -using Octopus.Versioning.Octopus; -namespace Octopus.Cli.Commands.RunbooksRun -{ +namespace Octopus.Cli.Commands.RunbooksRun { [Command("delete-runbookruns", Description = "Deletes a range of runbook runs.")] public class DeleteRunbookRunsCommand : RunbookRunCommandBase, ISupportFormattedOutput { List toDelete = new List(); List wouldDelete = new List(); - DateTime minDate = new DateTime(1900, 1, 1); - DateTime maxDate = DateTime.MaxValue; + DateTime? minDate = null; + DateTime? maxDate = null; bool whatIf = false; @@ -27,15 +25,24 @@ public DeleteRunbookRunsCommand(IOctopusAsyncRepositoryFactory repositoryFactory : base(repositoryFactory, fileSystem, clientFactory, commandOutputProvider) { var options = Options.For("Deletion"); - options.Add("minCreateDate=", "[Optional] Earliest (inclusive) create date for the range of runbook runs to delete.", v => minDate = v); - options.Add("maxCreateDate=", "[Optional] Latest (inclusive) create date for the range of runbooks to delete.", v => maxDate = v); + options.Add("minCreateDate=", "Earliest (inclusive) create date for the range of runbook runs to delete.", v => minDate = v); + options.Add("maxCreateDate=", "Latest (inclusive) create date for the range of runbooks to delete.", v => maxDate = v); options.Add("whatIf", "[Optional, Flag] if specified, releases won't actually be deleted, but will be listed as if simulating the command.", v => whatIf = true); } + protected override Task ValidateParameters() { + if(!minDate.HasValue) + throw new CommandException("Please specify the earliest (inclusive) create date for the range of runbook runs to delete using the parameter: --minCreateData=2022-01-01"); + if(!maxDate.HasValue) + throw new CommandException("Please specify the latest (inclusive) create date for the range of runbooks to delete using the parameter: --maxCreateDate=2022-01-01"); + + return base.ValidateParameters(); + } + public override async Task Request() { await base.Request(); - commandOutputProvider.Debug("Finding runbook runs..."); + commandOutputProvider.Debug($"Finding runbook runs created between {minDate:yyyy-mm-dd} and {maxDate:yyyy-mm-dd} ..."); await Repository.RunbookRuns .Paginate(projectsFilter, @@ -44,13 +51,13 @@ await Repository.RunbookRuns tenantsFilter, page => { foreach(var run in page.Items) { - if(run.Created >= minDate && run.Created <= maxDate) { + if(run.Created >= minDate.Value && run.Created <= maxDate.Value) { if(whatIf) { - commandOutputProvider.Information("[WhatIf] Run {RunId:l} would have been deleted", run.Id); + commandOutputProvider.Information("[WhatIf] Run {RunId:l} created on {CreatedOn:2} would have been deleted", run.Id, run.Created.ToString()); wouldDelete.Add(run); } else { toDelete.Add(run); - commandOutputProvider.Information("Deleting Run {RunId:l}", run.Id); + commandOutputProvider.Information("Deleting Run {RunId:l} created on {CreatedOn:2}", run.Id, run.Created.ToString()); } } } @@ -61,8 +68,8 @@ await Repository.RunbookRuns // Don't do anything else for WhatIf if (whatIf) return; - foreach (var run in toDelete) - await Repository.Client.Delete(run.Link("Self")).ConfigureAwait(false); + foreach(var run in toDelete) + await Repository.RunbookRuns.Delete(run); } public void PrintDefaultOutput()