Skip to content

Commit

Permalink
Add support for sourcing feature flags from bicepconfig.json and CLI …
Browse files Browse the repository at this point in the history
…args.

CLI args override environment variables, which override bicepconfig.json settings
  • Loading branch information
jeskew committed Oct 3, 2022
1 parent 1019838 commit 5b447a5
Show file tree
Hide file tree
Showing 100 changed files with 953 additions and 383 deletions.
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
},
"sdk": {
"allowPrerelease": false,
"version": "6.0.400",
"version": "6.0.100",
"rollForward": "latestFeature"
}
}
23 changes: 9 additions & 14 deletions src/Bicep.Cli.IntegrationTests/RootCommandTests.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Bicep.Core.Features;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

namespace Bicep.Cli.IntegrationTests
{
[TestClass]
public class RootCommandTests : TestBase
{
[NotNull]
public TestContext? TestContext { get; set; }

[TestMethod]
public async Task Build_WithWrongArgs_ShouldFail_WithExpectedErrorMessage()
{
Expand All @@ -25,7 +29,7 @@ public async Task Build_WithWrongArgs_ShouldFail_WithExpectedErrorMessage()
output.Should().BeEmpty();

error.Should().NotBeEmpty();
error.Should().Contain($"Unrecognized arguments \"wrong fake broken\" specified. Use \"bicep --help\" to view available options.");
error.Should().Contain("Unrecognized arguments \"wrong fake broken");
}
}

Expand All @@ -47,10 +51,7 @@ public async Task BicepVersionShouldPrintVersionInformation()
[TestMethod]
public async Task BicepHelpShouldPrintHelp()
{
var featuresMock = Repository.Create<IFeatureProvider>();
featuresMock.Setup(m => m.RegistryEnabled).Returns(true);

var settings = CreateDefaultSettings() with { Features = featuresMock.Object };
var settings = CreateDefaultSettings() with { Features = BicepTestConstants.CreateFeatureProvider(TestContext, registryEnabled: true) };

var (output, error, result) = await Bicep(settings, "--help");

Expand Down Expand Up @@ -130,10 +131,7 @@ public async Task BicepThirdPartyNoticesShouldPrintNotices()
[TestMethod]
public async Task BicepHelpShouldIncludePublishWhenRegistryEnabled()
{
var featuresMock = Repository.Create<IFeatureProvider>();
featuresMock.Setup(m => m.RegistryEnabled).Returns(true);

var settings = CreateDefaultSettings() with { Features = featuresMock.Object };
var settings = CreateDefaultSettings() with { Features = BicepTestConstants.CreateFeatureProvider(TestContext, registryEnabled: true) };

var (output, error, result) = await Bicep(settings, "--help");

Expand All @@ -154,10 +152,7 @@ public async Task BicepHelpShouldIncludePublishWhenRegistryEnabled()
[TestMethod]
public async Task BicepHelpShouldNotIncludePublishWhenRegistryDisabled()
{
var featuresMock = Repository.Create<IFeatureProvider>();
featuresMock.Setup(m => m.RegistryEnabled).Returns(false);

var settings = CreateDefaultSettings() with { Features = featuresMock.Object };
var settings = CreateDefaultSettings() with { Features = BicepTestConstants.CreateFeatureProvider(TestContext, registryEnabled: false) };

var (output, error, result) = await Bicep(settings, "--help");

Expand Down
31 changes: 22 additions & 9 deletions src/Bicep.Cli.IntegrationTests/TestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
using Bicep.Core.FileSystem;
using Bicep.Core.Registry;
using Bicep.Core.Semantics;
using Bicep.Core.Semantics.Namespaces;
using Bicep.Core.Text;
using Bicep.Core.UnitTests;
using Bicep.Core.UnitTests.Utils;
using Bicep.Core.Workspaces;
using FluentAssertions;
using Moq;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;

Expand Down Expand Up @@ -41,9 +41,8 @@ protected record InvocationSettings(IFeatureProvider Features, IContainerRegistr
TestTypeHelper.CreateEmptyAzResourceTypeLoader(),
@out,
err,
features: settings.Features,
clientFactory: settings.ClientFactory,
templateSpecRepositoryFactory: settings.TemplateSpecRepositoryFactory)).RunAsync(args));
templateSpecRepositoryFactory: settings.TemplateSpecRepositoryFactory)).RunAsync(args.Concat(ToFeatureArgs(settings.Features)).ToArray()));

