Skip to content

Commit

Permalink
Allow specifying container tag multiple times - fixes #116 (#180)
Browse files Browse the repository at this point in the history
  • Loading branch information
prom3theu5 authored May 7, 2024
1 parent ad90fad commit 53d6bdc
Show file tree
Hide file tree
Showing 28 changed files with 116 additions and 79 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,7 +14,7 @@
},
"generate": {
"commandName": "Project",
"commandLineArgs": "generate --skip-build --output-format compose",
"commandLineArgs": "generate --output-format helm",
"workingDirectory": "/Users/prom3theu5/git/test-compose/AspireSample/AspireSample.AppHost",
"hotReloadEnabled": false
},
Expand All @@ -26,7 +26,7 @@
},
"build": {
"commandName": "Project",
"commandLineArgs": "build",
"commandLineArgs": "build -ct hamburger -ct fries -ct mountaindew -ct icecream -ct pizza -ct hotdog -ct taco -ct burrito -ct sandwich -ct salad -ct soup -ct sushi -ct ramen -ct curry",
"workingDirectory": "/Users/prom3theu5/git/test-compose/AspireSample/AspireSample.AppHost",
"hotReloadEnabled": false
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,10 @@ private void HandleContainerRepositoryPrefix(AspirateSettings aspirateConfigurat

private void HandleContainerTag(AspirateSettings aspirateConfiguration)
{
if (!string.IsNullOrEmpty(CurrentState.ContainerImageTag))
if (CurrentState.ContainerImageTags?.Count > 0)
{
aspirateConfiguration.ContainerSettings.Tag = CurrentState.ContainerImageTag;
Logger.MarkupLine($"[green]({EmojiLiterals.CheckMark}) Done:[/] Set [blue]'Container fallback tag'[/] to [blue]'{aspirateConfiguration.ContainerSettings.Tag}'[/].");
aspirateConfiguration.ContainerSettings.Tags = CurrentState.ContainerImageTags;
Logger.MarkupLine($"[green]({EmojiLiterals.CheckMark}) Done:[/] Set [blue]'Container fallback tag'[/] to [blue]'{string.Join(';', aspirateConfiguration.ContainerSettings.Tags)}'[/].");
return;
}

Expand All @@ -126,9 +126,9 @@ private void HandleContainerTag(AspirateSettings aspirateConfiguration)
return;
}

var containerTag = Logger.Prompt(new TextPrompt<string>("Please enter the container tag to use as a fall-back value:").PromptStyle("blue"));
aspirateConfiguration.ContainerSettings.Tag = containerTag;
Logger.MarkupLine($"[green]({EmojiLiterals.CheckMark}) Done:[/] Set [blue]'Container fallback tag'[/] to [blue]'{aspirateConfiguration.ContainerSettings.Tag}'[/].");
var containerTag = Logger.Prompt(new TextPrompt<string>("Please enter the container tags to use as a fall-back value, you can enter multiple values split via semi-colon ';' :").PromptStyle("blue"));
aspirateConfiguration.ContainerSettings.Tags = containerTag.Split(';').ToList();
Logger.MarkupLine($"[green]({EmojiLiterals.CheckMark}) Done:[/] Set [blue]'Container fallback tag'[/] to [blue]'{aspirateConfiguration.ContainerSettings.Tags}'[/].");
}

private void HandleTemplateDirectory(AspirateSettings aspirateConfiguration)
Expand Down Expand Up @@ -242,7 +242,7 @@ public override void ValidateNonInteractiveState()
Logger.ValidationFailed("Container Registry must be supplied when running in non-interactive mode.");
}

