Skip to content
This repository was archived by the owner on May 2, 2023. It is now read-only.
Draft
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
166 changes: 152 additions & 14 deletions source/Calamari/AzureContextScriptWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Calamari.Common.Features.Discovery;
using Calamari.Common.Features.EmbeddedResources;
using Calamari.Common.Features.Processes;
using Calamari.Common.Features.Scripting;
using Calamari.Common.Features.Scripts;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;
using Calamari.Common.Plumbing.Variables;
using Newtonsoft.Json;

namespace Calamari.AzureScripting
{
Expand Down Expand Up @@ -49,37 +51,173 @@ public CommandResult ExecuteScript(Script script,
variables.Set("OctopusAzureTargetScript", script.File);
variables.Set("OctopusAzureTargetScriptParameters", script.Parameters);

SetOutputVariable("OctopusAzureSubscriptionId", variables.Get(SpecialVariables.Action.Azure.SubscriptionId)!);
SetOutputVariable("OctopusAzureStorageAccountName", variables.Get(SpecialVariables.Action.Azure.StorageAccountName)!);
var azureEnvironment = variables.Get(SpecialVariables.Action.Azure.Environment, DefaultAzureEnvironment)!;
if (azureEnvironment != DefaultAzureEnvironment)
var cloudConnectionContext = GetCloudConnectionContext(variables);
AzureContextScriptWrapperAuthentication? scriptAuthentication;
if (cloudConnectionContext != null)
{
log.InfoFormat("Using Azure Environment override - {0}", azureEnvironment);
scriptAuthentication = AzureContextScriptWrapperAuthentication.CreateFromCloudConnectionContext(cloudConnectionContext);
}
SetOutputVariable("OctopusAzureEnvironment", azureEnvironment);
else
{
scriptAuthentication = AzureContextScriptWrapperAuthentication.CreateFromKnownVariables(variables);
}

SetOutputVariable("OctopusAzureSubscriptionId", scriptAuthentication.SubscriptionId);
SetOutputVariable("OctopusAzureStorageAccountName", scriptAuthentication.StorageAccountName!);
if (scriptAuthentication.AzureEnvironment != DefaultAzureEnvironment)
{
log.InfoFormat("Using Azure Environment override - {0}", scriptAuthentication.AzureEnvironment!);
}
SetOutputVariable("OctopusAzureEnvironment", scriptAuthentication.AzureEnvironment!);
SetOutputVariable("OctopusAzureExtensionsDirectory", variables.Get(SpecialVariables.Action.Azure.ExtensionsDirectory)!);

using (new TemporaryFile(Path.Combine(workingDirectory, "AzureProfile.json")))
using (var contextScriptFile = new TemporaryFile(CreateContextScriptFile(workingDirectory, scriptSyntax)))
{
if (variables.Get(SpecialVariables.Account.AccountType) == "AzureServicePrincipal")
if (scriptAuthentication.UseServicePrincipal)
{
SetOutputVariable("OctopusUseServicePrincipal", bool.TrueString);
SetOutputVariable("OctopusAzureADTenantId", variables.Get(SpecialVariables.Action.Azure.TenantId)!);
SetOutputVariable("OctopusAzureADClientId", variables.Get(SpecialVariables.Action.Azure.ClientId)!);
variables.Set("OctopusAzureADPassword", variables.Get(SpecialVariables.Action.Azure.Password));
SetOutputVariable("OctopusAzureADTenantId", scriptAuthentication.TenantId!);
SetOutputVariable("OctopusAzureADClientId", scriptAuthentication.ClientId!);
variables.Set("OctopusAzureADPassword", scriptAuthentication.Password);
return NextWrapper!.ExecuteScript(new Script(contextScriptFile.FilePath), scriptSyntax, commandLineRunner, environmentVars);
}

//otherwise use management certificate
SetOutputVariable("OctopusUseServicePrincipal", false.ToString());
using (new TemporaryFile(CreateAzureCertificate(workingDirectory)))
using (new TemporaryFile(CreateAzureCertificate(workingDirectory, scriptAuthentication)))
{
return NextWrapper!.ExecuteScript(new Script(contextScriptFile.FilePath), scriptSyntax, commandLineRunner, environmentVars);
}
}
}

private class CloudConnectionContext<TAuthentication>
{
public TAuthentication Authentication { get; set; }

public CloudConnectionContext(TAuthentication authentication)
{
Authentication = authentication;
}
}

private class AzureCloudConnectionAuthentication
{
public ServicePrincipalAccount AccountDetails { get; set; }

public AzureCloudConnectionAuthentication(ServicePrincipalAccount accountDetails)
{
AccountDetails = accountDetails;
}
}

class AzureContextScriptWrapperAuthentication
{
public AzureContextScriptWrapperAuthentication(
string subscriptionId,
string azureEnvironment,
string? clientId,
string? tenantId,
string? password,
string? storageAccountName,
string? certificateThumbprint,
byte[]? certificateBytes,
bool useServicePrincipal)
{
SubscriptionId = subscriptionId;
AzureEnvironment = azureEnvironment;
ClientId = clientId;
TenantId = tenantId;
Password = password;
StorageAccountName = storageAccountName;
CertificateThumbprint = certificateThumbprint;
CertificateBytes = certificateBytes;
UseServicePrincipal = useServicePrincipal;
}

public static AzureContextScriptWrapperAuthentication CreateFromKnownVariables(IVariables variables)
{
return new AzureContextScriptWrapperAuthentication(
subscriptionId: variables.Get(SpecialVariables.Action.Azure.SubscriptionId)!,
azureEnvironment: variables.Get(SpecialVariables.Action.Azure.Environment, DefaultAzureEnvironment)!,
clientId: variables.Get(SpecialVariables.Action.Azure.ClientId),
tenantId: variables.Get(SpecialVariables.Action.Azure.TenantId),
password: variables.Get(SpecialVariables.Action.Azure.Password),
variables.Get(SpecialVariables.Action.Azure.StorageAccountName)!,
variables.Get(SpecialVariables.Action.Azure.CertificateThumbprint),
variables.IsSet(SpecialVariables.Action.Azure.CertificateBytes) ? Convert.FromBase64String(variables.Get(SpecialVariables.Action.Azure.CertificateBytes)!) : null,
variables.Get(SpecialVariables.Account.AccountType) == "AzureServicePrincipal");
}

public static AzureContextScriptWrapperAuthentication CreateFromCloudConnectionContext(CloudConnectionContext<AzureCloudConnectionAuthentication> context)
{
return new AzureContextScriptWrapperAuthentication(
context.Authentication.AccountDetails.SubscriptionNumber,
!string.IsNullOrEmpty(context.Authentication.AccountDetails.AzureEnvironment) ? context.Authentication.AccountDetails.AzureEnvironment! : DefaultAzureEnvironment,
context.Authentication.AccountDetails.ClientId,
context.Authentication.AccountDetails.TenantId,
context.Authentication.AccountDetails.Password,
null,
null,
null,
true);
}

public string SubscriptionId { get; }
public string AzureEnvironment { get; }
public string? ClientId { get; }
public string? TenantId { get; }
public string? Password { get; }
public string? StorageAccountName { get; set; }
public string? CertificateThumbprint { get; set; }
public byte[]? CertificateBytes { get; set; }
public bool UseServicePrincipal { get; set; }
}

class ServicePrincipalAccount
{
public ServicePrincipalAccount(
string subscriptionNumber,
string clientId,
string tenantId,
string password,
string? azureEnvironment)
{
SubscriptionNumber = subscriptionNumber;
ClientId = clientId;
TenantId = tenantId;
Password = password;
AzureEnvironment = azureEnvironment;
}

public string SubscriptionNumber { get; }
public string ClientId { get; }
public string TenantId { get; }
public string Password { get; }
public string? AzureEnvironment { get; }
}

private CloudConnectionContext<AzureCloudConnectionAuthentication>? GetCloudConnectionContext(IVariables variables)
{
const string contextVariableName = "Octopus.CloudConnection.Context";
var json = variables.Get(contextVariableName);
if (json == null)
{
return null;
}

try
{
return JsonConvert.DeserializeObject<CloudConnectionContext<AzureCloudConnectionAuthentication>>(json);
}
catch (Exception ex)
{
Log.Warn($"Cloud connection context from variable {contextVariableName} is in wrong format: {ex.Message}");
return null;
}
}

string CreateContextScriptFile(string workingDirectory, ScriptSyntax syntax)
{
string contextFile;
Expand All @@ -101,12 +239,12 @@ string CreateContextScriptFile(string workingDirectory, ScriptSyntax syntax)
return azureContextScriptFile;
}

string CreateAzureCertificate(string workingDirectory)
string CreateAzureCertificate(string workingDirectory, AzureContextScriptWrapperAuthentication scriptAuthentication)
{
var certificateFilePath = Path.Combine(workingDirectory, CertificateFileName);
var certificatePassword = GenerateCertificatePassword();
var azureCertificate = CalamariCertificateStore.GetOrAdd(variables.Get(SpecialVariables.Action.Azure.CertificateThumbprint)!,
Convert.FromBase64String(variables.Get(SpecialVariables.Action.Azure.CertificateBytes)!),
var azureCertificate = CalamariCertificateStore.GetOrAdd(scriptAuthentication.CertificateThumbprint!,
scriptAuthentication.CertificateBytes!,
StoreName.My);

variables.Set("OctopusAzureCertificateFileName", certificateFilePath);
Expand Down
36 changes: 36 additions & 0 deletions source/Sashimi.Tests/AzurePowerShellActionHandlerFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ public void ExecuteAnInlinePowerShellCoreScript()
.Execute();
}

[Test]
[RequiresPowerShell5OrAbove]
public void ExecuteAnInlinePowerShellCoreScriptWithCloudConnection()
{
var psScript = @"
$ErrorActionPreference = 'Continue'
az --version
Get-AzureEnvironment
az group list";

ActionHandlerTestBuilder.CreateAsync<AzurePowerShellActionHandler, Program>()
.WithArrange(context =>
{
AddCloudConnection(context);
context.Variables.Add(Calamari.Common.Plumbing.Variables.PowerShellVariables.Edition, "Core");
context.Variables.Add(KnownVariables.Action.Script.ScriptSource, KnownVariableValues.Action.Script.ScriptSource.Inline);
context.Variables.Add(KnownVariables.Action.Script.Syntax, ScriptSyntax.PowerShell.ToString());
context.Variables.Add(KnownVariables.Action.Script.ScriptBody, psScript);
})
.Execute();
}

[Test]
[RequiresPowerShell5OrAbove]
public void ExecuteAnInlinePowerShellCoreScriptAgainstAnInvalidAzureEnvironment()
Expand Down Expand Up @@ -100,5 +122,19 @@ void AddDefaults(TestActionHandlerContext<Program> context)
context.Variables.Add("Octopus.Action.Azure.ClientId", clientId);
context.Variables.Add("Octopus.Action.Azure.Password", clientSecret);
}

void AddCloudConnection(TestActionHandlerContext<Program> context)
{
context.Variables.Add("Octopus.CloudConnection.Context", @$"{{
""authentication"": {{
""account"": {{
""subscriptionNumber"": ""{subscriptionId}"",
""tenantId"": ""{tenantId}"",
""clientId"": ""{clientId}"",
""password"": ""{clientSecret}""
}}
}}
}}");
}
}
}
3 changes: 3 additions & 0 deletions source/Sashimi/AzurePowerShellActionHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using Octopus.Server.Extensibility.HostServices.Diagnostics;
using Sashimi.Server.Contracts;
using Sashimi.Server.Contracts.Accounts;
using Sashimi.Server.Contracts.ActionHandlers;

