From acdff8028994a8b8f03c72ba75c48036cbeb096b Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Wed, 11 Sep 2024 09:54:57 +0200 Subject: [PATCH] Get a basic software installer up and running --- .../ServiceTests/SoftwareInstallerTests.cs | 50 +++++++ Source/Bake/Core/SemVer.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 1 + Source/Bake/KnownSoftware.cs | 39 ++++++ Source/Bake/Services/ISoftwareInstaller.cs | 33 +++++ Source/Bake/Services/SoftwareInstaller.cs | 128 ++++++++++++++++++ Source/Bake/ValueObjects/InstalledSoftware.cs | 38 ++++++ Source/Bake/ValueObjects/Software.cs | 48 +++++++ .../SoftwareDownloadArchitecture.cs | 30 ++++ 9 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 Source/Bake.Tests/IntegrationTests/ServiceTests/SoftwareInstallerTests.cs create mode 100644 Source/Bake/KnownSoftware.cs create mode 100644 Source/Bake/Services/ISoftwareInstaller.cs create mode 100644 Source/Bake/Services/SoftwareInstaller.cs create mode 100644 Source/Bake/ValueObjects/InstalledSoftware.cs create mode 100644 Source/Bake/ValueObjects/Software.cs create mode 100644 Source/Bake/ValueObjects/SoftwareDownloadArchitecture.cs diff --git a/Source/Bake.Tests/IntegrationTests/ServiceTests/SoftwareInstallerTests.cs b/Source/Bake.Tests/IntegrationTests/ServiceTests/SoftwareInstallerTests.cs new file mode 100644 index 00000000..2fddd1d1 --- /dev/null +++ b/Source/Bake.Tests/IntegrationTests/ServiceTests/SoftwareInstallerTests.cs @@ -0,0 +1,50 @@ +// 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.Tests.Helpers; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace Bake.Tests.IntegrationTests.ServiceTests +{ + public class SoftwareInstallerTests : ServiceTest + { + public SoftwareInstallerTests() : base(null) {} + + [Test] + public async Task Install() + { + // Arrange + using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + + // Act + await Sut.InstallAsync(KnownSoftware.cosign, timeout.Token); + } + + protected override IServiceCollection Configure(IServiceCollection serviceCollection) + { + return base.Configure(serviceCollection) + .AddHttpClient(); + } + } +} diff --git a/Source/Bake/Core/SemVer.cs b/Source/Bake/Core/SemVer.cs index 08e4c316..35a262bf 100644 --- a/Source/Bake/Core/SemVer.cs +++ b/Source/Bake/Core/SemVer.cs @@ -122,7 +122,7 @@ public static SemVer With( private SemVer( int major, int? minor, - int? patch , + int? patch, string? meta) { Major = major; diff --git a/Source/Bake/Extensions/ServiceCollectionExtensions.cs b/Source/Bake/Extensions/ServiceCollectionExtensions.cs index f3cce426..86a5e2e0 100644 --- a/Source/Bake/Extensions/ServiceCollectionExtensions.cs +++ b/Source/Bake/Extensions/ServiceCollectionExtensions.cs @@ -81,6 +81,7 @@ public static IServiceCollection AddBake( .AddSingleton() .AddTransient() .AddTransient() + .AddSingleton() .AddTransient() .AddTransient() .AddTransient() diff --git a/Source/Bake/KnownSoftware.cs b/Source/Bake/KnownSoftware.cs new file mode 100644 index 00000000..8fe73f62 --- /dev/null +++ b/Source/Bake/KnownSoftware.cs @@ -0,0 +1,39 @@ +// 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.Core; +using Bake.ValueObjects; + +namespace Bake +{ + public static class KnownSoftware + { + public static readonly Software cosign = new( + "cosign", + SemVer.With(2, 4), + new Dictionary + { + [SoftwareDownloadArchitecture.LinuxX86] = new ("https://github.com/sigstore/cosign/releases/download/v2.4.0/cosign-linux-amd64"), + [SoftwareDownloadArchitecture.WindowsX86] = new("https://github.com/sigstore/cosign/releases/download/v2.4.0/cosign-windows-amd64.exe"), + }); + } +} diff --git a/Source/Bake/Services/ISoftwareInstaller.cs b/Source/Bake/Services/ISoftwareInstaller.cs new file mode 100644 index 00000000..67cbbef1 --- /dev/null +++ b/Source/Bake/Services/ISoftwareInstaller.cs @@ -0,0 +1,33 @@ +// 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.ValueObjects; + +namespace Bake.Services +{ + public interface ISoftwareInstaller + { + Task InstallAsync( + Software software, + CancellationToken cancellationToken); + } +} diff --git a/Source/Bake/Services/SoftwareInstaller.cs b/Source/Bake/Services/SoftwareInstaller.cs new file mode 100644 index 00000000..08d5690e --- /dev/null +++ b/Source/Bake/Services/SoftwareInstaller.cs @@ -0,0 +1,128 @@ +// 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 System.Collections.Concurrent; +using System.Runtime.InteropServices; +using Bake.ValueObjects; +using Microsoft.Extensions.Logging; + +namespace Bake.Services +{ + public class SoftwareInstaller : ISoftwareInstaller + { + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ConcurrentDictionary>> _alreadyInstalledSoftware = new(); + + public SoftwareInstaller( + ILogger logger, + IHttpClientFactory httpClientFactory) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + } + + public Task InstallAsync( + Software software, + CancellationToken cancellationToken) + { + return _alreadyInstalledSoftware.GetOrAdd( + CreateCacheKey(software), + _ => new Lazy>( + () => InstallInternalAsync(software, cancellationToken), + LazyThreadSafetyMode.ExecutionAndPublication)).Value; + } + + private async Task InstallInternalAsync( + Software software, + CancellationToken cancellationToken) + { + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(TimeSpan.FromMinutes(5)); + + var directory = Path.Combine( + Path.GetTempPath(), + $"bake-software-{Guid.NewGuid():N}"); + var softwareDownloadArchitecture = PickArchitecture(); + var filename = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"{software.Name}.exe" : software.Name; + var filepath = Path.Combine(directory, filename); + + _logger.LogInformation($"Installing {software.Name} v{software.Version} for {softwareDownloadArchitecture} to temp directory {directory}"); + + if (!software.Downloads.TryGetValue(softwareDownloadArchitecture, out var url)) + { + throw new InvalidOperationException($"No download URL found for {software.Name} v{software.Version} for {softwareDownloadArchitecture}"); + } + + var httpClient = _httpClientFactory.CreateClient(); + { + using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, timeout.Token); + response.EnsureSuccessStatusCode(); // TODO: Do something more readable + + await using var filestream = File.OpenWrite(filename); + await using var stream = await response.Content.ReadAsStreamAsync(timeout.Token); + await stream.CopyToAsync(filestream, timeout.Token); + } + + return new InstalledSoftware( + software, + filepath); + } + + private static SoftwareDownloadArchitecture PickArchitecture() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + switch (RuntimeInformation.OSArchitecture) + { + case Architecture.X64: + case Architecture.X86: + return SoftwareDownloadArchitecture.WindowsX86; + default: + throw new NotSupportedException( + $"Unsupported OS architecture {RuntimeInformation.OSArchitecture}"); + + } + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + switch (RuntimeInformation.OSArchitecture) + { + case Architecture.X64: + case Architecture.X86: + return SoftwareDownloadArchitecture.LinuxX86; + default: + throw new NotSupportedException($"Unsupported OS architecture {RuntimeInformation.OSArchitecture}"); + } + + } + + throw new NotSupportedException($"Unsupported OS {RuntimeInformation.OSDescription}"); + } + + private static string CreateCacheKey(Software software) + { + return $"{software.Name} v{software.Version}"; + } + } +} diff --git a/Source/Bake/ValueObjects/InstalledSoftware.cs b/Source/Bake/ValueObjects/InstalledSoftware.cs new file mode 100644 index 00000000..8c2ea7c8 --- /dev/null +++ b/Source/Bake/ValueObjects/InstalledSoftware.cs @@ -0,0 +1,38 @@ +// 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. + +namespace Bake.ValueObjects +{ + public class InstalledSoftware + { + public Software Software { get; } + public string Path { get; } + + public InstalledSoftware( + Software software, + string path) + { + Software = software; + Path = path; + } + } +} diff --git a/Source/Bake/ValueObjects/Software.cs b/Source/Bake/ValueObjects/Software.cs new file mode 100644 index 00000000..8102c732 --- /dev/null +++ b/Source/Bake/ValueObjects/Software.cs @@ -0,0 +1,48 @@ +// 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.Core; + +namespace Bake.ValueObjects +{ + public class Software + { + public string Name { get; } + public SemVer Version { get; } + public IReadOnlyDictionary Downloads { get; } + + public Software( + string name, + SemVer version, + IReadOnlyDictionary downloads) + { + Name = name; + Version = version; + Downloads = downloads; + } + + public override string ToString() + { + return $"{Name} v{Version}"; + } + } +} diff --git a/Source/Bake/ValueObjects/SoftwareDownloadArchitecture.cs b/Source/Bake/ValueObjects/SoftwareDownloadArchitecture.cs new file mode 100644 index 00000000..91c053f5 --- /dev/null +++ b/Source/Bake/ValueObjects/SoftwareDownloadArchitecture.cs @@ -0,0 +1,30 @@ +// 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. + +namespace Bake.ValueObjects +{ + public enum SoftwareDownloadArchitecture + { + LinuxX86, + WindowsX86 + } +}