Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/255 #264

Merged
merged 15 commits into from
May 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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