namespace Sashimi.AzureScripting
Expand All @@ -16,6 +18,7 @@ public class AzurePowerShellActionHandler : IActionHandlerWithAccount
public bool CanRunOnDeploymentTarget => false;
public ActionHandlerCategory[] Categories => new[] { ActionHandlerCategory.BuiltInStep, AzureConstants.AzureActionHandlerCategory, ActionHandlerCategory.Script };
public string[] StepBasedVariableNameForAccountIds { get; } = { SpecialVariables.Action.Azure.AccountId };
public IEnumerable<AccountType> SupportedAccountTypes => new[] { Sashimi.Azure.Accounts.AccountTypes.AzureServicePrincipalAccountType };

public IActionHandlerResult Execute(IActionHandlerContext context, ITaskLog taskLog)
{
Expand Down
13 changes: 7 additions & 6 deletions source/Sashimi/AzurePowerShellActionHandlerValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ class AzurePowerShellActionHandlerValidator : AbstractValidator<DeploymentAction
{
public AzurePowerShellActionHandlerValidator()
{
When(a => a.ActionType == SpecialVariables.Action.Azure.ActionTypeName,
() =>
{
RuleFor(a => a.Properties)
.MustHaveProperty(SpecialVariables.Action.Azure.AccountId, "Please select an Account or provide a variable expression for the Account ID to use.");
});
// TODO: Don't want this rule to run when cloud connections are configured
//When(a => a.ActionType == SpecialVariables.Action.Azure.ActionTypeName,
// () =>
// {
// RuleFor(a => a.Properties)
// .MustHaveProperty(SpecialVariables.Action.Azure.AccountId, "Please select an Account or provide a variable expression for the Account ID to use.");
// });
}
}
}
2 changes: 1 addition & 1 deletion source/Sashimi/Sashimi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<PackageReference Include="Octopus.Dependencies.AzureCmdlets" Version="6.13.1" />
<PackageReference Include="Sashimi.Azure.Common" Version="14.2.0" />
<PackageReference Include="Sashimi.Azure.Accounts" Version="14.2.0" />
<PackageReference Include="Sashimi.Server.Contracts" Version="14.2.0" />
<PackageReference Include="Sashimi.Server.Contracts" Version="14.2.1-geoffl-action-ha0001" />
</ItemGroup>
<Target Name="GetPackageFiles" AfterTargets="ResolveReferences" DependsOnTargets="RunResolvePackageDependencies">
<Message Text="Collecting nupkg packages to bundle with Sashimi module binaries" Importance="high" />
Expand Down