UNPKG

armpit

Version:

Another resource manager programming interface toolkit.

243 lines 9 kB
import { mergeAbortSignals } from "./tsUtils.js"; import { isSubscriptionIdOrName, isSubscriptionId, isTenantId, } from "./azureUtils.js"; import { ArmpitCliCredentialFactory, } from "./armpitCredential.js"; /** * Tools to work with Azure CLI accounts. * @remarks * Accounts roughly approximate a subscription accessed by a user via the Azure CLI. */ export class AccountTools { /** Invoker associated with a global Azure CLI shell */ #invoker; #credentialFactory; #options; constructor(dependencies, options) { this.#invoker = dependencies.invoker; this.#credentialFactory = dependencies.credentialFactory ?? new ArmpitCliCredentialFactory(this.#invoker); this.#options = options; } /** * Shows the current active Azure CLI account. * @returns The current Azure CLI account, if available. * @remarks * This effectively invokes `az account show`. */ async show(options) { const invoker = (this.#getLaxInvokerFn(options)); try { return await invoker `account show`; } catch (invocationError) { const stderr = invocationError?.stderr; if (stderr && typeof stderr === "string" && /az login|az account set/i.test(stderr)) { return null; } throw invocationError; } } /** * Shows the current signed in user. * @returns The current user. * This effectively invokes `az ad signed-in-user show`. */ async showSignedInUser(options) { return await this.#getInvokerFn(options) `ad signed-in-user show`; } /** * Lists accounts known to the Azure CLI instance. * @param options Query options. * @returns The accounts known to the Azure CLI instance. * @remarks * This effectively invokes `az account list`. */ async list(options) { const invoker = (this.#getLaxInvokerFn(options)); let args; if (options) { args = []; if (options.all) { args.push("--all"); } if (options.refresh) { args.push("--refresh"); } } const results = args && args.length > 0 ? await invoker `account list ${args}` : await invoker `account list`; return results ?? []; } /** * Sets the active account to the given subscription ID or name. * @param subscriptionIdOrName The subscription ID or name to switch the account to. * @remarks * This effectively invokes `az account set`. */ async set(subscriptionIdOrName, options) { const invoker = (this.#getLaxInvokerFn(options)); await invoker `account set --subscription ${subscriptionIdOrName}`; } async setOrLogin(criteria, secondArg, thirdArg) { let subscription; let tenantId; let options; let filterAccountsToSubscription; if (isSubscriptionIdOrName(criteria)) { // overload: subscription, tenantId?, options? if (thirdArg != null) { options = thirdArg; } subscription = criteria; if (secondArg != null) { if (isTenantId(secondArg)) { tenantId = secondArg; } else { throw new Error("Given tenant ID is not valid"); } } filterAccountsToSubscription = accounts => { let results = accounts.filter(a => a.id === subscription); if (results.length === 0) { results = accounts.filter(a => a.name === subscription); } return results; }; } else if (criteria.subscriptionId != null) { // overload: {subscriptionId, tenantId?}, options? if (secondArg != null) { options = secondArg; } if (isSubscriptionId(criteria.subscriptionId)) { subscription = criteria.subscriptionId; } else { throw new Error("Subscription ID is not valid"); } if ("tenantId" in criteria) { if (isTenantId(criteria.tenantId)) { tenantId = criteria.tenantId; } else { throw new Error("Given tenant ID is not valid"); } } filterAccountsToSubscription = accounts => accounts.filter(a => a.id === subscription); } else { throw new Error("Arguments not supported"); } const findAccount = (candidates) => { let matches = filterAccountsToSubscription(candidates.filter(a => a != null)); if (matches.length > 1 && tenantId) { matches = matches.filter(a => a.tenantId == tenantId); } if (matches.length === 0) { return null; } if (matches.length > 1) { throw new Error(`Multiple account matches found: ${matches.map(a => a.id)}`); } const match = matches[0]; if (tenantId && match.tenantId != tenantId) { throw new Error(`Account ${match.id} does not match expected tenant ${tenantId}`); } return match; }; let account = findAccount([await this.show(options)]); if (account) { return account; } // TODO: Consider refreshing and allowing a search of non-enabled accounts. // That could come at a cost to performance though. account = findAccount(await this.list(options)); if (account) { await this.set(subscription, options); return account; } console.debug("No current accounts match. Starting interactive login."); const accountResults = await this.login(tenantId, options); if (accountResults) { account = findAccount(accountResults); } if (!account || !account.isDefault) { await this.set(subscription, options); account = await this.show(options); } return account; } /** * Initiates an Azure CLI login. * @param tenantId The tenant to log into. * @returns An account if login is successful. */ async login(tenantId, options) { const invoker = (this.#getInvokerFn(options)); try { return tenantId ? await invoker `login --tenant ${tenantId}` : await invoker `login`; } catch (invocationError) { const stderr = invocationError?.stderr; if (stderr && typeof stderr === "string" && /User cancelled/i.test(stderr)) { return null; } throw invocationError; } } /** * Provides the current account or initiates a login if required. * @returns A logged in account when successful. */ async ensureActiveAccount(options) { let account = await this.show(options); if (account == null) { const accounts = await this.login(undefined, options); account = accounts?.find(a => a.isDefault) ?? null; if (account == null) { throw new Error("Failed to ensure active account"); } } return account; } /** * Lits Azure locations. * @param names The location names to filter locations to. * @returns A lot of Azure locations. */ async listLocations(names, options) { const invoker = (this.#getInvokerFn(options)); let results; if (names != null && names.length > 0) { const queryFilter = `[? contains([${names.map(n => `'${n}'`).join(",")}],name)]`; results = await invoker `account list-locations --query ${queryFilter}`; } else { results = await invoker `account list-locations`; } return results ?? []; } getCredential(options) { return this.#credentialFactory.getCredential(options); } #getInvokerFn(options) { const abortSignal = mergeAbortSignals(options?.abortSignal, this.#options.abortSignal); return abortSignal == null ? this.#invoker : this.#invoker({ abortSignal }); } #buildInvokerOptions(options) { const result = { forceAzCommandPrefix: true, simplifyContainerAppResults: true, }; const abortSignal = mergeAbortSignals(options?.abortSignal, this.#options.abortSignal); if (abortSignal != null) { result.abortSignal = abortSignal; } return result; } #getLaxInvokerFn(options) { return this.#invoker({ ...this.#buildInvokerOptions(options), allowBlanks: true, }); } } //# sourceMappingURL=accountTools.js.map