diff --git a/Bake.sln b/Bake.sln index f910eb65..1f88ac7c 100644 --- a/Bake.sln +++ b/Bake.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".build", ".build", "{F11194 ProjectSection(SolutionItems) = preProject docker-compose.yml = docker-compose.yml .github\workflows\pull-requests.yml = .github\workflows\pull-requests.yml + README.md = README.md .github\workflows\release.yml = .github\workflows\release.yml RELEASE_NOTES.md = RELEASE_NOTES.md EndProjectSection diff --git a/README.md b/README.md index af3bce7e..15c1144c 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,8 @@ Here are some examples of common used arguments to Bake * `helm-chart>octopus@http://octopus.local/` - Sends Helm charts to the built-in repository in [Octopus Deploy][octopus-repository]. Bake looks for the API-key in an environment variable named `OCTOPUS_DEPLOY_APIKEY` + * `helm-chart>chart-museum@http://chart-museum.local/` - Sends Helm charts to an + instance of [ChartMuseum](https://chartmuseum.com/) * **NuGet** * `nuget` - An unnamed destination will send NuGet packages to the central NuGet repository at [nuget.org](https://www.nuget.org/). Bake will look for diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8ae33915..fd917a40 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,6 @@ # 0.16-beta -* *Nothing yet...* +* New: Ability to upload Helm charts to ChartMuseum # 0.15-beta diff --git a/Source/Bake.Tests/IntegrationTests/BakeTests/HelmChartTests.cs b/Source/Bake.Tests/IntegrationTests/BakeTests/HelmChartTests.cs index a1f9cfeb..14d00bc0 100644 --- a/Source/Bake.Tests/IntegrationTests/BakeTests/HelmChartTests.cs +++ b/Source/Bake.Tests/IntegrationTests/BakeTests/HelmChartTests.cs @@ -74,5 +74,22 @@ public async Task PushToOctopusDeploy( returnCode.Should().Be(0); octopusDeploy.ReceivedPackages.Should().HaveCount(1); } + + [Test] + public async Task PushToChartMuseum() + { + // Arrange + var version = SemVer.Random.ToString(); + + // Act + var returnCode = await ExecuteAsync(TestState.New( + "run", + "--convention=Release", + $"--destination=helm-chart>chart-museum@http://localhost:5556", + "--build-version", version)); + + // Assert + returnCode.Should().Be(0); + } } } diff --git a/Source/Bake/Cooking/Composers/ChartMuseumComposer.cs b/Source/Bake/Cooking/Composers/ChartMuseumComposer.cs new file mode 100644 index 00000000..1a69e018 --- /dev/null +++ b/Source/Bake/Cooking/Composers/ChartMuseumComposer.cs @@ -0,0 +1,87 @@ +// MIT License +// +// Copyright (c) 2021 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 System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Bake.Services; +using Bake.ValueObjects.Artifacts; +using Bake.ValueObjects.Destinations; +using Bake.ValueObjects.Recipes; +using Bake.ValueObjects.Recipes.ChartMuseum; +using Bake.ValueObjects.Recipes.OctopusDeploy; + +namespace Bake.Cooking.Composers +{ + public class ChartMuseumComposer : Composer + { + private readonly IConventionInterpreter _conventionInterpreter; + + public override IReadOnlyCollection Consumes { get; } = new[] + { + ArtifactType.HelmChart, + }; + + public ChartMuseumComposer( + IConventionInterpreter conventionInterpreter) + { + _conventionInterpreter = conventionInterpreter; + } + + public override Task> ComposeAsync( + IContext context, + CancellationToken cancellationToken) + { + if (!_conventionInterpreter.ShouldArtifactsBePublished(context.Ingredients.Convention)) + { + return Task.FromResult(EmptyRecipes); + } + + var chartMuseumDestination = context.Ingredients.Destinations + .OfType() + .SingleOrDefault(); + + if (chartMuseumDestination == null) + { + return Task.FromResult(EmptyRecipes); + } + + var packages = Enumerable.Empty() + .Concat(context.GetArtifacts().Select(a => a.Path)) + .ToArray(); + + if (!packages.Any()) + { + return Task.FromResult(EmptyRecipes); + } + + return Task.FromResult>(new Recipe[] + { + new ChartMuseumUploadRecipe( + new Uri(chartMuseumDestination.Url, UriKind.Absolute), + packages) + }); + } + } +} diff --git a/Source/Bake/Cooking/Cooks/ChartMuseum/ChartMuseumUploadCook.cs b/Source/Bake/Cooking/Cooks/ChartMuseum/ChartMuseumUploadCook.cs new file mode 100644 index 00000000..f5b19c00 --- /dev/null +++ b/Source/Bake/Cooking/Cooks/ChartMuseum/ChartMuseumUploadCook.cs @@ -0,0 +1,63 @@ +// MIT License +// +// Copyright (c) 2021 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 System; +using System.Threading; +using System.Threading.Tasks; +using Bake.Services; +using Bake.ValueObjects.Recipes.ChartMuseum; +using Microsoft.Extensions.Logging; + +namespace Bake.Cooking.Cooks.ChartMuseum +{ + public class ChartMuseumUploadCook : Cook + { + private readonly ILogger _logger; + private readonly IUploader _uploader; + + public ChartMuseumUploadCook( + ILogger logger, + IUploader uploader) + { + _logger = logger; + _uploader = uploader; + } + + protected override async Task CookAsync( + IContext context, + ChartMuseumUploadRecipe recipe, + CancellationToken cancellationToken) + { + var url = new Uri(recipe.Url, "/api/charts"); + + foreach (var packagePath in recipe.Packages) + { + await _uploader.UploadAsync( + packagePath, + url, + cancellationToken); + } + + return true; + } + } +} diff --git a/Source/Bake/Extensions/ServiceCollectionExtensions.cs b/Source/Bake/Extensions/ServiceCollectionExtensions.cs index 7c7cedf9..731ff8cb 100644 --- a/Source/Bake/Extensions/ServiceCollectionExtensions.cs +++ b/Source/Bake/Extensions/ServiceCollectionExtensions.cs @@ -27,6 +27,7 @@ using Bake.Cooking; using Bake.Cooking.Composers; using Bake.Cooking.Cooks; +using Bake.Cooking.Cooks.ChartMuseum; using Bake.Cooking.Cooks.Docker; using Bake.Cooking.Cooks.DotNet; using Bake.Cooking.Cooks.GitHub; @@ -81,6 +82,7 @@ public static IServiceCollection AddBake( .AddTransient() .AddTransient() .AddTransient() + .AddSingleton() // Gathers .AddTransient() @@ -104,6 +106,7 @@ public static IServiceCollection AddBake( .AddTransient() .AddTransient() .AddTransient() + .AddTransient() // Cooks - .NET .AddTransient() @@ -114,6 +117,8 @@ public static IServiceCollection AddBake( .AddTransient() .AddTransient() .AddTransient() + // Cooks - ChartMuseum + .AddTransient() // Cooks - Docker .AddTransient() .AddTransient() diff --git a/Source/Bake/Names.cs b/Source/Bake/Names.cs index 8c724874..212f1a07 100644 --- a/Source/Bake/Names.cs +++ b/Source/Bake/Names.cs @@ -39,6 +39,7 @@ public static class Destinations public const string NuGetRegistry = "nuget-registry"; public const string Dynamic = "dynamic"; public const string OctopusDeploy = "octopus-deploy"; + public const string ChartMuseum = "chart-museum"; } public static class DynamicDestinations @@ -136,6 +137,11 @@ public static class OctopusDeploy public const string PackageRawPush = "octopus-deploy-package-raw-push"; } + public static class ChartMuseum + { + public const string Upload = "chart-museum-upload"; + } + public static class Docker { public const string Build = "docker-build"; diff --git a/Source/Bake/Services/DestinationParser.cs b/Source/Bake/Services/DestinationParser.cs index a3a7f746..631d9d3d 100644 --- a/Source/Bake/Services/DestinationParser.cs +++ b/Source/Bake/Services/DestinationParser.cs @@ -33,7 +33,7 @@ public class DestinationParser : IDestinationParser @"^[a-z\-0-9]+$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex TypedDestination = new( - "^(?[a-z]+)@(?.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + "^(?[a-z-]+)@(?.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private const char Separator = '>'; private readonly IDefaults _defaults; @@ -101,8 +101,11 @@ public bool TryParse(string str, out Destination destination) destination = typedMatch.Groups["type"].Value switch { - "octopus" => Uri.TryCreate(typedMatch.Groups["rest"].Value, UriKind.Absolute, out var octopusDeployUrl) - ? new OctopusDeployDestination(octopusDeployUrl.AbsoluteUri) + "octopus" => Uri.TryCreate(typedMatch.Groups["rest"].Value, UriKind.Absolute, out var u1) + ? new OctopusDeployDestination(u1.AbsoluteUri) + : null, + Names.Destinations.ChartMuseum => Uri.TryCreate(typedMatch.Groups["rest"].Value, UriKind.Absolute, out var u2) + ? new ChartMuseumDestination(u2.AbsoluteUri) : null, _ => null }; diff --git a/Source/Bake/Services/IUploader.cs b/Source/Bake/Services/IUploader.cs new file mode 100644 index 00000000..1428d80a --- /dev/null +++ b/Source/Bake/Services/IUploader.cs @@ -0,0 +1,36 @@ +// MIT License +// +// Copyright (c) 2021-2022 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 System; +using System.Threading; +using System.Threading.Tasks; + +namespace Bake.Services +{ + public interface IUploader + { + Task UploadAsync( + string filePath, + Uri url, + CancellationToken cancellationToken); + } +} diff --git a/Source/Bake/Services/Uploader.cs b/Source/Bake/Services/Uploader.cs new file mode 100644 index 00000000..d07f6daa --- /dev/null +++ b/Source/Bake/Services/Uploader.cs @@ -0,0 +1,98 @@ +// MIT License +// +// Copyright (c) 2021-2022 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 System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Bake.Core; +using Microsoft.Extensions.Logging; + +namespace Bake.Services +{ + public class Uploader : IUploader + { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types + private readonly IReadOnlyDictionary MediaTypes = new ConcurrentDictionary + { + ["bz"] = "application/x-bzip", + ["bz"] = "application/x-bzip2", + ["gz"] = "application/gzip", + ["png"] = "image/png", + ["zip"] = "application/zip", + ["tgz"] = "application/gzip", + }; + + private const string DefaultMediaType = "application/octet-stream"; + + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IFileSystem _fileSystem; + + public Uploader( + ILogger logger, + IHttpClientFactory httpClientFactory, + IFileSystem fileSystem) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + _fileSystem = fileSystem; + } + + public async Task UploadAsync( + string filePath, + Uri url, + CancellationToken cancellationToken) + { + var file = _fileSystem.Open(filePath); + var fileName = Path.GetFileName(filePath); + var fileExtension = Path.GetExtension(filePath).Trim('.'); + var mediaType = MediaTypes.TryGetValue(fileExtension, out var t) ? t : DefaultMediaType; + var httpClient = _httpClientFactory.CreateClient(); + + _logger.LogInformation( + "Uploading file {FileName} with extension {Extension} and MIME type {MIME} to URL {Url}", + fileName, fileExtension, mediaType, url); + + await using var stream = await file.OpenReadAsync(cancellationToken); + + using var multipartFormDataContent = new MultipartFormDataContent(); + + var fileStreamContent = new StreamContent(stream); + fileStreamContent.Headers.ContentType = new MediaTypeHeaderValue(mediaType); + + multipartFormDataContent.Add(fileStreamContent, name: "chart", fileName: fileName); + + var response = await httpClient.PostAsync(url, multipartFormDataContent, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(cancellationToken); + throw new Exception($"POST {url} failed with {response.StatusCode}{Environment.NewLine}{content[..(Math.Min(content.Length, 1024) - 1)]}"); + } + } + } +} diff --git a/Source/Bake/ValueObjects/Destinations/ChartMuseumDestination.cs b/Source/Bake/ValueObjects/Destinations/ChartMuseumDestination.cs new file mode 100644 index 00000000..85b46708 --- /dev/null +++ b/Source/Bake/ValueObjects/Destinations/ChartMuseumDestination.cs @@ -0,0 +1,50 @@ +// MIT License +// +// Copyright (c) 2021 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 System; +using YamlDotNet.Serialization; + +namespace Bake.ValueObjects.Destinations +{ + [Destination(Names.Destinations.ChartMuseum)] + public class ChartMuseumDestination : Destination + { + [YamlMember(typeof(string))] + public string Url { get; [Obsolete] set; } + + [Obsolete] + public ChartMuseumDestination() { } + + public ChartMuseumDestination( + string url) + { +#pragma warning disable CS0612 // Type or member is obsolete + Url = url; +#pragma warning restore CS0612 // Type or member is obsolete + } + + public override string ToString() + { + return $"{Names.Destinations.ChartMuseum}|{Url}"; + } + } +} \ No newline at end of file diff --git a/Source/Bake/ValueObjects/Recipes/ChartMuseum/ChartMuseumUploadRecipe.cs b/Source/Bake/ValueObjects/Recipes/ChartMuseum/ChartMuseumUploadRecipe.cs new file mode 100644 index 00000000..e1d61c82 --- /dev/null +++ b/Source/Bake/ValueObjects/Recipes/ChartMuseum/ChartMuseumUploadRecipe.cs @@ -0,0 +1,50 @@ +// MIT License +// +// Copyright (c) 2021 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 System; +using YamlDotNet.Serialization; + +namespace Bake.ValueObjects.Recipes.ChartMuseum +{ + [Recipe(Names.Recipes.ChartMuseum.Upload)] + public class ChartMuseumUploadRecipe : Recipe + { + [YamlMember(SerializeAs = typeof(string))] + public Uri Url { get; [Obsolete] set; } + + [YamlMember] + public string[] Packages { get; [Obsolete] set; } + + [Obsolete] + public ChartMuseumUploadRecipe() { } + + public ChartMuseumUploadRecipe( + Uri url, + string[] packages) + { +#pragma warning disable CS0612 // Type or member is obsolete + Url = url; + Packages = packages; +#pragma warning restore CS0612 // Type or member is obsolete + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml index e2043fc3..154c8925 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,10 +11,18 @@ services: registry: image: registry:2 ports: - - 5000:5000 + - 5000:5000 environment: - REGISTRY_AUTH: "htpasswd" - REGISTRY_AUTH_HTPASSWD_REALM: "Bake test registry" - REGISTRY_AUTH_HTPASSWD_PATH: "/auth/htpasswd" + REGISTRY_AUTH: "htpasswd" + REGISTRY_AUTH_HTPASSWD_REALM: "Bake test registry" + REGISTRY_AUTH_HTPASSWD_PATH: "/auth/htpasswd" volumes: - - ./Services/Registry:/auth + - ./Services/Registry:/auth + + chart-museum: + image: ghcr.io/helm/chartmuseum:v0.15.0 + ports: + - 5556:8080 + environment: + STORAGE: local + STORAGE_LOCAL_ROOTDIR: /tmp