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/Cooks/ChartMuseum/ChartMuseumUploadCook.cs b/Source/Bake/Cooking/Cooks/ChartMuseum/ChartMuseumUploadCook.cs index 572e51d0..f5b19c00 100644 --- a/Source/Bake/Cooking/Cooks/ChartMuseum/ChartMuseumUploadCook.cs +++ b/Source/Bake/Cooking/Cooks/ChartMuseum/ChartMuseumUploadCook.cs @@ -20,6 +20,7 @@ // 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; @@ -46,11 +47,13 @@ protected override async Task CookAsync( ChartMuseumUploadRecipe recipe, CancellationToken cancellationToken) { + var url = new Uri(recipe.Url, "/api/charts"); + foreach (var packagePath in recipe.Packages) { await _uploader.UploadAsync( packagePath, - recipe.Url, + url, cancellationToken); } diff --git a/Source/Bake/Services/DestinationParser.cs b/Source/Bake/Services/DestinationParser.cs index baeaf1b6..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,11 +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.OctopusDeploy => Uri.TryCreate(typedMatch.Groups["rest"].Value, UriKind.Absolute, out var octopusDeployUrl) - ? new ChartMuseumDestination(octopusDeployUrl.AbsoluteUri) + 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/Uploader.cs b/Source/Bake/Services/Uploader.cs index 2e7aa946..d07f6daa 100644 --- a/Source/Bake/Services/Uploader.cs +++ b/Source/Bake/Services/Uploader.cs @@ -29,6 +29,7 @@ using System.Threading; using System.Threading.Tasks; using Bake.Core; +using Microsoft.Extensions.Logging; namespace Bake.Services { @@ -42,17 +43,21 @@ public class Uploader : IUploader ["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; } @@ -64,9 +69,14 @@ public async Task UploadAsync( { var file = _fileSystem.Open(filePath); var fileName = Path.GetFileName(filePath); - var mediaType = MediaTypes.TryGetValue(Path.GetExtension(filePath), out var t) ? t : DefaultMediaType; + 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(); @@ -74,13 +84,14 @@ public async Task UploadAsync( var fileStreamContent = new StreamContent(stream); fileStreamContent.Headers.ContentType = new MediaTypeHeaderValue(mediaType); - multipartFormDataContent.Add(fileStreamContent, name: "file", fileName: fileName); + multipartFormDataContent.Add(fileStreamContent, name: "chart", fileName: fileName); var response = await httpClient.PostAsync(url, multipartFormDataContent, cancellationToken); if (!response.IsSuccessStatusCode) { - throw new Exception($"POST {url} failed with {response.StatusCode}"); + 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/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