From 3e94d97948e4736610c9056debaef65b8958f00c Mon Sep 17 00:00:00 2001 From: Akshaya M Date: Mon, 16 Mar 2020 15:15:59 +0530 Subject: [PATCH] Code refactor --- src/PowerShell/Constants.ts | 9 +++ src/PowerShell/IAzurePowerShellSession.ts | 4 + src/PowerShell/ServicePrincipalLogin.ts | 39 ++++++++++ .../Utilities/PowerShellToolRunner.ts | 20 +++++ src/PowerShell/Utilities/ScriptBuilder.ts | 16 ++++ src/PowerShell/Utilities/Utils.ts | 55 ++++++++++++++ src/loginAzurePowerShell.ts | 76 ------------------- src/main.ts | 10 ++- 8 files changed, 151 insertions(+), 78 deletions(-) create mode 100644 src/PowerShell/Constants.ts create mode 100644 src/PowerShell/IAzurePowerShellSession.ts create mode 100644 src/PowerShell/ServicePrincipalLogin.ts create mode 100644 src/PowerShell/Utilities/PowerShellToolRunner.ts create mode 100644 src/PowerShell/Utilities/ScriptBuilder.ts create mode 100644 src/PowerShell/Utilities/Utils.ts delete mode 100644 src/loginAzurePowerShell.ts diff --git a/src/PowerShell/Constants.ts b/src/PowerShell/Constants.ts new file mode 100644 index 00000000..f8f6036a --- /dev/null +++ b/src/PowerShell/Constants.ts @@ -0,0 +1,9 @@ +export class Constants { + static readonly prefix: string = "az_"; + static readonly moduleName: string = "Az.Accounts"; + static readonly versionPattern = /[0-9]\.[0-9]\.[0-9]/; + + static readonly environment: string = "AzureCloud"; + static readonly scopeLevel: string = "Subscription"; + static readonly scheme: string = "ServicePrincipal"; +} \ No newline at end of file diff --git a/src/PowerShell/IAzurePowerShellSession.ts b/src/PowerShell/IAzurePowerShellSession.ts new file mode 100644 index 00000000..544369d5 --- /dev/null +++ b/src/PowerShell/IAzurePowerShellSession.ts @@ -0,0 +1,4 @@ +interface IAzurePowerShellSession { + initialize(); + login(); +} \ No newline at end of file diff --git a/src/PowerShell/ServicePrincipalLogin.ts b/src/PowerShell/ServicePrincipalLogin.ts new file mode 100644 index 00000000..b7f00a90 --- /dev/null +++ b/src/PowerShell/ServicePrincipalLogin.ts @@ -0,0 +1,39 @@ +import * as core from '@actions/core'; + +import Utils from './Utils'; +import PowerShellToolRunner from './PowerShellToolRunner'; +import ScriptBuilder from './ScriptBuilder'; +import { Constants } from './Constants'; + +export class ServicePrincipalLogin implements IAzurePowerShellSession { + static readonly environment: string = Constants.environment; + static readonly scopeLevel: string = Constants.scopeLevel; + static readonly scheme: string = Constants.scheme; + servicePrincipalId: string; + servicePrincipalKey: string; + tenantId: string; + subscriptionId: string; + + constructor(servicePrincipalId: string, servicePrincipalKey: string, tenantId: string, subscriptionId: string) { + this.servicePrincipalId = servicePrincipalId; + this.servicePrincipalKey = servicePrincipalKey; + this.tenantId = tenantId; + this.subscriptionId = subscriptionId; + } + + async initialize() { + Utils.setPSModulePath(); + const azLatestVersion: string = await Utils.getLatestModule(Constants.moduleName); + core.debug(`Az Module version used: ${azLatestVersion}`); + Utils.setPSModulePath(`${Constants.prefix}${azLatestVersion}`); + } + + async login() { + PowerShellToolRunner.init(); + const scriptBuilder: ScriptBuilder = new ScriptBuilder(); + const script: string = scriptBuilder.getScript(ServicePrincipalLogin.scheme, this.tenantId, this.servicePrincipalId, this.servicePrincipalKey, + this.subscriptionId, ServicePrincipalLogin.environment, ServicePrincipalLogin.scopeLevel); + PowerShellToolRunner.executePowerShellCommand(script); + } + +} \ No newline at end of file diff --git a/src/PowerShell/Utilities/PowerShellToolRunner.ts b/src/PowerShell/Utilities/PowerShellToolRunner.ts new file mode 100644 index 00000000..57eb5783 --- /dev/null +++ b/src/PowerShell/Utilities/PowerShellToolRunner.ts @@ -0,0 +1,20 @@ +import * as io from '@actions/io'; +import * as exec from '@actions/exec'; + +export default class PowerShellToolRunner { + static psPath: string; + + static async init() { + if(!PowerShellToolRunner.psPath) { + PowerShellToolRunner.psPath = await io.which("pwsh", true); + } + } + + static async executePowerShellCommand(command: string, options: any = {}) { + try { + await exec.exec(`"${PowerShellToolRunner.psPath}" -Command "${command}"`, [], options); + } catch(error) { + throw new Error(error); + } + } +} \ No newline at end of file diff --git a/src/PowerShell/Utilities/ScriptBuilder.ts b/src/PowerShell/Utilities/ScriptBuilder.ts new file mode 100644 index 00000000..eaba02ea --- /dev/null +++ b/src/PowerShell/Utilities/ScriptBuilder.ts @@ -0,0 +1,16 @@ +export default class ScriptBuilder { + script: string; + getScript(scheme: string, tenantId: string, servicePrincipalId: string, servicePrincipalKey: string, subscriptionId: string, environment: string, scopeLevel: string): string { + this.script += `Clear-AzContext -Scope Process; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue;`; + if (scheme === "ServicePrincipal") { + this.script += `Connect-AzAccount -ServicePrincipal -Tenant ${tenantId} -Credential \ + (New-Object System.Management.Automation.PSCredential('${servicePrincipalId}',(ConvertTo-SecureString ${servicePrincipalKey} -AsPlainText -Force))) \ + -Environment ${environment};`; + if (scopeLevel === "Subscription") { + this.script += `Set-AzContext -SubscriptionId ${subscriptionId} -TenantId ${tenantId};`; + } + } + this.script += `Get-AzContext`; + return this.script; + } +} \ No newline at end of file diff --git a/src/PowerShell/Utilities/Utils.ts b/src/PowerShell/Utilities/Utils.ts new file mode 100644 index 00000000..f7ebcadc --- /dev/null +++ b/src/PowerShell/Utilities/Utils.ts @@ -0,0 +1,55 @@ +import * as os from 'os'; +import * as exec from '@actions/exec'; +import * as io from '@actions/io'; + +import { Constants } from './Constants'; +import PowerShellToolRunner from './PowerShellToolRunner'; + +export default class Utils { + static async getLatestModule(moduleName: string): Promise { + let output: string = ""; + let error: string = ""; + const options: any = { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + }, + stderr: (data: Buffer) => { + error += data.toString(); + } + } + }; + PowerShellToolRunner.init(); + await PowerShellToolRunner.executePowerShellCommand(`(Get-Module -Name ${moduleName} -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version.ToString()`, options); + if(!Utils.isValidVersion(output.trim())) { + return ""; + } + return output.trim(); + } + + private static isValidVersion(version: string): boolean { + return !!version.match(Constants.versionPattern); + } + + static setPSModulePath(azPSVersion: string = "") { + let modulePath: string = ""; + const RUNNER: string = process.env.RUNNER_OS || os.type(); + switch (RUNNER) { + case "Linux": + modulePath = `/usr/share/${azPSVersion}:`; + break; + case "Windows": + case "Windows_NT": + modulePath = `C:\\Modules\\${azPSVersion};`; + break; + case "macOS": + case "Darwin": + // TODO: add modulepath + break; + default: + throw new Error("Unknown os"); + } + process.env.PSModulePath = `${modulePath}${process.env.PSModulePath}`; + } +} + diff --git a/src/loginAzurePowerShell.ts b/src/loginAzurePowerShell.ts deleted file mode 100644 index 37997003..00000000 --- a/src/loginAzurePowerShell.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as os from 'os'; -import * as core from '@actions/core'; -import * as exec from '@actions/exec'; -import * as io from '@actions/io'; - -var psPath: string; - -export const initializeAz = async (servicePrincipalId: string, servicePrincipalKey: string, tenantId: string, subscriptionId: string) => { - psPath = await io.which("pwsh", true); - await importModule(); - await loginToAzure(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId); -} - -async function importModule() { - setPSModulePath(); - const prefix = "az_"; - const moduleName: string = "Az.Accounts"; - const azLatestVersion: string = await getLatestModule(moduleName); - core.debug(`Az Module version used: ${azLatestVersion}`); - setPSModulePath(`${prefix}${azLatestVersion}`); -} - -function setPSModulePath(azPSVersion: string = "") { - let modulePath: string = ""; - const RUNNER: string = process.env.RUNNER_OS || os.type(); - switch (RUNNER) { - case "Linux": - modulePath = `/usr/share/${azPSVersion}:`; - break; - case "Windows": - case "Windows_NT": - modulePath = `C:\\Modules\\${azPSVersion};`; - break; - case "macOS": - case "Darwin": - // TODO: add modulepath - break; - } - process.env.PSModulePath = `${modulePath}${process.env.PSModulePath}`; -} - -async function getLatestModule(moduleName: string) { - let output: string = ""; - let error: string = ""; - let options: any = { - listeners: { - stdout: (data: Buffer) => { - output += data.toString(); - }, - stderr: (data: Buffer) => { - error += data.toString(); - } - } - }; - await executePowerShellCommand(`(Get-Module -Name ${moduleName} -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version.ToString()`, options); - return output.trim(); -} - -async function loginToAzure(servicePrincipalId: string, servicePrincipalKey: string, tenantId: string, subscriptionId: string) { - const environment: string = "AzureCloud"; - await executePowerShellCommand(`Clear-AzContext -Scope Process`); - await executePowerShellCommand(`Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue`); - await executePowerShellCommand(`Connect-AzAccount -ServicePrincipal -Tenant ${tenantId} -Credential \ - (New-Object System.Management.Automation.PSCredential('${servicePrincipalId}',(ConvertTo-SecureString ${servicePrincipalKey} -AsPlainText -Force))) \ - -Environment ${environment}`); - await executePowerShellCommand(`Set-AzContext -SubscriptionId ${subscriptionId} -TenantId ${tenantId}`); - await executePowerShellCommand(`Get-AzContext`); -} - -async function executePowerShellCommand(command: string, options: any = {}) { - try { - await exec.exec(`"${psPath}" -Command "${command}"`, [], options); - } catch(error) { - throw new Error(error); - } -} diff --git a/src/main.ts b/src/main.ts index b73f50bb..949f7879 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import * as exec from '@actions/exec'; import * as io from '@actions/io'; import { FormatType, SecretParser } from 'actions-secret-parser'; -import { initializeAz } from './loginAzurePowerShell'; +import { ServicePrincipalLogin } from './ServicePrincipalLogin'; var azPath: string; var prefix = !!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT}` : ""; @@ -26,12 +26,18 @@ async function main() { let servicePrincipalKey = secrets.getSecret("$.clientSecret", true); let tenantId = secrets.getSecret("$.tenantId", false); let subscriptionId = secrets.getSecret("$.subscriptionId", false); + const enablePSSession = !!core.getInput('enable-PSSession'); if (!servicePrincipalId || !servicePrincipalKey || !tenantId || !subscriptionId) { throw new Error("Not all values are present in the creds object. Ensure clientId, clientSecret, tenantId and subscriptionId are supplied."); } await executeAzCliCommand(`login --service-principal -u "${servicePrincipalId}" -p "${servicePrincipalKey}" --tenant "${tenantId}"`); await executeAzCliCommand(`account set --subscription "${subscriptionId}"`); - await initializeAz(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId); + if (enablePSSession) { + console.log(`Running Azure PS Login`); + const spnlogin: ServicePrincipalLogin = new ServicePrincipalLogin(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId); + spnlogin.initialize(); + spnlogin.login(); + } console.log("Login successful."); } catch (error) { core.error("Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows");