Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New List-Runbooks, List-RunbookRuns and Delete-RunbookRuns commands #243

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions source/Octopus.Cli/Commands/RunbookRun/DeleteRunbookRunsCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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.RunbooksRun {
[Command("delete-runbookruns", Description = "Deletes a range of runbook runs.")]
public class DeleteRunbookRunsCommand : RunbookRunCommandBase, ISupportFormattedOutput
{
List<RunbookRunResource> toDelete = new List<RunbookRunResource>();
List<RunbookRunResource> wouldDelete = new List<RunbookRunResource>();
DateTime? minDate = null;
DateTime? maxDate = null;

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<DateTime>("minCreateDate=", "Earliest (inclusive) create date for the range of runbook runs to delete.", v => minDate = v);
options.Add<DateTime>("maxCreateDate=", "Latest (inclusive) create date for the range of runbooks to delete.", v => maxDate = v);
options.Add<bool>("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 created between {minDate:yyyy-mm-dd} and {maxDate:yyyy-mm-dd} ...");

await Repository.RunbookRuns
.Paginate(projectsFilter,
runbooksFilter,
environmentsFilter,
tenantsFilter,
page => {
foreach(var run in page.Items) {
if(run.Created >= minDate.Value && run.Created <= maxDate.Value) {
if(whatIf) {
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} created on {CreatedOn:2}", run.Id, run.Created.ToString());
}
}
}
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.RunbookRuns.Delete(run);
}

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
}));
}
}
}
111 changes: 111 additions & 0 deletions source/Octopus.Cli/Commands/RunbookRun/ListRunbookRunsCommand.cs
Original file line number Diff line number Diff line change
@@ -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<RunbookRunResource> runbookRuns = new List<RunbookRunResource>();

public ListRunbookRunsCommand(IOctopusAsyncRepositoryFactory repositoryFactory, IOctopusFileSystem fileSystem, IOctopusClientFactory clientFactory, ICommandOutputProvider commandOutputProvider)
: base(repositoryFactory, fileSystem, clientFactory, commandOutputProvider)
{
var options = Options.For("Listing");
options.Add<int>("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<RunbookRunResource> 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("\tFailure Encountered: {FailureEncountered:l}", runbookRunItem.FailureEncountered ? "Yes" : "No");

outputProvider.Information(string.Empty);
}
}
}
119 changes: 119 additions & 0 deletions source/Octopus.Cli/Commands/RunbookRun/RunbookRunCommandBase.cs
Original file line number Diff line number Diff line change
@@ -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 {

/// <summary>
/// Base class for Runbook related commands with shared logic for all Runbook (and RunbookRun) commands
/// </summary>
public abstract class RunbookRunCommandBase : RunbookCommandBase {
protected readonly HashSet<string> environments = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
protected readonly HashSet<string> runbooks = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
protected readonly HashSet<string> tenants = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
protected IDictionary<string, RunbookResource> runbooksById;
protected IDictionary<string, EnvironmentResource> environmentsById;
protected IDictionary<string, TenantResource> 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<string>("runbook=", "[Optional] Name of a runbook to filter by. Can be specified many times.", v => runbooks.Add(v), allowsMultiple: true);
options.Add<string>("environment=", "[Optional] Name of an environment to filter by. Can be specified many times.", v => environments.Add(v), allowsMultiple: true);
options.Add<string>("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<IDictionary<string, EnvironmentResource>> 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<IDictionary<string, RunbookResource>> LoadRunbooks() {
commandOutputProvider.Information("Loading runbooks...");

Task<List<RunbookResource>> 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<IDictionary<string, TenantResource>> 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<string, TenantResource>();
}
}
}
}
68 changes: 68 additions & 0 deletions source/Octopus.Cli/Commands/Runbooks/ListRunbooksCommand.cs
Original file line number Diff line number Diff line change
@@ -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<RunbookResource> 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<string>();
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
})
}));
}
}
}
Loading