Skip to content

Commit

Permalink
Merge pull request #264 from tfsaggregator/feature/255
Browse files Browse the repository at this point in the history
Feature/255
  • Loading branch information
giuliov authored May 11, 2022
2 parents 95a12bc + cc18cdd commit 1c2ed2f
Show file tree
Hide file tree
Showing 76 changed files with 1,985 additions and 727 deletions.
8 changes: 6 additions & 2 deletions Next-Release-ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ This release has a few fixes and a new feature.

CLI Commands and Options
========================
Fixes upgrading a previous instance to the latest run-time (PR #263).
* Fixes upgrading a previous instance to the latest run-time (PR #263).
* New algorithm to generate unique Azure resource names.


Docker and Azure Function Hosting
Expand All @@ -13,7 +14,8 @@ No changes.

Rule Language
========================
No changes.
* New pre-defined constant `ruleName`, returns the name of executing rule.
* New `store.TransitionToState` method to change the state of a Work Item (see #255).


Rule Interpreter Engine
Expand All @@ -24,6 +26,8 @@ No changes.
Build, Test, Documentation
========================
* Renamed `aggregator-cli.sln` to `aggregator3.sln`.
* Moved assembly properties, in particoular `VersionPrefix` and `VersionSuffix`, to common `Directory.Build.props` file.
* Added tests to upgrade from an older version.


File Hashes
Expand Down
9 changes: 9 additions & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<Company>TFS Aggregator Team</Company>
<Product>Aggregator 3</Product>
<Copyright>Copyright © TFS Aggregator Team</Copyright>
<VersionPrefix>1.3.0</VersionPrefix>
<VersionSuffix>beta.1</VersionSuffix>
</PropertyGroup>
</Project>
9 changes: 5 additions & 4 deletions src/aggregator-cli/AzureBaseClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@

using Microsoft.Azure.Management.AppService.Fluent;
using Microsoft.Azure.Management.Fluent;

using Microsoft.Azure.Management.ResourceManager.Fluent;

namespace aggregator.cli
{
internal abstract class AzureBaseClass
{
protected IAzure _azure;

protected IResourceManagementClient _azureManagement;
protected ILogger _logger;

protected AzureBaseClass(IAzure azure, ILogger logger)
protected AzureBaseClass(IAzure azure, ILogger logger, IResourceManagementClient azureManagement = null)
{
_azure = azure;
_azureManagement = azureManagement;
_logger = logger;
}

Expand All @@ -41,4 +42,4 @@ protected KuduApi GetKudu(InstanceName instance)
return kudu;
}
}
}
}
79 changes: 59 additions & 20 deletions src/aggregator-cli/CommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@

namespace aggregator.cli
{
internal enum ReturnType
{
ExitCode,
SuccessBooleanPlusIntegerValue
}

abstract class CommandBase
{
[ShowInTelemetry]
Expand All @@ -25,7 +31,13 @@ abstract class CommandBase

internal abstract Task<int> RunAsync(CancellationToken cancellationToken);

internal int Run(CancellationToken cancellationToken)
// virtual because it is the exception not the norm, don't want to force every command with a dummy implementation
internal virtual async Task<(bool success, int returnCode)> RunWithReturnAsync(CancellationToken cancellationToken)
{
return (true, 0);
}

internal (bool success, int returnCode) Run(CancellationToken cancellationToken, ReturnType returnMode = ReturnType.ExitCode)
{
var thisCommandName = this.GetType().GetCustomAttribute<VerbAttribute>().Name;

Expand Down Expand Up @@ -61,31 +73,58 @@ internal int Run(CancellationToken cancellationToken)
// Hello World
Logger.WriteInfo($"{title.Title} v{infoVersion.InformationalVersion} (build: {fileVersion.Version} {config.Configuration}) (c) {copyright.Copyright}");

var t = RunAsync(cancellationToken);
t.Wait(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
int rc = t.Result;
(bool success, int returnCode) packedResult;

switch (returnMode)
{
case ReturnType.ExitCode:
var t = RunAsync(cancellationToken);
t.Wait(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
packedResult = (default, t.Result);
if (packedResult.returnCode == ExitCodes.Success)
{
packedResult.success = true;
Logger.WriteSuccess($"{thisCommandName} Succeeded");
}
else if (packedResult.returnCode == ExitCodes.NotFound)
{
packedResult.success = true;
Logger.WriteWarning($"{thisCommandName} Item Not found");
}
else
{
packedResult.success = false;
Logger.WriteError($"{thisCommandName} Failed!");
}
break;
case ReturnType.SuccessBooleanPlusIntegerValue:
var t2 = RunWithReturnAsync(cancellationToken);
t2.Wait(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
packedResult = t2.Result;
if (packedResult.success)
{
Logger.WriteSuccess($"{thisCommandName} Succeeded");
}
else
{
Logger.WriteError($"{thisCommandName} Failed!");
}
break;
default:
throw new NotImplementedException($"Fix code and add {returnMode} to ReturnType enum.");
}

var eventEnd = new EventTelemetry
{
Name = $"{thisCommandName} End"
};
eventEnd.Properties["exitCode"] = rc.ToString();
// SuccessBooleanPlusIntegerValue is used only in testing scenarios
eventEnd.Properties["exitCode"] = packedResult.returnCode.ToString();
Telemetry.TrackEvent(eventEnd);

if (rc == ExitCodes.Success)
{
Logger.WriteSuccess($"{thisCommandName} Succeeded");
}
else if (rc == ExitCodes.NotFound)
{
Logger.WriteWarning($"{thisCommandName} Item Not found");
}
else
{
Logger.WriteError($"{thisCommandName} Failed!");
}
return rc;
return packedResult;
}
catch (Exception ex)
{
Expand All @@ -95,7 +134,7 @@ internal int Run(CancellationToken cancellationToken)
: ex.InnerException.Message
);
Telemetry.TrackException(ex);
return ExitCodes.Unexpected;
return (false, ExitCodes.Unexpected);
}
}

Expand Down
28 changes: 26 additions & 2 deletions src/aggregator-cli/ContextBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Management.Fluent;
using Microsoft.Azure.Management.ResourceManager.Fluent;
using Microsoft.VisualStudio.Services.WebApi;

namespace aggregator.cli
Expand All @@ -12,12 +13,14 @@ internal class CommandContext
{
internal ILogger Logger { get; }
internal IAzure Azure { get; }
internal IResourceManagementClient AzureManagement { get; }
internal VssConnection Devops { get; }
internal INamingTemplates Naming { get; }
internal CommandContext(ILogger logger, IAzure azure, VssConnection devops, INamingTemplates naming)
internal CommandContext(ILogger logger, IAzure azure, IResourceManagementClient azureManagement, VssConnection devops, INamingTemplates naming)
{
Logger = logger;
Azure = azure;
AzureManagement = azureManagement;
Devops = devops;
Naming = naming;
}
Expand All @@ -28,6 +31,7 @@ internal class ContextBuilder
private readonly string namingTemplate;
private readonly ILogger logger;
private bool azureLogon;
private bool azureManagementLogon;
private bool devopsLogon;

internal ContextBuilder(ILogger logger, string namingTemplate)
Expand All @@ -42,6 +46,12 @@ internal ContextBuilder WithAzureLogon()
return this;
}

internal ContextBuilder WithAzureManagement()
{
azureManagementLogon = true;
return this;
}

internal ContextBuilder WithDevOpsLogon()
{
devopsLogon = true;
Expand All @@ -51,6 +61,7 @@ internal ContextBuilder WithDevOpsLogon()
internal async Task<CommandContext> BuildAsync(CancellationToken cancellationToken)
{
IAzure azure = null;
IResourceManagementClient azureManagement = null;
VssConnection devops = null;

if (azureLogon)
Expand All @@ -66,6 +77,19 @@ internal async Task<CommandContext> BuildAsync(CancellationToken cancellationTok
azure = connection.Logon();
logger.WriteInfo($"Connected to subscription {azure.SubscriptionId}");
}
if (azureManagementLogon)
{
logger.WriteVerbose($"Authenticating to Azure...");
var (connection, reason) = AzureLogon.Load();
if (reason != LogonResult.Succeeded)
{
string msg = TranslateResult(reason);
throw new InvalidOperationException(string.Format(msg, "Azure", "logon.azure"));
}

azureManagement = connection.LogonManagement();
logger.WriteInfo($"Connected to subscription {azure.SubscriptionId}");
}

if (devopsLogon)
{
Expand Down Expand Up @@ -97,7 +121,7 @@ internal async Task<CommandContext> BuildAsync(CancellationToken cancellationTok
break;
}

return new CommandContext(logger, azure, devops, naming);
return new CommandContext(logger, azure, azureManagement, devops, naming);
}

private static string TranslateResult(LogonResult reason)
Expand Down
25 changes: 25 additions & 0 deletions src/aggregator-cli/DevOps/Boards.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,30 @@ internal async Task<int> CreateWorkItemAsync(string projectName, string title, C
return newWorkItem.Id ?? 0;
}

internal async Task<int> UpdateWorkItemAsync(string projectName, int workItemId, string newTitle, CancellationToken cancellationToken)
{
logger.WriteVerbose($"Reading Azure DevOps project data...");
var projectClient = devops.GetClient<ProjectHttpClient>();
var project = await projectClient.GetProject(projectName);
logger.WriteInfo($"Project {projectName} data read.");

var witClient = devops.GetClient<WorkItemTrackingHttpClient>();
JsonPatchDocument patchDocument = new JsonPatchDocument
{
new JsonPatchOperation()
{
Operation = Operation.Replace,
Path = "/fields/System.Title",
Value = newTitle
}
};

logger.WriteVerbose($"Updating work item #{workItemId} in '{project.Name}' with new Title '{newTitle}'");
var workItem = await witClient.UpdateWorkItemAsync(patchDocument, workItemId, validateOnly: false, bypassRules: false, cancellationToken: cancellationToken);
logger.WriteInfo($"Updated work item ID {workItem.Id} '{workItem.Fields["System.Title"]}' in '{project.Name}'");

return 0;
}

}
}
55 changes: 46 additions & 9 deletions src/aggregator-cli/Instances/AggregatorInstances.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ class AggregatorInstances : AzureBaseClass
{
private readonly INamingTemplates naming;

public AggregatorInstances(IAzure azure, ILogger logger, INamingTemplates naming)
: base(azure, logger)
public AggregatorInstances(IAzure azure, IResourceManagementClient azureManagement, ILogger logger, INamingTemplates naming)
: base(azure, logger, azureManagement)
{
this.naming = naming;
}
Expand Down Expand Up @@ -329,30 +329,66 @@ internal async Task<string> ReadLogAsync(InstanceName instance, string functionN
return logData;
}

internal async Task<bool> UpdateAsync(InstanceName instance, string requiredVersion, string sourceUrl, CancellationToken cancellationToken)
internal async Task<bool> UpdateAsync(InstanceCreateNames instance, string requiredVersion, string sourceUrl, CancellationToken cancellationToken)
{
// capture the list of rules/Functions _before_
_logger.WriteInfo($"Upgrading instance '{instance.PlainName}': retriving rules");
var rules = new AggregatorRules(_azure, _logger);
var allRules = await rules.ListAsync(instance, cancellationToken);
var ruleNames = allRules.Select(r => r.RuleName).ToList();

// update runtime package
_logger.WriteInfo($"Upgrading instance '{instance.PlainName}': updating run-time package to {requiredVersion} from {sourceUrl}");
var package = new FunctionRuntimePackage(_logger);
bool ok = await package.UpdateVersionAsync(requiredVersion, sourceUrl, instance, _azure, cancellationToken);
if (!ok)
return false;

_logger.WriteInfo($"Upgrading instance '{instance.PlainName}': force AzFunction runtime version");
await ForceFunctionRuntimeVersionAsync(instance, cancellationToken);

_logger.WriteInfo($"Upgrading instance '{instance.PlainName}': updating root files");
var uploadFiles = await UpdateDefaultFilesAsync(package);

var rules = new AggregatorRules(_azure, _logger);
var allRules = await rules.ListAsync(instance, cancellationToken);

foreach (var ruleName in allRules.Select(r => r.RuleName))
foreach (var ruleName in ruleNames)
{
_logger.WriteInfo($"Updating Rule '{ruleName}'");
_logger.WriteInfo($"Upgrading instance '{instance.PlainName}': updating Rule '{ruleName}'");
await rules.UploadRuleFilesAsync(instance, ruleName, uploadFiles, cancellationToken);
}

_logger.WriteInfo($"Upgrading instance '{instance.PlainName}': updating Tag 'aggregatorVersion' on resources");
// TODO we should use APIs instead of template in creation...
await SetVersionTag(instance, "Microsoft.Web", "sites", instance.FunctionAppName, "2021-01-01", cancellationToken);
// HACK cannot use instance.StorageAccountName because older version used a random seed to generate the name
var minVersionFixingStorageNameGeneration = new Semver.SemVersion(1, 3, 0, "beta");
var storageAccounts = await _azure.StorageAccounts.ListByResourceGroupAsync(instance.ResourceGroupName);
// we assume that there is one and only one StorageAccount created by aggregator in the ResourceGroup
var storageAccount = storageAccounts.FirstOrDefault(a => a.Tags.ContainsKey("aggregatorVersion"));
if (Semver.SemVersion.TryParse(storageAccount?.Tags["aggregatorVersion"], out var semVer)
&& semVer >= minVersionFixingStorageNameGeneration)
{
await SetVersionTag(instance, "Microsoft.Storage", "storageAccounts", instance.StorageAccountName, "2021-01-01", cancellationToken);
}
else
{
await SetVersionTag(instance, "Microsoft.Storage", "storageAccounts", storageAccount.Name, "2021-01-01", cancellationToken);
}
await SetVersionTag(instance, "microsoft.insights", "components", instance.AppInsightName, "2020-02-02", cancellationToken);
await SetVersionTag(instance, "Microsoft.Web", "serverfarms", instance.HostingPlanName, "2021-01-01", cancellationToken);

_logger.WriteInfo($"Upgrading instance '{instance.PlainName}': complete");
return true;
}

private async Task SetVersionTag(InstanceName instance, string resourceProviderNamespace, string resourceType, string resourceName, string apiVersion, CancellationToken cancellationToken)
{
//string apiVersion = ;
var theResource = await _azureManagement.Resources.GetAsync(instance.ResourceGroupName, resourceProviderNamespace, "", resourceType, resourceName, apiVersion, cancellationToken);
var infoVersion = GetCustomAttribute<AssemblyInformationalVersionAttribute>();
theResource.Tags["aggregatorVersion"] = infoVersion.InformationalVersion;
await _azureManagement.Resources.UpdateAsync(instance.ResourceGroupName, resourceProviderNamespace, "", resourceType, resourceName, apiVersion, theResource, cancellationToken);
}

private static async Task<Dictionary<string, string>> UpdateDefaultFilesAsync(FunctionRuntimePackage package)
{
var uploadFiles = new Dictionary<string, string>();
Expand All @@ -372,9 +408,10 @@ private static async Task<Dictionary<string, string>> UpdateDefaultFilesAsync(Fu

private async Task ForceFunctionRuntimeVersionAsync(InstanceName instance, CancellationToken cancellationToken)
{
// HACK this must match appSettings of Microsoft.Web/sites resource in instance-template.json !!!
const string TargetVersion = "~4";
const string DotNetVersion = "v6.0";
// Change V2/V3 to V4 FUNCTIONS_EXTENSION_VERSION ~4
// Change FUNCTIONS_EXTENSION_VERSION to TargetVersion
var webFunctionApp = await GetWebApp(instance, cancellationToken);
var currentAzureRuntimeVersion = webFunctionApp.GetAppSettings()
.GetValueOrDefault("FUNCTIONS_EXTENSION_VERSION");
Expand Down
Loading

0 comments on commit 1c2ed2f

Please sign in to comment.