From e3c8e9628daa31136556ca6236321f38a2c6d287 Mon Sep 17 00:00:00 2001 From: Chase S Date: Wed, 5 Apr 2023 16:43:18 -0500 Subject: [PATCH] Update configure command to support configuring feature flags (#96) * Update configure command to support configuring feature flags This uses the changes from github/valet#5879 to fetch available flags via the list-features --json flag. It parses out the possible env vars and walks the customer through setting any feature flags they desire. If there are any issues getting the available feature flags JSON, we don't ask the customer to configure feature flags, as it won't cause any breaking issues and we don't want to break the configure command. * Move feature configuration to its own flag * Add unit tests * Undo changes to ConfigurationServiceTests * Code review fixes --- .../Models/FeatureTests.cs | 53 +++++++++++++ .../Services/DockerServiceTests.cs | 75 +++++++++++++++++++ src/ActionsImporter/App.cs | 29 ++++++- src/ActionsImporter/Commands/Configure.cs | 16 +++- src/ActionsImporter/Commands/ListFeatures.cs | 4 +- .../Handlers/ConfigureHandler.cs | 13 ++++ .../Interfaces/IConfigurationService.cs | 2 + .../Interfaces/IDockerService.cs | 6 +- src/ActionsImporter/Models/Feature.cs | 17 +++++ src/ActionsImporter/Program.cs | 2 +- .../Services/ConfigurationService.cs | 22 ++++++ src/ActionsImporter/Services/DockerService.cs | 23 ++++++ 12 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 src/ActionsImporter.UnitTests/Models/FeatureTests.cs create mode 100644 src/ActionsImporter/Handlers/ConfigureHandler.cs create mode 100644 src/ActionsImporter/Models/Feature.cs diff --git a/src/ActionsImporter.UnitTests/Models/FeatureTests.cs b/src/ActionsImporter.UnitTests/Models/FeatureTests.cs new file mode 100644 index 00000000..088abc20 --- /dev/null +++ b/src/ActionsImporter.UnitTests/Models/FeatureTests.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using ActionsImporter.Models; +using NUnit.Framework; + +namespace ActionsImporter.UnitTests.Models; + +[TestFixture] +public class FeatureTests +{ + private readonly string featureResult = @" + { + ""name"": ""actions/cache"", + ""description"": ""Control usage of actions/cache inside of workflows. Outputs a comment if not enabled."", + ""enabled"": false, + ""ghes_version"": ""ghes-3.5"", + ""customer_facing"": true, + ""env_name"": ""FEATURE_ACTIONS_CACHE"" + } + "; + + [Test] + public void Initialize() + { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }; + var feature = JsonSerializer.Deserialize(featureResult, options); + + Assert.AreEqual("actions/cache", feature?.Name); + Assert.AreEqual("Control usage of actions/cache inside of workflows. Outputs a comment if not enabled.", feature?.Description); + Assert.AreEqual("FEATURE_ACTIONS_CACHE", feature?.EnvName); + Assert.IsFalse(feature?.Enabled); + } + + [Test] + public void EnabledMessage() + { + var enabledFeature = new Feature + { + Enabled = true, + }; + Assert.AreEqual("enabled", enabledFeature.EnabledMessage()); + } + + [Test] + public void DisabledMessage() + { + var disabledFeature = new Feature + { + Enabled = false, + }; + Assert.AreEqual("disabled", disabledFeature.EnabledMessage()); + + } +} diff --git a/src/ActionsImporter.UnitTests/Services/DockerServiceTests.cs b/src/ActionsImporter.UnitTests/Services/DockerServiceTests.cs index 99cf7379..bd3cc57e 100644 --- a/src/ActionsImporter.UnitTests/Services/DockerServiceTests.cs +++ b/src/ActionsImporter.UnitTests/Services/DockerServiceTests.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.Json; using System.Threading.Tasks; using ActionsImporter.Interfaces; +using ActionsImporter.Models; using ActionsImporter.Services; using Moq; using NUnit.Framework; @@ -212,6 +214,79 @@ public async Task ExecuteCommandAsync_InvokesDocker_OnLinuxOS_ReturnsTrue() _processService.VerifyAll(); } + [Test] + public async Task GetFeaturesAsync_ReturnsFeatures() + { + // Arrange + var image = "actions-importer/cli"; + var server = "ghcr.io"; + var version = "latest"; + var arguments = new[] { "list-features", "--json" }; + var features = new List + { + new Feature + { + Name = "actions/cache", + Description = "Control usage of actions/cache inside of workflows. Outputs a comment if not enabled.", + Enabled = false, + EnvName = "FEATURE_ACTIONS_CACHE", + }, + new Feature + { + Name = "composite-actions", + Description = "Minimizes resulting workflow complexity through the use of composite actions. See https://docs.github.com/en/actions/creating-actions/creating-a-composite-action for more information.", + Enabled = true, + EnvName = "FEATURE_COMPOSITE_ACTIONS", + } + }; + var featuresJSON = JsonSerializer.Serialize(features); + + _processService.Setup(handler => + handler.RunAndCaptureAsync( + "docker", + $"run --rm -t {server}/{image}:{version} {string.Join(' ', arguments)}", + null, + null, + false, + null + ) + ).Returns(Task.FromResult((featuresJSON, "", 0))); + + // Act + var featuresResult = await _dockerService.GetFeaturesAsync(image, server, version); + var featuresResultJSON = JsonSerializer.Serialize(featuresResult); + + // Assert + Assert.AreEqual(featuresJSON, featuresResultJSON); + } + + [Test] + public async Task GetFeaturesAsync_BadJSONReturnsEmptyList() + { + // Arrange + var image = "actions-importer/cli"; + var server = "ghcr.io"; + var version = "latest"; + var arguments = new[] { "list-features", "--json" }; + + _processService.Setup(handler => + handler.RunAndCaptureAsync( + "docker", + $"run --rm -t {server}/{image}:{version} {string.Join(' ', arguments)}", + null, + null, + false, + null + ) + ).Returns(Task.FromResult(("", "", 0))); + + // Act + var featuresResult = await _dockerService.GetFeaturesAsync(image, server, version); + + // Assert + Assert.IsEmpty(featuresResult); + } + [Test] public void VerifyDockerRunningAsync_IsRunning_NoException() { diff --git a/src/ActionsImporter/App.cs b/src/ActionsImporter/App.cs index 98455e4a..1488daab 100644 --- a/src/ActionsImporter/App.cs +++ b/src/ActionsImporter/App.cs @@ -1,4 +1,5 @@ -using ActionsImporter.Interfaces; +using System.Collections.Immutable; +using ActionsImporter.Interfaces; using ActionsImporter.Models; namespace ActionsImporter; @@ -99,14 +100,34 @@ public async Task CheckForUpdatesAsync() } } - public async Task ConfigureAsync() + public async Task ConfigureAsync(string[] args) { var currentVariables = await _configurationService.ReadCurrentVariablesAsync().ConfigureAwait(false); - var newVariables = _configurationService.GetUserInput(); + ImmutableDictionary? newVariables; + + if (args.Contains($"--{Commands.Configure.OptionalFeaturesOption.Name}")) + { + await _dockerService.VerifyDockerRunningAsync().ConfigureAwait(false); + var availableFeatures = await _dockerService.GetFeaturesAsync(ActionsImporterImage, ActionsImporterContainerRegistry, ImageTag).ConfigureAwait(false); + try + { + newVariables = _configurationService.GetFeaturesInput(availableFeatures); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.Message); + return 1; + } + } + else + { + newVariables = _configurationService.GetUserInput(); + } + var mergedVariables = _configurationService.MergeVariables(currentVariables, newVariables); await _configurationService.WriteVariablesAsync(mergedVariables); - Console.WriteLine("Environment variables successfully updated."); + await Console.Out.WriteLineAsync("Environment variables successfully updated."); return 0; } } diff --git a/src/ActionsImporter/Commands/Configure.cs b/src/ActionsImporter/Commands/Configure.cs index fe110f85..090ded08 100644 --- a/src/ActionsImporter/Commands/Configure.cs +++ b/src/ActionsImporter/Commands/Configure.cs @@ -1,20 +1,32 @@ using System.CommandLine; -using System.CommandLine.NamingConventionBinder; +using ActionsImporter.Handlers; namespace ActionsImporter.Commands; public class Configure : BaseCommand { + private readonly string[] _args; protected override string Name => "configure"; protected override string Description => "Start an interactive prompt to configure credentials used to authenticate with your CI server(s)."; + public static readonly Option OptionalFeaturesOption = new(new[] { "--features" }) + { + Description = "Configure the feature flags for GitHub Actions Importer." + }; + + public Configure(string[] args) + { + _args = args; + } + protected override Command GenerateCommand(App app) { ArgumentNullException.ThrowIfNull(app); var command = base.GenerateCommand(app); - command.Handler = CommandHandler.Create(app.ConfigureAsync); + command.AddGlobalOption(OptionalFeaturesOption); + command.SetHandler(new ConfigureHandler(app).Run(_args)); return command; } diff --git a/src/ActionsImporter/Commands/ListFeatures.cs b/src/ActionsImporter/Commands/ListFeatures.cs index d0b8e5c0..d6b95fe3 100644 --- a/src/ActionsImporter/Commands/ListFeatures.cs +++ b/src/ActionsImporter/Commands/ListFeatures.cs @@ -12,5 +12,7 @@ public ListFeatures(string[] args) : base(args) protected override string Name => "list-features"; protected override string Description => "List the available feature flags for GitHub Actions Importer."; - protected override ImmutableArray