Skip to content

Commit ff46602

Browse files
authored
Naming templates (#129)
* refactor to support custom naming templates (ref #126) * cleaning Naming interface * refactored for custom naming template and initial set of unit tests * more unit tests * convert ARM template to use additional parameters * rename to better convey * fix ARM template sanity check * editorconfig, attributes and using clean-up * fixed implementation of FileNamingTemplates added more argument validation added unit tests * Integration test for naming template feature and some fixes and some doc notes * fix integration test cleanup should be moved to a special test command * factored out integration test cleanup that is delete all azure resources except for the resource group
1 parent 01dd13f commit ff46602

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+975
-187
lines changed

.editorconfig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_style = space
6+
insert_final_newline = true
7+
8+
[**.md]
9+
indent_size = 2
10+
trim_trailing_whitespace = false

doc/command-examples.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
11
# Sample Aggregator CLI usage
22

33
run `aggregator-cli.exe` (Windows), `aggregator-cli` (Linux) or `dotnet aggregator-cli.dll` followed by the command and any option.
4+
5+
6+
### Options valid for all command
47
All commands accept the `--verbose` option to print additional messages for troubleshooting.
58

9+
With `--namingTemplate`, you can specify affixes for all Azure resource that will be created or used.
10+
follows
11+
12+
```json
13+
{
14+
"ResourceGroupPrefix": "aggregator-",
15+
"ResourceGroupSuffix": "-sfx",
16+
"FunctionAppPrefix": "fp",
17+
"FunctionAppSuffix": "fs",
18+
"HostingPlanPrefix": "hp",
19+
"HostingPlanSuffix": "hs",
20+
"AppInsightPrefix": "aip",
21+
"AppInsightSuffix": "ais",
22+
"StorageAccountPrefix": "strg",
23+
"StorageAccountSuffix": "31415"
24+
}
25+
```
26+
27+
If you use this, the `--resourceGroup` option is mandatory.
28+
629

730
### Logon
831
You are required to log into both Azure and ADO. The credentials are cached locally and expire after 2 hours. _(Replace the below asterisks `*` with valid values.)_
@@ -181,4 +204,4 @@ Runs existing rule in Azure, no change is sent to Azure DevOps thanks to the `dr
181204
invoke.rule --instance my7 --resourceGroup test-aggregator7 --name r1 --event workitem.created --account giuliovaad --project WorkItemTracking --workItemId 14 --verbose --dryrun
182205
```
183206
If you want to see the log messages, run a `stream.logs` command in another window.
184-
This can be used (without the `dryrun` flag) to apply rules to existing work items.
207+
This can be used (without the `dryrun` flag) to apply rules to existing work items.

src/.editorconfig

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
2+
###############################
3+
# Core EditorConfig Options #
4+
###############################
5+
# All files
6+
[*]
7+
indent_style = space
8+
# Code files
9+
[*.{cs,csx}]
10+
indent_size = 4
11+
insert_final_newline = true
12+
charset = utf-8-bom
13+
###############################
14+
# .NET Coding Conventions #
15+
###############################
16+
[*.{cs,vb}]
17+
# Organize usings
18+
dotnet_sort_system_directives_first = true
19+
# this. preferences
20+
dotnet_style_qualification_for_field = false:silent
21+
dotnet_style_qualification_for_property = false:silent
22+
dotnet_style_qualification_for_method = false:silent
23+
dotnet_style_qualification_for_event = false:silent
24+
# Language keywords vs BCL types preferences
25+
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
26+
dotnet_style_predefined_type_for_member_access = true:silent
27+
# Parentheses preferences
28+
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
29+
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
30+
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
31+
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
32+
# Modifier preferences
33+
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
34+
dotnet_style_readonly_field = true:suggestion
35+
# Expression-level preferences
36+
dotnet_style_object_initializer = true:suggestion
37+
dotnet_style_collection_initializer = true:suggestion
38+
dotnet_style_explicit_tuple_names = true:suggestion
39+
dotnet_style_null_propagation = true:suggestion
40+
dotnet_style_coalesce_expression = true:suggestion
41+
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
42+
dotnet_style_prefer_inferred_tuple_names = true:suggestion
43+
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
44+
dotnet_style_prefer_auto_properties = true:silent
45+
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
46+
dotnet_style_prefer_conditional_expression_over_return = true:silent
47+
###############################
48+
# Naming Conventions #
49+
###############################
50+
# Style Definitions
51+
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
52+
# Use PascalCase for constant fields
53+
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
54+
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
55+
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
56+
dotnet_naming_symbols.constant_fields.applicable_kinds = field
57+
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
58+
dotnet_naming_symbols.constant_fields.required_modifiers = const
59+
###############################
60+
# C# Coding Conventions #
61+
###############################
62+
[*.cs]
63+
# var preferences
64+
csharp_style_var_for_built_in_types = true:silent
65+
csharp_style_var_when_type_is_apparent = true:silent
66+
csharp_style_var_elsewhere = true:silent
67+
# Expression-bodied members
68+
csharp_style_expression_bodied_methods = false:silent
69+
csharp_style_expression_bodied_constructors = false:silent
70+
csharp_style_expression_bodied_operators = false:silent
71+
csharp_style_expression_bodied_properties = true:silent
72+
csharp_style_expression_bodied_indexers = true:silent
73+
csharp_style_expression_bodied_accessors = true:silent
74+
# Pattern matching preferences
75+
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
76+
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
77+
# Null-checking preferences
78+
csharp_style_throw_expression = true:suggestion
79+
csharp_style_conditional_delegate_call = true:suggestion
80+
# Modifier preferences
81+
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
82+
# Expression-level preferences
83+
csharp_prefer_braces = true:silent
84+
csharp_style_deconstructed_variable_declaration = true:suggestion
85+
csharp_prefer_simple_default_expression = true:suggestion
86+
csharp_style_pattern_local_over_anonymous_function = true:suggestion
87+
csharp_style_inlined_variable_declaration = true:suggestion
88+
###############################
89+
# C# Formatting Rules #
90+
###############################
91+
# New line preferences
92+
csharp_new_line_before_open_brace = all
93+
csharp_new_line_before_else = true
94+
csharp_new_line_before_catch = true
95+
csharp_new_line_before_finally = true
96+
csharp_new_line_before_members_in_object_initializers = true
97+
csharp_new_line_before_members_in_anonymous_types = true
98+
csharp_new_line_between_query_expression_clauses = true
99+
# Indentation preferences
100+
csharp_indent_case_contents = true
101+
csharp_indent_switch_labels = true
102+
csharp_indent_labels = flush_left
103+
# Space preferences
104+
csharp_space_after_cast = false
105+
csharp_space_after_keywords_in_control_flow_statements = true
106+
csharp_space_between_method_call_parameter_list_parentheses = false
107+
csharp_space_between_method_declaration_parameter_list_parentheses = false
108+
csharp_space_between_parentheses = false
109+
csharp_space_before_colon_in_inheritance_clause = true
110+
csharp_space_after_colon_in_inheritance_clause = true
111+
csharp_space_around_binary_operators = before_and_after
112+
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
113+
csharp_space_between_method_call_name_and_opening_parenthesis = false
114+
csharp_space_between_method_call_empty_parameter_list_parentheses = false
115+
# Wrapping preferences
116+
csharp_preserve_single_line_statements = true
117+
csharp_preserve_single_line_blocks = true

src/aggregator-cli/CommandBase.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ abstract class CommandBase
1616
[Option('v', "verbose", Default = false, HelpText = "Prints all messages to standard output.")]
1717
public bool Verbose { get; set; }
1818

19-
protected ContextBuilder Context => new ContextBuilder(Logger);
19+
[Option("namingTemplate", Default = "", HelpText = "Define template-set for generating names of Azure Resources.")]
20+
public string NamingTemplate { get; set; }
21+
22+
protected ContextBuilder Context => new ContextBuilder(Logger, this.NamingTemplate);
2023

2124
internal ILogger Logger { get; private set; }
2225

src/aggregator-cli/ContextBuilder.cs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.VisualStudio.Services.WebApi;
33
using System;
44
using System.Collections.Generic;
5+
using System.IO;
56
using System.Text;
67
using System.Threading;
78
using System.Threading.Tasks;
@@ -14,21 +15,28 @@ internal class CommandContext
1415
internal ILogger Logger { get; }
1516
internal IAzure Azure { get; }
1617
internal VssConnection Devops { get; }
17-
internal CommandContext(ILogger logger, IAzure azure, VssConnection devops)
18+
internal INamingTemplates Naming { get; }
19+
internal CommandContext(ILogger logger, IAzure azure, VssConnection devops, INamingTemplates naming)
1820
{
1921
Logger = logger;
2022
Azure = azure;
2123
Devops = devops;
24+
Naming = naming;
2225
}
2326
}
2427

2528
internal class ContextBuilder
2629
{
30+
private readonly string namingTemplate;
2731
private readonly ILogger logger;
2832
private bool azureLogon;
2933
private bool devopsLogon;
3034

31-
internal ContextBuilder(ILogger logger) => this.logger = logger;
35+
internal ContextBuilder(ILogger logger, string namingTemplate)
36+
{
37+
this.logger = logger;
38+
this.namingTemplate = namingTemplate;
39+
}
3240

3341
internal ContextBuilder WithAzureLogon()
3442
{
@@ -75,7 +83,21 @@ internal async Task<CommandContext> BuildAsync(CancellationToken cancellationTok
7583
logger.WriteInfo($"Connected to {devops.Uri.Host}");
7684
}
7785

78-
return new CommandContext(logger, azure, devops);
86+
INamingTemplates naming = null;
87+
switch (namingTemplate.ToLower())
88+
{
89+
case "builtin":
90+
goto case "";
91+
case "":
92+
naming = new BuiltInNamingTemplates();
93+
break;
94+
default:
95+
// implement custom Naming Templates, e.g. reading from a file
96+
naming = new FileNamingTemplates(File.ReadAllText(namingTemplate));
97+
break;
98+
}
99+
100+
return new CommandContext(logger, azure, devops, naming);
79101
}
80102

81103
private string TranslateResult(LogonResult reason)

src/aggregator-cli/GitHubVersionResponse.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
using Microsoft.Extensions.Logging;
2-
using Microsoft.TeamFoundation.TestManagement.WebApi.Legacy;
1+
using System;
32
using Newtonsoft.Json;
4-
using System;
5-
using System.Collections.Generic;
6-
using System.IO;
7-
using System.Text;
83

94
namespace aggregator.cli
105
{

src/aggregator-cli/Instances/AggregatorInstances.cs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,24 @@ namespace aggregator.cli
1515
{
1616
class AggregatorInstances : AzureBaseClass
1717
{
18+
private readonly INamingTemplates naming;
1819

19-
public AggregatorInstances(IAzure azure, ILogger logger) : base(azure, logger)
20-
{ }
20+
public AggregatorInstances(IAzure azure, ILogger logger, INamingTemplates naming)
21+
: base(azure, logger)
22+
{
23+
this.naming = naming;
24+
}
2125

2226
public async Task<IEnumerable<ILogDataObject>> ListAllAsync(CancellationToken cancellationToken)
2327
{
2428
var runtime = new FunctionRuntimePackage(logger);
2529
var rgs = await azure.ResourceGroups.ListAsync(cancellationToken: cancellationToken);
2630
var filter = rgs
27-
.Where(rg => rg.Name.StartsWith(InstanceName.ResourceGroupInstancePrefix));
31+
.Where(rg => naming.ResourceGroupMatches(rg));
2832
var result = new List<InstanceOutputData>();
2933
foreach (var rg in filter)
3034
{
31-
var name = InstanceName.FromResourceGroupName(rg.Name);
35+
var name = naming.FromResourceGroupName(rg.Name);
3236
result.Add(new InstanceOutputData(
3337
name.PlainName,
3438
rg.RegionName,
@@ -42,13 +46,13 @@ public async Task<IEnumerable<ILogDataObject>> ListByLocationAsync(string locati
4246
{
4347
var runtime = new FunctionRuntimePackage(logger);
4448
var rgs = await azure.ResourceGroups.ListAsync(cancellationToken: cancellationToken);
45-
var filter = rgs.Where(rg =>
46-
rg.Name.StartsWith(InstanceName.ResourceGroupInstancePrefix)
47-
&& string.Compare(rg.RegionName, location, StringComparison.Ordinal) == 0);
49+
var filter = rgs.Where(
50+
rg => naming.ResourceGroupMatches(rg)
51+
&& string.Compare(rg.RegionName, location, StringComparison.Ordinal) == 0);
4852
var result = new List<InstanceOutputData>();
4953
foreach (var rg in filter)
5054
{
51-
var name = InstanceName.FromResourceGroupName(rg.Name);
55+
var name = naming.FromResourceGroupName(rg.Name);
5256
result.Add(new InstanceOutputData(
5357
name.PlainName,
5458
rg.RegionName,
@@ -61,13 +65,14 @@ await runtime.GetDeployedRuntimeVersion(name, azure, cancellationToken))
6165
internal async Task<IEnumerable<ILogDataObject>> ListInResourceGroupAsync(string resourceGroup, CancellationToken cancellationToken)
6266
{
6367
var runtime = new FunctionRuntimePackage(logger);
64-
var apps = await azure.AppServices.FunctionApps.ListByResourceGroupAsync(resourceGroup, cancellationToken: cancellationToken);
68+
string rgName = naming.GetResourceGroupName(resourceGroup);
69+
var apps = await azure.AppServices.FunctionApps.ListByResourceGroupAsync(rgName, cancellationToken: cancellationToken);
6570

6671
var result = new List<InstanceOutputData>();
6772
foreach (var app in apps)
6873
{
6974
cancellationToken.ThrowIfCancellationRequested();
70-
var name = InstanceName.FromFunctionAppName(app.Name, resourceGroup);
75+
var name = naming.FromFunctionAppName(app.Name, resourceGroup);
7176
result.Add(new InstanceOutputData(
7277
name.PlainName,
7378
app.Region.Name,
@@ -83,7 +88,7 @@ private static T GetCustomAttribute<T>()
8388
return Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(T), false).FirstOrDefault() as T;
8489
}
8590

86-
internal async Task<bool> AddAsync(InstanceName instance, string location, string requiredVersion, string sourceUrl, InstanceFineTuning tuning, CancellationToken cancellationToken)
91+
internal async Task<bool> AddAsync(InstanceCreateNames instance, string location, string requiredVersion, string sourceUrl, InstanceFineTuning tuning, CancellationToken cancellationToken)
8792
{
8893
string rgName = instance.ResourceGroupName;
8994
bool ok = await MakeSureResourceGroupExistsAsync(instance.IsCustom, location, rgName, cancellationToken);
@@ -151,7 +156,7 @@ internal class InstanceFineTuning
151156
public string AppInsightLocation { get; set; }
152157
}
153158

154-
private async Task<bool> DeployArmTemplateAsync(InstanceName instance, string location, string rgName, InstanceFineTuning tuning, CancellationToken cancellationToken)
159+
private async Task<bool> DeployArmTemplateAsync(InstanceCreateNames instance, string location, string rgName, InstanceFineTuning tuning, CancellationToken cancellationToken)
155160
{
156161
// IDEA the template should create a Storage account and/or a Key Vault for Rules' use
157162
// TODO https://github.com/gjlumsden/AzureFunctionsSlots suggest that slots must be created in template
@@ -166,21 +171,25 @@ private async Task<bool> DeployArmTemplateAsync(InstanceName instance, string lo
166171

167172
var parsedTemplate = JObject.Parse(armTemplateString);
168173
// sanity checks
169-
if (parsedTemplate.SelectToken("parameters.appName") == null)
174+
if (parsedTemplate.SelectToken("parameters.aggregatorVersion") == null)
170175
{
171176
// not good, blah
172177
logger.WriteWarning($"Something is wrong with the ARM template");
173178
return false;
174179
}
175180

176-
string appName = instance.FunctionAppName;
177181
var infoVersion = GetCustomAttribute<AssemblyInformationalVersionAttribute>();
178182
var templateParams = new Dictionary<string, Dictionary<string, object>>{
179183
// TODO give use more control by setting more parameters
180184
{"webLocation", new Dictionary<string, object>{{"value", location } }},
181185
{"aiLocation", new Dictionary<string, object>{{"value", tuning.AppInsightLocation } }},
182186
{"storageAccountType", new Dictionary<string, object>{{"value", "Standard_LRS" } }},
183-
{"appName", new Dictionary<string, object>{{"value", appName } }},
187+
188+
{"functionAppName", new Dictionary<string, object>{{"value", instance.FunctionAppName } }},
189+
{"storageAccountName", new Dictionary<string, object>{{"value", instance.StorageAccountName } }},
190+
{"hostingPlanName", new Dictionary<string, object>{{"value", instance.HostingPlanName } }},
191+
{"appInsightName", new Dictionary<string, object>{{"value", instance.AppInsightName } }},
192+
184193
{"aggregatorVersion", new Dictionary<string, object>{{"value", infoVersion.InformationalVersion } }},
185194
{"hostingPlanSkuName", new Dictionary<string, object>{{"value", tuning.HostingPlanSku } }},
186195
{"hostingPlanSkuTier", new Dictionary<string, object>{{"value", tuning.HostingPlanTier } }},

src/aggregator-cli/Instances/ConfigureInstanceCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ internal override async Task<int> RunAsync(CancellationToken cancellationToken)
3434
.WithAzureLogon()
3535
.WithDevOpsLogon() // need the token, so we can save it in the app settings
3636
.BuildAsync(cancellationToken);
37-
var instances = new AggregatorInstances(context.Azure, context.Logger);
38-
var instance = new InstanceName(Name, ResourceGroup);
37+
var instances = new AggregatorInstances(context.Azure, context.Logger, context.Naming);
38+
var instance = context.Naming.Instance(Name, ResourceGroup);
3939
bool ok = false;
4040
if (Authentication)
4141
{

src/aggregator-cli/Instances/InstallInstanceCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ internal override async Task<int> RunAsync(CancellationToken cancellationToken)
6262
.WithAzureLogon()
6363
.WithDevOpsLogon() // need the token, so we can save it in the app settings
6464
.BuildAsync(cancellationToken);
65-
var instances = new AggregatorInstances(context.Azure, context.Logger);
66-
var instance = new InstanceName(Name, ResourceGroup);
65+
var instances = new AggregatorInstances(context.Azure, context.Logger, context.Naming);
66+
var instance = context.Naming.GetInstanceCreateNames(Name, ResourceGroup);
6767
bool ok = await instances.AddAsync(instance, Location, RequiredVersion, SourceUrl, tuning, cancellationToken);
6868
return ok ? 0 : 1;
6969
}

0 commit comments

Comments
 (0)