diff --git a/__tests__/PowerShell/ServicePrinicipalLogin.test.ts b/__tests__/PowerShell/ServicePrinicipalLogin.test.ts index ffd59881..373d15a8 100644 --- a/__tests__/PowerShell/ServicePrinicipalLogin.test.ts +++ b/__tests__/PowerShell/ServicePrinicipalLogin.test.ts @@ -5,7 +5,7 @@ jest.mock('../../src/PowerShell/Utilities/PowerShellToolRunner'); let spnlogin: ServicePrincipalLogin; beforeAll(() => { - spnlogin = new ServicePrincipalLogin("servicePrincipalID", "servicePrinicipalkey", "tenantId", "subscriptionId", false); + spnlogin = new ServicePrincipalLogin("servicePrincipalID", "servicePrinicipalkey", "tenantId", "subscriptionId", false, null, null); }); afterEach(() => { diff --git a/action.yml b/action.yml index e7bd4f63..70d5274b 100644 --- a/action.yml +++ b/action.yml @@ -9,6 +9,10 @@ inputs: description: 'Set this value to true to enable Azure PowerShell Login in addition to Az CLI login' required: false default: false + environment: + description: 'Name of the environment. Supported values are azurecloud, azurestack, azureusgovernment, azurechinacloud, azuregermancloud. Default being azurecloud' + required: false + default: AzureCloud allow-no-subscriptions: description: 'Set this value to true to enable support for accessing tenants without subscriptions' required: false diff --git a/lib/PowerShell/ServicePrincipalLogin.js b/lib/PowerShell/ServicePrincipalLogin.js index 519b0d2c..673ca42a 100644 --- a/lib/PowerShell/ServicePrincipalLogin.js +++ b/lib/PowerShell/ServicePrincipalLogin.js @@ -25,11 +25,13 @@ const PowerShellToolRunner_1 = __importDefault(require("./Utilities/PowerShellTo const ScriptBuilder_1 = __importDefault(require("./Utilities/ScriptBuilder")); const Constants_1 = __importDefault(require("./Constants")); class ServicePrincipalLogin { - constructor(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId, allowNoSubscriptionsLogin) { + constructor(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId, allowNoSubscriptionsLogin, environment, resourceManagerEndpointUrl) { this.servicePrincipalId = servicePrincipalId; this.servicePrincipalKey = servicePrincipalKey; this.tenantId = tenantId; this.subscriptionId = subscriptionId; + this.environment = environment; + this.resourceManagerEndpointUrl = resourceManagerEndpointUrl; this.allowNoSubscriptionsLogin = allowNoSubscriptionsLogin; } initialize() { @@ -54,9 +56,10 @@ class ServicePrincipalLogin { servicePrincipalId: this.servicePrincipalId, servicePrincipalKey: this.servicePrincipalKey, subscriptionId: this.subscriptionId, - environment: ServicePrincipalLogin.environment, + environment: this.environment, scopeLevel: ServicePrincipalLogin.scopeLevel, - allowNoSubscriptionsLogin: this.allowNoSubscriptionsLogin + allowNoSubscriptionsLogin: this.allowNoSubscriptionsLogin, + resourceManagerEndpointUrl: this.resourceManagerEndpointUrl }; const script = new ScriptBuilder_1.default().getAzPSLoginScript(ServicePrincipalLogin.scheme, this.tenantId, args); yield PowerShellToolRunner_1.default.init(); @@ -70,6 +73,5 @@ class ServicePrincipalLogin { } } exports.ServicePrincipalLogin = ServicePrincipalLogin; -ServicePrincipalLogin.environment = Constants_1.default.AzureCloud; ServicePrincipalLogin.scopeLevel = Constants_1.default.Subscription; ServicePrincipalLogin.scheme = Constants_1.default.ServicePrincipal; diff --git a/lib/PowerShell/Utilities/ScriptBuilder.js b/lib/PowerShell/Utilities/ScriptBuilder.js index 30cb4421..24164bbf 100644 --- a/lib/PowerShell/Utilities/ScriptBuilder.js +++ b/lib/PowerShell/Utilities/ScriptBuilder.js @@ -20,6 +20,9 @@ class ScriptBuilder { let command = `Clear-AzContext -Scope Process; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue;`; if (scheme === Constants_1.default.ServicePrincipal) { + if (args.environment.toLowerCase() == "azurestack") { + command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`; + } command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \ (New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \ -Environment '${args.environment}' | out-null;`; diff --git a/lib/main.js b/lib/main.js index c3933519..274dc1d3 100644 --- a/lib/main.js +++ b/lib/main.js @@ -36,6 +36,13 @@ function main() { core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString); core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azurePSHostEnv); azPath = yield io.which("az", true); + let azureSupportedCloudName = new Set([ + "azureusgovernment", + "azurechinacloud", + "azuregermancloud", + "azurecloud", + "azurestack" + ]); let output = ""; const execOptions = { listeners: { @@ -52,6 +59,8 @@ function main() { let servicePrincipalKey = secrets.getSecret("$.clientSecret", true); let tenantId = secrets.getSecret("$.tenantId", false); let subscriptionId = secrets.getSecret("$.subscriptionId", false); + let resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false); + let environment = core.getInput("environment").toLowerCase(); const enableAzPSSession = core.getInput('enable-AzPSSession').toLowerCase() === "true"; const allowNoSubscriptionsLogin = core.getInput('allow-no-subscriptions').toLowerCase() === "true"; if (!servicePrincipalId || !servicePrincipalKey || !tenantId) { @@ -60,6 +69,40 @@ function main() { if (!subscriptionId && !allowNoSubscriptionsLogin) { throw new Error("Not all values are present in the creds object. Ensure subscriptionId is supplied."); } + if (!azureSupportedCloudName.has(environment)) { + throw new Error("Unsupported value for environment is passed.The list of supported values for environment are ‘azureusgovernment', ‘azurechinacloud’, ‘azuregermancloud’, ‘azurecloud’ or ’azurestack’"); + } + // Attempting Az cli login + if (environment == "azurestack") { + if (!resourceManagerEndpointUrl) { + throw new Error("resourceManagerEndpointUrl is a required parameter when environment is defined."); + } + console.log(`Unregistering cloud: "${environment}" first if it exists`); + try { + yield executeAzCliCommand(`cloud set -n AzureCloud`, true); + yield executeAzCliCommand(`cloud unregister -n "${environment}"`, false); + } + catch (error) { + console.log(`Ignore cloud not registered error: "${error}"`); + } + console.log(`Registering cloud: "${environment}" with ARM endpoint: "${resourceManagerEndpointUrl}"`); + try { + let baseUri = resourceManagerEndpointUrl; + if (baseUri.endsWith('/')) { + baseUri = baseUri.substring(0, baseUri.length - 1); // need to remove trailing / from resourceManagerEndpointUrl to correctly derive suffixes below + } + let suffixKeyvault = ".vault" + baseUri.substring(baseUri.indexOf('.')); // keyvault suffix starts with . + let suffixStorage = baseUri.substring(baseUri.indexOf('.') + 1); // storage suffix starts without . + let profileVersion = "2019-03-01-hybrid"; + yield executeAzCliCommand(`cloud register -n "${environment}" --endpoint-resource-manager "${resourceManagerEndpointUrl}" --suffix-keyvault-dns "${suffixKeyvault}" --suffix-storage-endpoint "${suffixStorage}" --profile "${profileVersion}"`, false); + } + catch (error) { + core.error(`Error while trying to register cloud "${environment}": "${error}"`); + } + console.log(`Done registering cloud: "${environment}"`); + } + yield executeAzCliCommand(`cloud set -n "${environment}"`, false); + console.log(`Done setting cloud: "${environment}"`); // Attempting Az cli login if (allowNoSubscriptionsLogin) { let args = [ @@ -89,7 +132,7 @@ function main() { if (enableAzPSSession) { // Attempting Az PS login console.log(`Running Azure PS Login`); - const spnlogin = new ServicePrincipalLogin_1.ServicePrincipalLogin(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId, allowNoSubscriptionsLogin); + const spnlogin = new ServicePrincipalLogin_1.ServicePrincipalLogin(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId, allowNoSubscriptionsLogin, environment, resourceManagerEndpointUrl); yield spnlogin.initialize(); yield spnlogin.login(); } diff --git a/src/PowerShell/ServicePrincipalLogin.ts b/src/PowerShell/ServicePrincipalLogin.ts index 580bff56..cc8a2dde 100644 --- a/src/PowerShell/ServicePrincipalLogin.ts +++ b/src/PowerShell/ServicePrincipalLogin.ts @@ -6,24 +6,30 @@ import ScriptBuilder from './Utilities/ScriptBuilder'; import Constants from './Constants'; export class ServicePrincipalLogin implements IAzurePowerShellSession { - static readonly environment: string = Constants.AzureCloud; static readonly scopeLevel: string = Constants.Subscription; static readonly scheme: string = Constants.ServicePrincipal; + environment: string; servicePrincipalId: string; servicePrincipalKey: string; tenantId: string; subscriptionId: string; + resourceManagerEndpointUrl: string; allowNoSubscriptionsLogin: boolean; constructor(servicePrincipalId: string, servicePrincipalKey: string, tenantId: string, subscriptionId: string, - allowNoSubscriptionsLogin: boolean) { + allowNoSubscriptionsLogin: boolean, + environment: string, + resourceManagerEndpointUrl: string) { + this.servicePrincipalId = servicePrincipalId; this.servicePrincipalKey = servicePrincipalKey; this.tenantId = tenantId; this.subscriptionId = subscriptionId; + this.environment = environment; + this.resourceManagerEndpointUrl = resourceManagerEndpointUrl; this.allowNoSubscriptionsLogin = allowNoSubscriptionsLogin; } @@ -47,9 +53,10 @@ export class ServicePrincipalLogin implements IAzurePowerShellSession { servicePrincipalId: this.servicePrincipalId, servicePrincipalKey: this.servicePrincipalKey, subscriptionId: this.subscriptionId, - environment: ServicePrincipalLogin.environment, + environment: this.environment, scopeLevel: ServicePrincipalLogin.scopeLevel, - allowNoSubscriptionsLogin: this.allowNoSubscriptionsLogin + allowNoSubscriptionsLogin: this.allowNoSubscriptionsLogin, + resourceManagerEndpointUrl: this.resourceManagerEndpointUrl } const script: string = new ScriptBuilder().getAzPSLoginScript(ServicePrincipalLogin.scheme, this.tenantId, args); await PowerShellToolRunner.init(); diff --git a/src/PowerShell/Utilities/ScriptBuilder.ts b/src/PowerShell/Utilities/ScriptBuilder.ts index 9d606f1b..36fe4815 100644 --- a/src/PowerShell/Utilities/ScriptBuilder.ts +++ b/src/PowerShell/Utilities/ScriptBuilder.ts @@ -9,6 +9,9 @@ export default class ScriptBuilder { let command = `Clear-AzContext -Scope Process; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue;`; if (scheme === Constants.ServicePrincipal) { + if (args.environment.toLowerCase() == "azurestack") { + command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`; + } command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \ (New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \ -Environment '${args.environment}' | out-null;`; diff --git a/src/main.ts b/src/main.ts index 3fd4b03a..abc1c4d7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,6 @@ import * as core from '@actions/core'; import * as exec from '@actions/exec'; import * as io from '@actions/io'; - import { FormatType, SecretParser } from 'actions-secret-parser'; import { ServicePrincipalLogin } from './PowerShell/ServicePrincipalLogin'; @@ -21,6 +20,14 @@ async function main() { core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azurePSHostEnv); azPath = await io.which("az", true); + + let azureSupportedCloudName = new Set([ + "azureusgovernment", + "azurechinacloud", + "azuregermancloud", + "azurecloud", + "azurestack"]); + let output: string = ""; const execOptions: any = { listeners: { @@ -38,8 +45,11 @@ async function main() { let servicePrincipalKey = secrets.getSecret("$.clientSecret", true); let tenantId = secrets.getSecret("$.tenantId", false); let subscriptionId = secrets.getSecret("$.subscriptionId", false); + let resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false); + let environment = core.getInput("environment").toLowerCase(); const enableAzPSSession = core.getInput('enable-AzPSSession').toLowerCase() === "true"; const allowNoSubscriptionsLogin = core.getInput('allow-no-subscriptions').toLowerCase() === "true"; + if (!servicePrincipalId || !servicePrincipalKey || !tenantId) { throw new Error("Not all values are present in the creds object. Ensure clientId, clientSecret and tenantId are supplied."); } @@ -47,6 +57,46 @@ async function main() { if (!subscriptionId && !allowNoSubscriptionsLogin) { throw new Error("Not all values are present in the creds object. Ensure subscriptionId is supplied."); } + + if (!azureSupportedCloudName.has(environment)){ + throw new Error("Unsupported value for environment is passed.The list of supported values for environment are ‘azureusgovernment', ‘azurechinacloud’, ‘azuregermancloud’, ‘azurecloud’ or ’azurestack’"); + } + + // Attempting Az cli login + if (environment == "azurestack") { + if (!resourceManagerEndpointUrl) { + throw new Error("resourceManagerEndpointUrl is a required parameter when environment is defined."); + } + + console.log(`Unregistering cloud: "${environment}" first if it exists`); + try { + await executeAzCliCommand(`cloud set -n AzureCloud`, true); + await executeAzCliCommand(`cloud unregister -n "${environment}"`, false); + } + catch (error) { + console.log(`Ignore cloud not registered error: "${error}"`); + } + + console.log(`Registering cloud: "${environment}" with ARM endpoint: "${resourceManagerEndpointUrl}"`); + try { + let baseUri = resourceManagerEndpointUrl; + if (baseUri.endsWith('/')) { + baseUri = baseUri.substring(0, baseUri.length-1); // need to remove trailing / from resourceManagerEndpointUrl to correctly derive suffixes below + } + let suffixKeyvault = ".vault" + baseUri.substring(baseUri.indexOf('.')); // keyvault suffix starts with . + let suffixStorage = baseUri.substring(baseUri.indexOf('.')+1); // storage suffix starts without . + let profileVersion = "2019-03-01-hybrid"; + await executeAzCliCommand(`cloud register -n "${environment}" --endpoint-resource-manager "${resourceManagerEndpointUrl}" --suffix-keyvault-dns "${suffixKeyvault}" --suffix-storage-endpoint "${suffixStorage}" --profile "${profileVersion}"`, false); + } + catch (error) { + core.error(`Error while trying to register cloud "${environment}": "${error}"`); + } + + console.log(`Done registering cloud: "${environment}"`) + } + + await executeAzCliCommand(`cloud set -n "${environment}"`, false); + console.log(`Done setting cloud: "${environment}"`); // Attempting Az cli login if (allowNoSubscriptionsLogin) { @@ -73,35 +123,52 @@ async function main() { ]; await executeAzCliCommand(`account set`, true, {}, args); } + isAzCLISuccess = true; if (enableAzPSSession) { // Attempting Az PS login console.log(`Running Azure PS Login`); - const spnlogin: ServicePrincipalLogin = new ServicePrincipalLogin(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId, allowNoSubscriptionsLogin); + const spnlogin: ServicePrincipalLogin = new ServicePrincipalLogin( + servicePrincipalId, + servicePrincipalKey, + tenantId, + subscriptionId, + allowNoSubscriptionsLogin, + environment, + resourceManagerEndpointUrl); await spnlogin.initialize(); await spnlogin.login(); } + console.log("Login successful."); - } catch (error) { + } + catch (error) { if (!isAzCLISuccess) { core.error("Az CLI Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows"); - } else { + } + else { core.error(`Azure PowerShell Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows"`); } core.setFailed(error); - } finally { + } + finally { // Reset AZURE_HTTP_USER_AGENT core.exportVariable('AZURE_HTTP_USER_AGENT', prefix); core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azPSHostEnv); } } -async function executeAzCliCommand(command: string, silent?: boolean, execOptions: any = {}, args: any = []) { +async function executeAzCliCommand( + command: string, + silent?: boolean, + execOptions: any = {}, + args: any = []) { + execOptions.silent = !!silent; try { await exec.exec(`"${azPath}" ${command}`, args, execOptions); } - catch(error) { + catch (error) { throw new Error(error); } }