diff --git a/Source/Bake.Tests/IntegrationTests/BakeTests/DockerFileSimpleTests.cs b/Source/Bake.Tests/IntegrationTests/BakeTests/DockerFileSimpleTests.cs index adc7b93c..4c3c6aef 100644 --- a/Source/Bake.Tests/IntegrationTests/BakeTests/DockerFileSimpleTests.cs +++ b/Source/Bake.Tests/IntegrationTests/BakeTests/DockerFileSimpleTests.cs @@ -20,9 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; using Bake.Core; using Bake.Tests.Helpers; using FluentAssertions; @@ -68,6 +65,36 @@ public async Task Run() }); } + [Test] + public async Task SignContainer() + { + // Arrange + var version = SemVer.Random.ToString(); + var expectedContainerNameAndTag = $"awesome-container:{version}"; + + // Act + var returnCode = await ExecuteAsync(TestState.New( + "run", + "--convention=Release", + "--sign-artifacts=true", + "--destination=container>localhost:5000", + "--build-version", version) + .WithEnvironmentVariables(new Dictionary + { + ["bake_credentials_docker_localhost_username"] = "registryuser", + ["bake_credentials_docker_localhost_password"] = "registrypassword", + })); + + // Assert + returnCode.Should().Be(0); + var images = await DockerHelper.ListImagesAsync(); + images.Should().Contain(new[] + { + $"bake.local/{expectedContainerNameAndTag}", + $"localhost:5000/{expectedContainerNameAndTag}" + }); + } + [Test] public async Task NamedDockerfile() { diff --git a/Source/Bake/Commands/Plan/PlanCommand.cs b/Source/Bake/Commands/Plan/PlanCommand.cs index cde236a9..da296f04 100644 --- a/Source/Bake/Commands/Plan/PlanCommand.cs +++ b/Source/Bake/Commands/Plan/PlanCommand.cs @@ -62,7 +62,8 @@ public async Task ExecuteAsync( Destination[]? destination = null, LogEventLevel logLevel = LogEventLevel.Information, Platform[]? targetPlatform = null, - bool pushContainerLatest = false) + bool pushContainerLatest = false, + bool signArtifacts = false) { _logCollector.LogLevel = logLevel; @@ -105,7 +106,8 @@ public async Task ExecuteAsync( Directory.GetCurrentDirectory(), targetPlatform, convention, - pushContainerLatest)); + pushContainerLatest, + signArtifacts)); content.Ingredients.Destinations.AddRange(destination ?? Enumerable.Empty()); var book = await _editor.ComposeAsync( diff --git a/Source/Bake/Commands/Run/RunCommand.cs b/Source/Bake/Commands/Run/RunCommand.cs index 534d5657..d90da01e 100644 --- a/Source/Bake/Commands/Run/RunCommand.cs +++ b/Source/Bake/Commands/Run/RunCommand.cs @@ -64,6 +64,7 @@ public async Task ExecuteAsync( LogEventLevel logLevel = LogEventLevel.Information, bool printPlan = true, bool pushContainerLatestTag = false, + bool signArtifacts = false, Platform[]? targetPlatform = null) { _logCollector.LogLevel = logLevel; @@ -74,7 +75,8 @@ public async Task ExecuteAsync( Directory.GetCurrentDirectory(), targetPlatform, convention, - pushContainerLatestTag)); + pushContainerLatestTag, + signArtifacts)); content.Ingredients.Destinations.AddRange(destination ?? Enumerable.Empty()); diff --git a/Source/Bake/Cooking/Composers/ContainerSignComposer.cs b/Source/Bake/Cooking/Composers/ContainerSignComposer.cs new file mode 100644 index 00000000..45a1099e --- /dev/null +++ b/Source/Bake/Cooking/Composers/ContainerSignComposer.cs @@ -0,0 +1,73 @@ +// MIT License +// +// Copyright (c) 2021-2024 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using Bake.Services; +using Bake.ValueObjects.Artifacts; +using Bake.ValueObjects.Recipes; +using Microsoft.Extensions.Logging; + +namespace Bake.Cooking.Composers +{ + public class ContainerSignComposer : Composer + { + private readonly ILogger _logger; + private readonly ISoftwareInstaller _softwareInstaller; + + public override IReadOnlyCollection Consumes { get; } = new[] + { + ArtifactType.Container, + }; + + public ContainerSignComposer( + ILogger logger, + ISoftwareInstaller softwareInstaller) + { + _logger = logger; + _softwareInstaller = softwareInstaller; + } + + public override Task> ComposeAsync( + IContext context, + CancellationToken cancellationToken) + { + if (!context.Ingredients.SignArtifacts) + { + return Task.FromResult(EmptyRecipes); + } + + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_TOKEN"))) + { + _logger.LogError("Signing of containers is currently only supported while running in GitHub Actions!"); + return Task.FromResult(EmptyRecipes); + } + + // Start install process in the background + _softwareInstaller.Install(KnownSoftware.cosign); + + var containers = context + .GetArtifacts() + .ToArray(); + + return Task.FromResult(EmptyRecipes); + } + } +} diff --git a/Source/Bake/Core/Defaults.cs b/Source/Bake/Core/Defaults.cs index 8f9df111..4aabf396 100644 --- a/Source/Bake/Core/Defaults.cs +++ b/Source/Bake/Core/Defaults.cs @@ -20,11 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - // ReSharper disable StringLiteralTypo namespace Bake.Core @@ -42,6 +37,7 @@ public class Defaults : IDefaults public string GoLdFlags { get; private set; } = "-s -w"; public string GoEnvPrivate { get; private set; } = "direct"; public string DotNetRollForward { get; private set; } = "LatestMajor"; + public bool InstallSoftwareInBackground { get; private set; } = true; public Defaults( IEnvironmentVariables environmentVariables) @@ -63,6 +59,7 @@ public async Task InitializeAsync( GoLdFlags = GetString(e, "go_ldflags", GoLdFlags); GoEnvPrivate = GetString(e, "go_env_goprivate", GoEnvPrivate); DotNetRollForward = GetString(e, "dotnet_roll_forward", DotNetRollForward); + InstallSoftwareInBackground = GetBool(e, "software_install_background", true); } private static bool GetBool( diff --git a/Source/Bake/Core/IDefaults.cs b/Source/Bake/Core/IDefaults.cs index 79f1b493..4d61cec3 100644 --- a/Source/Bake/Core/IDefaults.cs +++ b/Source/Bake/Core/IDefaults.cs @@ -20,9 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -using System.Threading; -using System.Threading.Tasks; - namespace Bake.Core { public interface IDefaults @@ -41,6 +38,8 @@ public interface IDefaults string DotNetRollForward { get; } + bool InstallSoftwareInBackground { get; } + Task InitializeAsync( CancellationToken cancellationToken); } diff --git a/Source/Bake/Extensions/ServiceCollectionExtensions.cs b/Source/Bake/Extensions/ServiceCollectionExtensions.cs index 86a5e2e0..e5b53dcd 100644 --- a/Source/Bake/Extensions/ServiceCollectionExtensions.cs +++ b/Source/Bake/Extensions/ServiceCollectionExtensions.cs @@ -103,16 +103,17 @@ public static IServiceCollection AddBake( .AddTransient() // Composers - .AddTransient() + .AddTransient() + .AddTransient() .AddTransient() + .AddTransient() + .AddTransient() .AddTransient() - .AddTransient() .AddTransient() + .AddTransient() + .AddTransient() .AddTransient() - .AddTransient() .AddTransient() - .AddTransient() - .AddTransient() // Cooks - .NET .AddTransient() diff --git a/Source/Bake/Services/ISoftwareInstaller.cs b/Source/Bake/Services/ISoftwareInstaller.cs index 67cbbef1..a9502543 100644 --- a/Source/Bake/Services/ISoftwareInstaller.cs +++ b/Source/Bake/Services/ISoftwareInstaller.cs @@ -29,5 +29,8 @@ public interface ISoftwareInstaller Task InstallAsync( Software software, CancellationToken cancellationToken); + + void Install( + Software software); } } diff --git a/Source/Bake/Services/SoftwareInstaller.cs b/Source/Bake/Services/SoftwareInstaller.cs index 08d5690e..b084310f 100644 --- a/Source/Bake/Services/SoftwareInstaller.cs +++ b/Source/Bake/Services/SoftwareInstaller.cs @@ -22,8 +22,10 @@ using System.Collections.Concurrent; using System.Runtime.InteropServices; +using Bake.Core; using Bake.ValueObjects; using Microsoft.Extensions.Logging; +using File = System.IO.File; namespace Bake.Services { @@ -31,14 +33,33 @@ public class SoftwareInstaller : ISoftwareInstaller { private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; + private readonly IDefaults _defaults; private readonly ConcurrentDictionary>> _alreadyInstalledSoftware = new(); public SoftwareInstaller( ILogger logger, - IHttpClientFactory httpClientFactory) + IHttpClientFactory httpClientFactory, + IDefaults defaults) { _logger = logger; _httpClientFactory = httpClientFactory; + _defaults = defaults; + } + + public void Install( + Software software) + { + if (!_defaults.InstallSoftwareInBackground) + { + _logger.LogInformation( + "Installing software in the background disabled, waiting to install {Name} version {Version} to when needed", + software.Name, + software.Version); + return; + } + + // Installs in the background to have software ready when needed + Task.Run(() => InstallAsync(software, CancellationToken.None)); } public Task InstallAsync( diff --git a/Source/Bake/ValueObjects/Ingredients.cs b/Source/Bake/ValueObjects/Ingredients.cs index 88c323b4..455886ee 100644 --- a/Source/Bake/ValueObjects/Ingredients.cs +++ b/Source/Bake/ValueObjects/Ingredients.cs @@ -33,14 +33,16 @@ public static Ingredients New( string workingDirectory, IReadOnlyCollection? targetPlatforms = null, Convention convention = Convention.Default, - bool pushContainerLatestTag = false) => new( + bool pushContainerLatestTag = false, + bool signArtifacts = false) => new( version, workingDirectory, targetPlatforms != null && targetPlatforms.Any() ? targetPlatforms.ToArray() : Platform.Defaults, convention, - pushContainerLatestTag); + pushContainerLatestTag, + signArtifacts); [YamlMember] public SemVer Version { get; [Obsolete] set; } @@ -52,7 +54,10 @@ public static Ingredients New( public Convention Convention { get; [Obsolete] set; } [YamlMember] - public bool PushContainerLatestTag { get; } + public bool PushContainerLatestTag { get; [Obsolete] set; } + + [YamlMember] + public bool SignArtifacts { get; [Obsolete] set; } [YamlMember] public Platform[] Platforms { get; [Obsolete] set; } @@ -188,7 +193,8 @@ public Ingredients( string workingDirectory, Platform[] platforms, Convention convention, - bool pushContainerLatestTag) + bool pushContainerLatestTag, + bool signArtifacts) { #pragma warning disable CS0612 // Type or member is obsolete Version = version; @@ -196,6 +202,7 @@ public Ingredients( Platforms = platforms; Convention = convention; PushContainerLatestTag = pushContainerLatestTag; + SignArtifacts = signArtifacts; #pragma warning restore CS0612 // Type or member is obsolete }