if (string.IsNullOrEmpty(CurrentState.ContainerImageTag))
if (CurrentState.ContainerImageTags?.Count == 0)
{
Logger.ValidationFailed("Container image tag must be supplied when running in non-interactive mode.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ public override Task<bool> ExecuteAsync()
CurrentState.ContainerRepositoryPrefix = aspirateSettings.ContainerSettings?.RepositoryPrefix ?? null;
}

if (string.IsNullOrEmpty(CurrentState.ContainerImageTag))
{
CurrentState.ContainerImageTag = aspirateSettings.ContainerSettings?.Tag ?? null;
}
CurrentState.ContainerImageTags ??= aspirateSettings.ContainerSettings?.Tags ?? null;

Logger.MarkupLine($"[bold]Successfully loaded existing aspirate bootstrap settings from [blue]'{CurrentState.ProjectPath}'[/].[/]");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private void CacheContainerDetails(DockerfileProcessor? dockerfileProcessor)
{
Registry = CurrentState.ContainerRegistry,
Prefix = CurrentState.ContainerRepositoryPrefix,
Tag = CurrentState.ContainerImageTag,
Tags = CurrentState.ContainerImageTags,
});
}
}
Expand Down Expand Up @@ -121,7 +121,7 @@ private async Task PerformBuildAndPushes(DockerfileProcessor? dockerfileProcesso
ImageName = resource.Key,
Registry = CurrentState.ContainerRegistry,
Prefix = CurrentState.ContainerRepositoryPrefix,
Tag = CurrentState.ContainerImageTag
Tags = CurrentState.ContainerImageTags
}, CurrentState.NonInteractive);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private async Task HandleProjects()
{
Registry = CurrentState.ContainerRegistry,
Prefix = CurrentState.ContainerRepositoryPrefix,
Tag = CurrentState.ContainerImageTag,
Tags = CurrentState.ContainerImageTags,
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspirate.Commands/Commands/Build/BuildOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public sealed class BuildOptions : BaseCommandOptions, IBuildOptions, IContainer

public string? ContainerRepositoryPrefix { get; set; }

public string? ContainerImageTag { get; set; }
public List<string>? ContainerImageTags { get; set; }
public string? RuntimeIdentifier { get; set; }
public List<string>? ComposeBuilds { get; set; }
}
3 changes: 1 addition & 2 deletions src/Aspirate.Commands/Commands/Generate/GenerateOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ public sealed class GenerateOptions : BaseCommandOptions,
public string? Namespace { get; set; }
public bool? SkipBuild { get; set; }
public bool? SkipFinalKustomizeGeneration { get; set; }
public bool? SkipHelmGeneration { get; set; }
public string? ContainerBuilder { get; set; }
public string? ContainerRegistry { get; set; }
public string? ContainerRepositoryPrefix { get; set; }
public string? ContainerImageTag { get; set; }
public List<string>? ContainerImageTags { get; set; }
public string? ImagePullPolicy { get; set; }
public string? OutputFormat { get; set; }
public string? RuntimeIdentifier { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/Aspirate.Commands/Commands/Init/InitOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public sealed class InitOptions : BaseCommandOptions, IInitOptions, IContainerOp

public string? ContainerRepositoryPrefix { get; set; }

public string? ContainerImageTag { get; set; }
public List<string>? ContainerImageTags { get; set; }
public string? TemplatePath { get; set; }
}
8 changes: 4 additions & 4 deletions src/Aspirate.Commands/Options/ContainerImageTagOption.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Aspirate.Commands.Options;

