Skip to content

Commit

Permalink
Compose Builds (#167)
Browse files Browse the repository at this point in the history
* WIP - Compose Builds...

* Only implement compose builds for dockerfiles
  • Loading branch information
prom3theu5 authored May 1, 2024
1 parent 5521b28 commit b63b902
Show file tree
Hide file tree
Showing 23 changed files with 314 additions and 162 deletions.
4 changes: 2 additions & 2 deletions src/Aspirate.Cli/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
},
"generate": {
"commandName": "Project",
"commandLineArgs": "generate",
"workingDirectory": "$(ProjectDir)/../../../aspire/samples/eShopLite/AppHost",
"commandLineArgs": "generate --output-format compose --compose-build ham --compose-build cheese --non-interactive --include-dashboard=false",
"workingDirectory": "/Users/prom3theu5/git/test-compose/AspireSample/AspireSample.AppHost",
"hotReloadEnabled": false
},
"generate-non-interactive": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using Aspirate.Processors.Resources.Dockerfile;
using Aspirate.Services.Parameters;

namespace Aspirate.Commands.Actions.Containers;

public sealed class BuildAndPushContainersFromDockerfilesAction(
IServiceProvider serviceProvider) : BaseAction(serviceProvider)
IServiceProvider serviceProvider) : BaseActionWithNonInteractiveValidation(serviceProvider)
{
public override async Task<bool> ExecuteAsync()
{
Expand All @@ -15,14 +12,27 @@ public override async Task<bool> ExecuteAsync()
return true;
}

if (CurrentState.ComposeBuilds?.Any() == false)
{
SelectComposeItemsToIncludeAsComposeBuilds();
}

if (CurrentState.ComposeBuilds?.Any() == true)
{
Logger.MarkupLine("[bold]Compose builds selected:[/]");
foreach (var composeBuild in CurrentState.ComposeBuilds)
{
Logger.MarkupLine($"[blue] - {composeBuild}[/]");
}
}

var dockerfileProcessor = Services.GetRequiredKeyedService<IResourceProcessor>(AspireComponentLiterals.Dockerfile) as DockerfileProcessor;

CacheContainerDetails(dockerfileProcessor);

if (CurrentState.SkipBuild)
{
var rule = new Rule("[blue][bold]Skipping build and push action as requested.[/][/]");
AnsiConsole.Write(rule);
Logger.MarkupLine("[bold]Skipping build and push action as requested.[/]");
return true;
}

Expand All @@ -33,9 +43,7 @@ public override async Task<bool> ExecuteAsync()

private void CacheContainerDetails(DockerfileProcessor? dockerfileProcessor)
{
Logger.MarkupLine("[bold]Building all dockerfile resources, and pushing containers[/]");

foreach (var resource in CurrentState.SelectedDockerfileComponents)
foreach (var resource in CurrentState.SelectedDockerfileComponents.Where(resource => CurrentState.ComposeBuilds?.Contains(resource.Key) != true))
{
dockerfileProcessor.PopulateContainerImageCacheWithImage(resource, new()
{
Expand All @@ -44,15 +52,58 @@ private void CacheContainerDetails(DockerfileProcessor? dockerfileProcessor)
Tag = CurrentState.ContainerImageTag,
});
}
}

private void SelectComposeItemsToIncludeAsComposeBuilds()
{
if (CurrentState.NonInteractive)
{
return;
}

Logger.MarkupLine("[bold]Building and push completed for all selected dockerfile components.[/]");
var dockerFileEntries = CurrentState.LoadedAspireManifestResources.Where(x => x.Value is DockerfileResource)
.Select(x => x.Key).ToList();

var selectedEntries = Logger.Prompt(
new MultiSelectionPrompt<string>()
.Title(
"Select [green]Dockerfiles[/] to include as compose built images (The compose file is responsible for building the image)")
.PageSize(10)
.Required(false)
.MoreChoicesText("[grey](Move up and down to reveal more components)[/]")
.InstructionsText(
"[grey](Press [blue]<space>[/] to toggle a component, " +
"[green]<enter>[/] to accept)[/]")
.AddChoiceGroup("Select all", dockerFileEntries))
.ToArray();

ProcessSelectedComponents(selectedEntries);
}

private async Task PerformBuildAndPushes(DockerfileProcessor? dockerfileProcessor)
private void ProcessSelectedComponents(string[] selectedEntries)
{
Logger.MarkupLine("[bold]Building all dockerfile resources, and pushing containers:[/]");
if (selectedEntries.Length == 0)
{
CurrentState.ComposeBuilds = null;
return;
}

CurrentState.ComposeBuilds ??= [];

foreach (var resource in CurrentState.SelectedDockerfileComponents)
foreach (var selectedEntry in selectedEntries)
{
if (CurrentState.ComposeBuilds.Contains(selectedEntry))
{
continue;
}

CurrentState.ComposeBuilds.Add(selectedEntry);
}
}

private async Task PerformBuildAndPushes(DockerfileProcessor? dockerfileProcessor)
{
foreach (var resource in CurrentState.SelectedDockerfileComponents.Where(resource => CurrentState.ComposeBuilds?.Contains(resource.Key) != true))
{
await dockerfileProcessor.BuildAndPushContainerForDockerfile(resource, new()
{
Expand All @@ -63,8 +114,6 @@ private async Task PerformBuildAndPushes(DockerfileProcessor? dockerfileProcesso
Tag = CurrentState.ContainerImageTag
}, CurrentState.NonInteractive);
}

Logger.MarkupLine("[bold]Building and push completed for all selected dockerfile components.[/]");
}

private bool HasSelectedDockerfileComponents()
Expand All @@ -77,4 +126,23 @@ private bool HasSelectedDockerfileComponents()
Logger.MarkupLine("[bold]No Dockerfile components selected. Skipping build and publish action.[/]");
return false;
}

public override void ValidateNonInteractiveState()
{
if (!CurrentState.NonInteractive || !HasSelectedDockerfileComponents())
{
return;
}

if (CurrentState.ComposeBuilds?.Any() == true)
{
foreach (var composeBuild in CurrentState.ComposeBuilds)
{
if (!CurrentState.LoadedAspireManifestResources.ContainsKey(composeBuild))
{
NonInteractiveValidationFailed($"The resource '{composeBuild}' is not found in the loaded manifest.");
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,12 @@ private void ProcessIndividualComponent(KeyValuePair<string, Resource> resource,
return;
}

var response = handler.CreateComposeEntry(resource, CurrentState.IncludeDashboard);
var response = handler.CreateComposeEntry(new()
{
Resource = resource,
WithDashboard = CurrentState.IncludeDashboard,
ComposeBuilds = CurrentState.ComposeBuilds?.Any(x=> x == resource.Key) ?? false,
});

if (response.IsProject)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ private async Task ProcessIndividualResourceManifests(KeyValuePair<string, Resou
return;
}

var success = await handler.CreateManifests(resource, CurrentState.OutputPath, CurrentState.ImagePullPolicy, CurrentState.TemplatePath, CurrentState.DisableSecrets, CurrentState.WithPrivateRegistry, CurrentState.IncludeDashboard);
var success = await handler.CreateManifests(new()
{
Resource = resource,
OutputPath = CurrentState.OutputPath,
ImagePullPolicy = CurrentState.ImagePullPolicy,
TemplatePath = CurrentState.TemplatePath,
DisableSecrets = CurrentState.DisableSecrets,
WithPrivateRegistry = CurrentState.WithPrivateRegistry,
WithDashboard = CurrentState.IncludeDashboard
});

if (success && !AspirateState.IsNotDeployable(resource.Value) && resource.Value is not DaprComponentResource)
{
Expand Down
3 changes: 3 additions & 0 deletions src/Aspirate.Commands/AspirateState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public class AspirateState :
[JsonPropertyName("runtimeIdentifier")]
public string? RuntimeIdentifier { get; set; }

[JsonPropertyName("composeBuilds")]
public List<string>? ComposeBuilds { get; set; }

[JsonPropertyName("imagePullPolicy")]
public string? ImagePullPolicy { get; set; }

Expand Down
1 change: 1 addition & 0 deletions src/Aspirate.Commands/Commands/Build/BuildOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ public sealed class BuildOptions : BaseCommandOptions, IBuildOptions, IContainer

public string? ContainerImageTag { get; set; }
public string? RuntimeIdentifier { get; set; }
public List<string>? ComposeBuilds { get; set; }
}
1 change: 1 addition & 0 deletions src/Aspirate.Commands/Commands/Generate/GenerateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ public GenerateCommand() : base("generate", "Builds, pushes containers, generate
AddOption(PrivateRegistryPasswordOption.Instance);
AddOption(PrivateRegistryEmailOption.Instance);
AddOption(IncludeDashboardOption.Instance);
AddOption(ComposeBuildsOption.Instance);
}
}
1 change: 1 addition & 0 deletions src/Aspirate.Commands/Commands/Generate/GenerateOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public sealed class GenerateOptions : BaseCommandOptions,
public string? OutputFormat { get; set; }

public string? RuntimeIdentifier { get; set; }
public List<string>? ComposeBuilds { get; set; }

public string? SecretPassword { get; set; }

Expand Down
1 change: 1 addition & 0 deletions src/Aspirate.Commands/Contracts/IBuildOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ namespace Aspirate.Commands.Contracts;
public interface IBuildOptions
{
string? RuntimeIdentifier { get; set; }
List<string>? ComposeBuilds { get; set; }
}
1 change: 1 addition & 0 deletions src/Aspirate.Commands/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
global using Aspirate.DockerCompose.Extensions;
global using Aspirate.DockerCompose.Models;
global using Aspirate.DockerCompose.Models.Services;
global using Aspirate.Processors.Resources.Dockerfile;
global using Aspirate.Processors.Transformation;
global using Aspirate.Secrets.Extensions;
global using Aspirate.Secrets.Literals;
Expand Down
16 changes: 16 additions & 0 deletions src/Aspirate.Commands/Options/ComposeBuildsOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Aspirate.Commands.Options;

public sealed class ComposeBuildsOption : BaseOption<List<string>?>
{
private static readonly string[] _aliases = ["--compose-build"];

private ComposeBuildsOption() : base(_aliases, "ASPIRATE_COMPOSE_BUILDS", null)
{
Name = nameof(IBuildOptions.ComposeBuilds);
Description = "Specify the resource names which will be built by the compose file.";
Arity = ArgumentArity.ZeroOrMore;
IsRequired = false;
}

public static ComposeBuildsOption Instance { get; } = new();
}
27 changes: 4 additions & 23 deletions src/Aspirate.Processors/BaseResourceProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,35 +92,16 @@ protected Dictionary<string, string> GetSecretEnvironmentalVariables(Resource re
return envVars == null ? [] : envVars.Where(e => ProtectorType.List.Any(p => e.Key.StartsWith(p))).ToDictionary(e => e.Key, e => e.Value);
}

/// <summary>
/// Creates manifests for a resource.
/// </summary>
/// <param name="resource">The key-value pair representing the resource and its name.</param>
/// <param name="outputPath">The path where the manifests will be created.</param>
/// <param name="imagePullPolicy">The image pull policy for the resource.</param>
/// <param name="templatePath">Optional. The path to the template used for creating the manifests.</param>
/// <param name="disableSecrets">Passing this will disable all secret generation.</param>
/// <param name="withPrivateRegistry">Specifies if image pull secret should be set.</param>
/// <param name="withDashboard">Specifies if the dashboard OTLP endpoint should be included.</param>
/// <returns>A task that represents the asynchronous operation. The task result is a boolean indicating if the manifests were created successfully.</returns>
public virtual Task<bool> CreateManifests(KeyValuePair<string, Resource> resource, string outputPath, string imagePullPolicy,
string? templatePath = null,
bool? disableSecrets = false,
bool? withPrivateRegistry = false,
bool? withDashboard = false)
/// <inheritdoc />
public virtual Task<bool> CreateManifests(CreateManifestsOptions options)
{
LogCreateManifestNotOverridden(GetType().Name);

return Task.FromResult(false);
}

/// <summary>
/// Creates a compose entry with the given resource.
/// </summary>
/// <param name="resource">The key-value pair representing the resource.</param>
/// <param name="withDashboard">Should include the dashboard OTLP endpoint.</param>
/// <returns>The created compose entry service, or null if creation is not overridden.</returns>
public virtual ComposeService CreateComposeEntry(KeyValuePair<string, Resource> resource, bool? withDashboard = false)
/// <inheritdoc />
public virtual ComposeService CreateComposeEntry(CreateComposeEntryOptions options)
{
LogCreateComposeNotOverridden(GetType().Name);

Expand Down
1 change: 1 addition & 0 deletions src/Aspirate.Processors/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
global using System.Diagnostics.CodeAnalysis;
global using System.IO.Abstractions;
global using System.Text;
global using System.Text.Json;
global using System.Text.Json.Nodes;
global using System.Text.RegularExpressions;
Expand Down
Loading

0 comments on commit b63b902

Please sign in to comment.