protected static void AssertNoErrors(string error)
{
Expand All @@ -55,9 +54,9 @@ protected static void AssertNoErrors(string error)

protected static IEnumerable<string> GetAllDiagnostics(string bicepFilePath, IContainerRegistryClientFactory clientFactory, ITemplateSpecRepositoryFactory templateSpecRepositoryFactory)
{
var dispatcher = new ModuleDispatcher(new DefaultModuleRegistryProvider(BicepTestConstants.FileResolver, clientFactory, templateSpecRepositoryFactory, BicepTestConstants.Features, BicepTestConstants.BuiltInOnlyConfigurationManager), BicepTestConstants.BuiltInOnlyConfigurationManager);
var dispatcher = new ModuleDispatcher(new DefaultModuleRegistryProvider(BicepTestConstants.FileResolver, clientFactory, templateSpecRepositoryFactory, BicepTestConstants.FeatureProviderFactory, BicepTestConstants.BuiltInOnlyConfigurationManager), BicepTestConstants.BuiltInOnlyConfigurationManager);
var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, dispatcher, new Workspace(), PathHelper.FilePathToFileUrl(bicepFilePath));
var compilation = new Compilation(BicepTestConstants.Features, TestTypeHelper.CreateEmptyProvider(), sourceFileGrouping, BicepTestConstants.BuiltInOnlyConfigurationManager, BicepTestConstants.ApiVersionProvider, BicepTestConstants.LinterAnalyzer);
var compilation = new Compilation(BicepTestConstants.FeatureProviderFactory, TestTypeHelper.CreateEmptyProvider(), sourceFileGrouping, BicepTestConstants.BuiltInOnlyConfigurationManager, BicepTestConstants.ApiVersionProviderFactory, BicepTestConstants.LinterAnalyzer);

var output = new List<string>();
foreach (var (bicepFile, diagnostics) in compilation.GetAllDiagnosticsByBicepFile())
Expand All @@ -76,15 +75,17 @@ protected static IEnumerable<string> GetAllDiagnostics(string bicepFilePath, ICo
protected static IEnumerable<string> GetAllParamDiagnostics(string paramFilePath, IContainerRegistryClientFactory clientFactory, ITemplateSpecRepositoryFactory templateSpecRepositoryFactory)
{
var configuration = BicepTestConstants.BuiltInConfiguration;
var dispatcher = new ModuleDispatcher(new DefaultModuleRegistryProvider(BicepTestConstants.FileResolver, clientFactory, templateSpecRepositoryFactory, BicepTestConstants.Features, BicepTestConstants.BuiltInOnlyConfigurationManager), BicepTestConstants.BuiltInOnlyConfigurationManager);
var features = BicepTestConstants.Features;
var featureManager = BicepTestConstants.FeatureProviderFactory;
var dispatcher = new ModuleDispatcher(new DefaultModuleRegistryProvider(BicepTestConstants.FileResolver, clientFactory, templateSpecRepositoryFactory, featureManager, BicepTestConstants.BuiltInOnlyConfigurationManager), BicepTestConstants.BuiltInOnlyConfigurationManager);
var sourceFileGrouping = SourceFileGroupingBuilder.Build(BicepTestConstants.FileResolver, dispatcher, new Workspace(), PathHelper.FilePathToFileUrl(paramFilePath));
var compilation = new Compilation(BicepTestConstants.Features, TestTypeHelper.CreateEmptyProvider(), sourceFileGrouping, BicepTestConstants.BuiltInOnlyConfigurationManager, BicepTestConstants.ApiVersionProvider, BicepTestConstants.LinterAnalyzer);
var compilation = new Compilation(featureManager, TestTypeHelper.CreateEmptyProvider(), sourceFileGrouping, BicepTestConstants.BuiltInOnlyConfigurationManager, BicepTestConstants.ApiVersionProviderFactory, BicepTestConstants.LinterAnalyzer);

var semanticModel = new ParamsSemanticModel(sourceFileGrouping, configuration, BicepTestConstants.Features, file => {
var semanticModel = new ParamsSemanticModel(sourceFileGrouping, configuration, features, file => {
var compilationGrouping = new SourceFileGrouping(BicepTestConstants.FileResolver, file.FileUri, sourceFileGrouping.FileResultByUri, sourceFileGrouping.UriResultByModule, sourceFileGrouping.SourceFileParentLookup);


return new Compilation(BicepTestConstants.Features, BicepTestConstants.NamespaceProvider, compilationGrouping, BicepTestConstants.BuiltInOnlyConfigurationManager, BicepTestConstants.ApiVersionProvider, BicepTestConstants.LinterAnalyzer);
return new Compilation(featureManager, BicepTestConstants.NamespaceProvider, compilationGrouping, BicepTestConstants.BuiltInOnlyConfigurationManager, BicepTestConstants.ApiVersionProviderFactory, BicepTestConstants.LinterAnalyzer);
});

var output = new List<string>();
Expand All @@ -97,5 +98,17 @@ protected static IEnumerable<string> GetAllParamDiagnostics(string paramFilePath

return output;
}

private static string[] ToFeatureArgs(IFeatureProvider features) => new[]
{
"--cache-root-directory", features.CacheRootDirectory,
"--enable-registry", features.RegistryEnabled.ToString(),
"--enable-symbolic-name-codegen", features.SymbolicNameCodegenEnabled.ToString(),
"--enable-imports", features.ImportsEnabled.ToString(),
"--enable-resource-typed-params-and-outputs", features.ResourceTypedParamsAndOutputsEnabled.ToString(),
"--enable-source-mapping", features.SourceMappingEnabled.ToString(),
"--enable-params-files", features.ParamsFilesEnabled.ToString(),
"--assembly-version-override", features.AssemblyVersion,
};
}
}
96 changes: 95 additions & 1 deletion src/Bicep.Cli/Arguments/ArgumentsBase.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,109 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using Bicep.Core.Features;

namespace Bicep.Cli.Arguments
{
public abstract class ArgumentsBase
public abstract class ArgumentsBase : IFeatureProviderSource
{
private delegate int ArgHandler(ArgumentsBase instance, string[] args, int position);
private static readonly IReadOnlyDictionary<string, ArgHandler> FeatureArgHandlers = new Dictionary<string, ArgHandler>
{
{
"--cache-root-directory",
(i, a, p) => HandleParameterWithSingleArg(i, a, p, instance => instance.CacheRootDirectory, (instance, result) => instance.CacheRootDirectory = result)
},
{
"--enable-registry",
(i, a, p) => HandleSwitchWithOptionalBooleanArg(i, a, p, instance => instance.RegistryEnabled, (instance, result) => instance.RegistryEnabled = result)
},
{
"--enable-symbolic-name-codegen",
(i, a, p) => HandleSwitchWithOptionalBooleanArg(i, a, p, instance => instance.SymbolicNameCodegenEnabled, (instance, result) => instance.SymbolicNameCodegenEnabled = result)
},
{
"--enable-imports",
(i, a, p) => HandleSwitchWithOptionalBooleanArg(i, a, p, instance => instance.ImportsEnabled, (instance, result) => instance.ImportsEnabled = result)
},
{
"--enable-resource-typed-params-and-outputs",
(i, a, p) => HandleSwitchWithOptionalBooleanArg(i, a, p, instance => instance.ResourceTypedParamsAndOutputsEnabled, (instance, result) => instance.ResourceTypedParamsAndOutputsEnabled = result)
},
{
"--enable-source-mapping",
(i, a, p) => HandleSwitchWithOptionalBooleanArg(i, a, p, instance => instance.SourceMappingEnabled, (instance, result) => instance.SourceMappingEnabled = result)
},
{
"--enable-params-files",
(i, a, p) => HandleSwitchWithOptionalBooleanArg(i, a, p, instance => instance.ParamsFilesEnabled, (instance, result) => instance.ParamsFilesEnabled = result)
},
{
"--assembly-version-override",
(i, a, p) => HandleParameterWithSingleArg(i, a, p, instance => instance.AssemblyVersion, (instance, result) => instance.AssemblyVersion = result)
}
};


public string CommandName { get; }

sbyte IFeatureProviderSource.Priority => -1;

public string? AssemblyVersion { get; private set; }

public string? CacheRootDirectory { get; private set; }

public bool? RegistryEnabled { get; private set; }

public bool? SymbolicNameCodegenEnabled { get; private set; }

public bool? ImportsEnabled { get; private set; }

public bool? ResourceTypedParamsAndOutputsEnabled { get; private set; }

public bool? SourceMappingEnabled { get; private set; }

public bool? ParamsFilesEnabled { get; private set; }

protected ArgumentsBase(string commandName)
{
CommandName = commandName;
}

protected static bool IsFeatureArg(string arg) => FeatureArgHandlers.ContainsKey(arg);
protected int HandleFeatureArg(string[] args, int position) => FeatureArgHandlers[args[position]].Invoke(this, args, position);

private static int HandleParameterWithSingleArg(ArgumentsBase instance, string[] args, int position, Func<ArgumentsBase, string?> prevSettingGetter, Action<ArgumentsBase, string> paramSetter)
{
if (args.Length == position + 1)
{
throw new CommandLineException($"The {args[position]} parameter expects an argument");
}
if (prevSettingGetter.Invoke(instance) is not null)
{
throw new CommandLineException($"The {args[position]} parameter cannot be specified twice");
}
paramSetter.Invoke(instance, args[position + 1]);
return 1;
}

private static int HandleSwitchWithOptionalBooleanArg(ArgumentsBase instance, string[] args, int position, Func<ArgumentsBase, bool?> prevSettingGetter, Action<ArgumentsBase, bool> flagSetter)
{
if (prevSettingGetter.Invoke(instance) is not null)
{
throw new CommandLineException($"The {args[position]} parameter cannot be specified twice");
}

if (args.Length > position + 1 && args[position + 1] is string maybeBoolArg && bool.TryParse(maybeBoolArg, out var result))
{
flagSetter(instance, result);
return 1;
}

flagSetter(instance, true);
return 0;
}
}
}
4 changes: 4 additions & 0 deletions src/Bicep.Cli/Arguments/BuildArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ public BuildArguments(string[] args) : base(Constants.Command.Build)
i++;
break;

case string maybeFeatureArg when IsFeatureArg(maybeFeatureArg):
i += HandleFeatureArg(args, i);
break;

default:
if (args[i].StartsWith("--"))
{
Expand Down
3 changes: 3 additions & 0 deletions src/Bicep.Cli/Arguments/DecompileArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public DecompileArguments(string[] args) : base(Constants.Command.Decompile)
OutputFile = args[i + 1];
i++;
break;
case string maybeFeatureArg when IsFeatureArg(maybeFeatureArg):
i += HandleFeatureArg(args, i);
break;
default:
if (args[i].StartsWith("--"))
{
Expand Down
4 changes: 4 additions & 0 deletions src/Bicep.Cli/Arguments/GenerateParametersFileArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ public GenerateParametersFileArguments(string[] args) : base(Constants.Command.G
i++;
break;

case string maybeFeatureArg when IsFeatureArg(maybeFeatureArg):
i += HandleFeatureArg(args, i);
break;

default:
if (args[i].StartsWith("--"))
{
Expand Down
4 changes: 4 additions & 0 deletions src/Bicep.Cli/Arguments/PublishArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public PublishArguments(string[] args) : base(Constants.Command.Publish)
i++;
break;

case string maybeFeatureArg when IsFeatureArg(maybeFeatureArg):
i += HandleFeatureArg(args, i);
break;

default:
if (args[i].StartsWith("--"))
{
Expand Down
14 changes: 9 additions & 5 deletions src/Bicep.Cli/Arguments/RestoreArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,30 @@ public class RestoreArguments : ArgumentsBase
{
public RestoreArguments(string[] args) : base(Constants.Command.Restore)
{
foreach (var argument in args)
for (int i = 0; i < args.Length; i++)
{
switch (argument.ToLowerInvariant())
switch (args[i].ToLowerInvariant())
{
case "--force":
ForceModulesRestore = true;
break;

case string maybeFeatureArg when IsFeatureArg(maybeFeatureArg):
i += HandleFeatureArg(args, i);
break;

default:
if (argument.StartsWith("--"))
if (args[i].StartsWith("--"))
{
throw new CommandLineException($"Unrecognized parameter \"{argument}\"");
throw new CommandLineException($"Unrecognized parameter \"{args[i]}\"");
}

if (InputFile is not null)
{
throw new CommandLineException($"The input file path cannot be specified multiple times.");
}

InputFile = argument;
InputFile = args[i];
break;
}
}
Expand Down
19 changes: 18 additions & 1 deletion src/Bicep.Cli/Arguments/RootArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Bicep.Cli.Arguments
{
public class RootArguments : ArgumentsBase
{
public RootArguments(string arg, string commandName) : base(commandName)
public RootArguments(string arg, string commandName, string[] additionalArgs) : base(commandName)
{
switch (arg)
{
Expand All @@ -27,6 +27,23 @@ public RootArguments(string arg, string commandName) : base(commandName)
PrintThirdPartyNotices = true;
break;
};

for (var i = 0; i < additionalArgs.Length; i++)
{
switch (additionalArgs[i].ToLowerInvariant())
{
case string maybeFeatureArg when IsFeatureArg(maybeFeatureArg):
i += HandleFeatureArg(additionalArgs, i);
break;

default:
if (additionalArgs[i].StartsWith("--"))
{
throw new CommandLineException($"Unrecognized parameter \"{additionalArgs[i]}\"");
}
break;
}
}
}

public bool PrintHelp { get; }
Expand Down
Loading

0 comments on commit 5b447a5

Please sign in to comment.