public sealed class ContainerImageTagOption : BaseOption<string?>
public sealed class ContainerImageTagOption : BaseOption<List<string>?>
{
private static readonly string[] _aliases =
{
Expand All @@ -10,9 +10,9 @@ public sealed class ContainerImageTagOption : BaseOption<string?>

private ContainerImageTagOption() : base(_aliases, "ASPIRATE_CONTAINER_IMAGE_TAG", null)
{
Name = nameof(IContainerOptions.ContainerImageTag);
Description = "The Container Image Tag to use as the fall-back value for all containers";
Arity = ArgumentArity.ExactlyOne;
Name = nameof(IContainerOptions.ContainerImageTags);
Description = "The Container Image Tags to use for all containers. Can include multiple times.";
Arity = ArgumentArity.ZeroOrMore;
IsRequired = false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class DockerfileProcessor(
$"{TemplateLiterals.ServiceType}.yaml",
];

private readonly Dictionary<string, string> _containerImageCache = [];
private readonly Dictionary<string, List<string>> _containerImageCache = [];

/// <inheritdoc />
public override Resource? Deserialize(ref Utf8JsonReader reader) =>
Expand All @@ -35,12 +35,12 @@ public override Task<bool> CreateManifests(CreateManifestsOptions options)

var dockerFile = options.Resource.Value as DockerfileResource;

if (!_containerImageCache.TryGetValue(options.Resource.Key, out var containerImage))
if (!_containerImageCache.TryGetValue(options.Resource.Key, out var containerImages))
{
throw new InvalidOperationException($"Container Image for dockerfile {options.Resource.Key} not found.");
}

var data = PopulateKubernetesDeploymentData(options, containerImage, dockerFile);
var data = PopulateKubernetesDeploymentData(options, containerImages.First(), dockerFile);

_manifestWriter.CreateDeployment(resourceOutputPath, data, options.TemplatePath);
_manifestWriter.CreateService(resourceOutputPath, data, options.TemplatePath);
Expand Down Expand Up @@ -78,7 +78,7 @@ public async Task BuildAndPushContainerForDockerfile(KeyValuePair<string, Resour

public void PopulateContainerImageCacheWithImage(KeyValuePair<string, Resource> resource, ContainerOptions options)
{
_containerImageCache.Add(resource.Key, options.ToImageName(resource.Key));
_containerImageCache.Add(resource.Key, options.ToImageNames(resource.Key));

_console.MarkupLine($"[green]({EmojiLiterals.CheckMark}) Done: [/] Setting container details for Dockerfile [blue]{resource.Key}[/]");
}
Expand Down Expand Up @@ -111,7 +111,7 @@ public override ComposeService CreateComposeEntry(CreateComposeEntryOptions opti
throw new InvalidOperationException($"Container Image for dockerfile {options.Resource.Key} not found.");
}

newService = newService.WithImage(containerImage.ToLowerInvariant());
newService = newService.WithImage(containerImage[0].ToLowerInvariant());
}

response.Service = newService.Build();
Expand All @@ -128,7 +128,7 @@ public override List<object> CreateKubernetesObjects(CreateKubernetesObjectsOpti
throw new InvalidOperationException($"Container Image for dockerfile {options.Resource.Key} not found.");
}

var data = PopulateKubernetesDeploymentData(options, containerImage, dockerFile);
var data = PopulateKubernetesDeploymentData(options, containerImage[0], dockerFile);

return data.ToKubernetesObjects();
}
Expand Down
1 change: 1 addition & 0 deletions src/Aspirate.Services/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
global using System.IO.Abstractions;
global using System.Runtime.InteropServices;
global using System.Security.Cryptography;
global using System.Text;
global using System.Text.Json;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Runtime.InteropServices;

namespace Aspirate.Services.Implementations;

public sealed class ContainerCompositionService(
Expand Down Expand Up @@ -49,27 +47,31 @@ public async Task<bool> BuildAndPushContainerForDockerfile(DockerfileResource do

var fullDockerfilePath = filesystem.GetFullPath(dockerfileResource.Path);

var fullImage = options.ToImageName(dockerfileResource.Name);
var fullImages = options.ToImageNames(dockerfileResource.Name);

var result = await BuildContainer(dockerfileResource, options.ContainerBuilder, nonInteractive, fullImage, fullDockerfilePath);
var result = await BuildContainer(dockerfileResource, options.ContainerBuilder, nonInteractive, fullImages, fullDockerfilePath);

CheckSuccess(result);

result = await PushContainer(options.ContainerBuilder, options.Registry, fullImage, nonInteractive);
result = await PushContainer(options.ContainerBuilder, options.Registry, fullImages, nonInteractive);

CheckSuccess(result);

return true;
}

private async Task<ShellCommandResult> PushContainer(string builder, string? registry, string fullImage, bool? nonInteractive)
private async Task<ShellCommandResult> PushContainer(string builder, string? registry, List<string> fullImages, bool? nonInteractive)
{
if (!string.IsNullOrEmpty(registry))
{
var pushArgumentBuilder = ArgumentsBuilder
.Create()
.AppendArgument(DockerLiterals.PushCommand, string.Empty, quoteValue: false)
.AppendArgument(fullImage.ToLower(), string.Empty, quoteValue: false);
.AppendArgument(DockerLiterals.PushCommand, string.Empty, quoteValue: false);

foreach (var fullImage in fullImages)
{
pushArgumentBuilder.AppendArgument(fullImage.ToLower(), string.Empty, quoteValue: false, allowDuplicates: true);
}

return await shellExecutionService.ExecuteCommand(
new()
Expand All @@ -84,13 +86,16 @@ private async Task<ShellCommandResult> PushContainer(string builder, string? reg
return new ShellCommandResult(true, string.Empty, string.Empty, 0);
}

private Task<ShellCommandResult> BuildContainer(DockerfileResource dockerfileResource, string builder, bool? nonInteractive, string tag, string fullDockerfilePath)
private Task<ShellCommandResult> BuildContainer(DockerfileResource dockerfileResource, string builder, bool? nonInteractive, List<string> tags, string fullDockerfilePath)
{
var buildArgumentBuilder = ArgumentsBuilder
.Create()
.AppendArgument(DockerLiterals.BuildCommand, string.Empty, quoteValue: false)
.AppendArgument(DockerLiterals.TagArgument, tag.ToLower());
.AppendArgument(DockerLiterals.BuildCommand, string.Empty, quoteValue: false);

foreach (var tag in tags)
{
buildArgumentBuilder.AppendArgument(DockerLiterals.TagArgument, tag.ToLower(), allowDuplicates: true);
}

if (dockerfileResource.Env is not null)
{
Expand Down Expand Up @@ -215,6 +220,13 @@ private static void AddContainerDetailsToArguments(ArgumentsBuilder argumentsBui
argumentsBuilder.AppendArgument(DotNetSdkLiterals.ContainerImageNameArgument, containerDetails.ContainerImageName);
}

if (containerDetails.ContainerImageTag is not null && containerDetails.ContainerImageTag.Contains(';'))
{
argumentsBuilder.AppendArgument(DotNetSdkLiterals.ContainerImageTagArguments,
$"\\\"{containerDetails.ContainerImageTag}\\\"");
return;
}

argumentsBuilder.AppendArgument(DotNetSdkLiterals.ContainerImageTagArgument, containerDetails.ContainerImageTag);
}

Expand Down
18 changes: 11 additions & 7 deletions src/Aspirate.Services/Implementations/ContainerDetailsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public async Task<MsBuildContainerProperties> GetContainerDetails(
}

// Fallback to latest tag if tag not specified.
HandleTag(msBuildProperties, options.Tag);
HandleTags(msBuildProperties, options.Tags);

msBuildProperties.Properties.FullContainerImage = GetFullImage(msBuildProperties.Properties, options.Prefix);

Expand All @@ -50,7 +50,7 @@ private static string GetFullImage(MsBuildContainerProperties containerDetails,
}

private static void HandleTag(MsBuildContainerProperties containerDetails) =>
_imageBuilder.Append($":{containerDetails.ContainerImageTag}");
_imageBuilder.Append($":{containerDetails.ContainerImageTag.Split(";").First()}");

private static void HandleImage(MsBuildContainerProperties containerDetails)
{
Expand Down Expand Up @@ -107,26 +107,30 @@ private void EnsureContainerRegistryIsNotEmpty(
if (!string.IsNullOrEmpty(containerRegistry))
{
details.ContainerRegistry = containerRegistry;
return;
}
//
// console.MarkupLine($"[red bold]Required MSBuild property [blue]'ContainerRegistry'[/] not set in project [blue]'{project.Path}'. Cannot continue[/].[/]");
// throw new ActionCausesExitException(1);
}

private static void HandleTag(
private static void HandleTags(
MsBuildProperties<MsBuildContainerProperties> msBuildProperties,
string containerImageTag)
List<string>? containerImageTag)
{
if (!string.IsNullOrEmpty(msBuildProperties.Properties.ContainerImageTag))
{
return;
}

// Use our custom fall-back value if it exists
if (!string.IsNullOrEmpty(containerImageTag))
if (containerImageTag is not null)
{
msBuildProperties.Properties.ContainerImageTag = containerImageTag;
if (!containerImageTag.Contains("latest", StringComparer.OrdinalIgnoreCase))
{
containerImageTag.Insert(0, "latest");
}

msBuildProperties.Properties.ContainerImageTag = string.Join(';', containerImageTag);
return;
}

Expand Down
41 changes: 25 additions & 16 deletions src/Aspirate.Shared/Extensions/DockerfileParametersExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,39 @@ public static class DockerfileParametersExtensions
{
private static readonly StringBuilder _tagBuilder = new();

public static string ToImageName(this ContainerOptions options, string resourceKey)
public static List<string> ToImageNames(this ContainerOptions options, string resourceKey)
{
_tagBuilder.Clear();
var images = new List<string>();
options.Tags.Remove("latest");
options.Tags.Insert(0, "latest");

if (!string.IsNullOrEmpty(options.Registry))
foreach (var tag in options.Tags)
{
_tagBuilder.Append($"{options.Registry}/");
}
_tagBuilder.Clear();

if (!string.IsNullOrEmpty(options.Prefix))
{
_tagBuilder.Append($"{options.Prefix}/");
}
if (!string.IsNullOrEmpty(options.Registry))
{
_tagBuilder.Append($"{options.Registry}/");
}

if (string.IsNullOrEmpty(options.ImageName))
{
options.ImageName = resourceKey;
}
if (!string.IsNullOrEmpty(options.Prefix))
{
_tagBuilder.Append($"{options.Prefix}/");
}

if (string.IsNullOrEmpty(options.ImageName))
{
options.ImageName = resourceKey;
}

_tagBuilder.Append(options.ImageName);
_tagBuilder.Append(options.ImageName);

AppendTag(options.Tag);
AppendTag(tag);

images.Add(_tagBuilder.ToString());
}

return _tagBuilder.ToString();
return images;
}

private static void AppendTag(string? tag)
Expand Down
2 changes: 1 addition & 1 deletion src/Aspirate.Shared/Inputs/ContainerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ public class ContainerOptions
public string ImageName { get; set; } = default!;
public string Registry { get; set; } = default!;
public string? Prefix { get; set; }
public string? Tag { get; set; }
public List<string>? Tags { get; set; } = ["latest"];
}
Loading

0 comments on commit 53d6bdc

Please sign in to comment.