armpit
Version:
Another resource manager programming interface toolkit.
243 lines • 9 kB
JavaScript
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