Files
login/src/Cli/AzureCliLogin.ts

187 lines
7.6 KiB
TypeScript

import * as exec from '@actions/exec';
import { LoginConfig } from "../common/LoginConfig";
import { ExecOptions } from '@actions/exec/lib/interfaces';
import * as core from '@actions/core';
import * as io from '@actions/io';
export class AzureCliLogin {
loginConfig: LoginConfig;
azPath: string;
loginOptions: ExecOptions;
azVersion: string;
constructor(loginConfig: LoginConfig) {
this.loginConfig = loginConfig;
this.loginOptions = defaultExecOptions();
}
async login() {
core.info(`Running Azure CLI Login.`);
this.azPath = await io.which("az", true);
core.debug(`Azure CLI path: ${this.azPath}`);
let output: string = "";
const execOptions: any = {
listeners: {
stdout: (data: Buffer) => {
output += data.toString();
}
}
};
await this.executeAzCliCommand(["version"], true, execOptions);
core.debug(`Azure CLI version used:\n${output}`);
try {
this.azVersion = JSON.parse(output)["azure-cli"];
}
catch (error) {
core.warning("Failed to parse Azure CLI version.");
}
await this.registerAzurestackEnvIfNecessary();
await this.executeAzCliCommand(["cloud", "set", "-n", this.loginConfig.environment], false);
core.info(`Done setting cloud: "${this.loginConfig.environment}"`);
if (this.loginConfig.authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL) {
let args = ["--service-principal",
"--username", this.loginConfig.servicePrincipalId,
"--tenant", this.loginConfig.tenantId
];
if (this.loginConfig.servicePrincipalSecret) {
await this.loginWithSecret(args);
}
else {
await this.loginWithOIDC(args);
}
}
else {
let args = ["--identity"];
if (this.loginConfig.servicePrincipalId) {
await this.loginWithUserAssignedIdentity(args);
}
else {
await this.loginWithSystemAssignedIdentity(args);
}
}
}
async registerAzurestackEnvIfNecessary() {
if (this.loginConfig.environment != "azurestack") {
return;
}
if (!this.loginConfig.resourceManagerEndpointUrl) {
throw new Error("resourceManagerEndpointUrl is a required parameter when environment is defined.");
}
core.info(`Unregistering cloud: "${this.loginConfig.environment}" first if it exists`);
try {
await this.executeAzCliCommand(["cloud", "set", "-n", "AzureCloud"], true);
await this.executeAzCliCommand(["cloud", "unregister", "-n", this.loginConfig.environment], false);
}
catch (error) {
core.info(`Ignore cloud not registered error: "${error}"`);
}
core.info(`Registering cloud: "${this.loginConfig.environment}" with ARM endpoint: "${this.loginConfig.resourceManagerEndpointUrl}"`);
try {
let baseUri = this.loginConfig.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 this.executeAzCliCommand(["cloud", "register", "-n", this.loginConfig.environment, "--endpoint-resource-manager", this.loginConfig.resourceManagerEndpointUrl, "--suffix-keyvault-dns", suffixKeyvault, "--suffix-storage-endpoint", suffixStorage, "--profile", profileVersion], false);
}
catch (error) {
core.error(`Error while trying to register cloud "${this.loginConfig.environment}"`);
throw error;
}
core.info(`Done registering cloud: "${this.loginConfig.environment}"`)
}
async loginWithSecret(args: string[]) {
core.info("Note: Azure/login action also supports OIDC login mechanism. Refer https://github.com/azure/login#configure-a-service-principal-with-a-federated-credential-to-use-oidc-based-authentication for more details.")
args.push(`--password=${this.loginConfig.servicePrincipalSecret}`);
await this.callCliLogin(args, 'service principal with secret');
}
async loginWithOIDC(args: string[]) {
await this.loginConfig.getFederatedToken();
args.push("--federated-token", this.loginConfig.federatedToken);
await this.callCliLogin(args, 'OIDC');
}
async loginWithUserAssignedIdentity(args: string[]) {
let azcliMinorVersion = 0;
try {
azcliMinorVersion = parseInt(this.azVersion.split('.')[1], 10);
}
catch (error) {
core.warning("Failed to parse the minor version of Azure CLI. Assuming the version is less than 2.69.0");
}
//From Azure-cli v2.69.0, `--username` is replaced with `--client-id`, `--object-id` or `--resource-id`: https://github.com/Azure/azure-cli/pull/30525
if (azcliMinorVersion < 69) {
args.push("--username", this.loginConfig.servicePrincipalId);
}
else {
args.push("--client-id", this.loginConfig.servicePrincipalId);
}
await this.callCliLogin(args, 'user-assigned managed identity');
}
async loginWithSystemAssignedIdentity(args: string[]) {
await this.callCliLogin(args, 'system-assigned managed identity');
}
async callCliLogin(args: string[], methodName: string) {
core.info(`Attempting Azure CLI login by using ${methodName}...`);
args.unshift("login");
if (this.loginConfig.allowNoSubscriptionsLogin) {
args.push("--allow-no-subscriptions");
}
await this.executeAzCliCommand(args, true, this.loginOptions);
if (this.loginConfig.subscriptionId) {
await this.setSubscription();
}
core.info(`Azure CLI login succeeds by using ${methodName}.`);
}
async setSubscription() {
let args = ["account", "set", "--subscription", this.loginConfig.subscriptionId];
await this.executeAzCliCommand(args, true, this.loginOptions);
core.info("Subscription is set successfully.");
}
async executeAzCliCommand(
args: string[],
silent?: boolean,
execOptions: any = {}) {
execOptions.silent = !!silent;
await exec.exec(`"${this.azPath}"`, args, execOptions);
}
}
function defaultExecOptions(): exec.ExecOptions {
return {
silent: true,
listeners: {
stderr: (data: Buffer) => {
let error = data.toString();
let startsWithWarning = error.toLowerCase().startsWith('warning');
let startsWithError = error.toLowerCase().startsWith('error');
// printing ERROR
if (error && error.trim().length !== 0 && !startsWithWarning) {
if (startsWithError) {
//removing the keyword 'ERROR' to avoid duplicates while throwing error
error = error.slice(7);
}
core.error(error);
}
}
}
};
}