Skip to content

Commit

Permalink
Auto Deployments Utility Logic (Azure#1095)
Browse files Browse the repository at this point in the history
  • Loading branch information
ReinierCC authored Nov 26, 2024
1 parent 940a3e8 commit db9bf62
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 0 deletions.
48 changes: 48 additions & 0 deletions src/commands/utils/acrs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,54 @@ export async function getRepositoryTags(
return errmap(propsResult, (props) => props.flatMap((p) => p.tags));
}

export async function createAcr( //TODO: proper name input checking
sessionProvider: ReadyAzureSessionProvider,
subscriptionId: string,
resourceGroup: string,
acrName: string,
location: string,
): Promise<Errorable<DefinedRegistry>> {
const client = getAcrManagementClient(sessionProvider, subscriptionId);
try {
const registry = await client.registries.beginCreateAndWait(resourceGroup, acrName, {
location,
sku: {
name: "Basic", //As quoted by azure doc, Basic SKU is the "cost optimized entry point for developers": https://learn.microsoft.com/en-us/azure/container-registry/container-registry-skus
}, //Future: Can provide users the ability to select their desired SKU
});
if (isDefinedRegistry(registry)) {
return { succeeded: true, result: registry };
}
return {
succeeded: false,
error: `Failed to create Azure Container Registry (ACR) "${acrName}" in resource group "${resourceGroup}" under subscription "${subscriptionId}".`,
};
} catch (error) {
return {
succeeded: false,
error: `An error occurred while creating ACR "${acrName}" in resource group "${resourceGroup}" under subscription "${subscriptionId}": ${getErrorMessage(error)}`,
};
}
}

export async function deleteAcr(
sessionProvider: ReadyAzureSessionProvider,
subscriptionId: string,
resourceGroup: string,
acrName: string,
): Promise<Errorable<void>> {
const client = getAcrManagementClient(sessionProvider, subscriptionId);
try {
await client.registries.beginDeleteAndWait(resourceGroup, acrName);
return { succeeded: true, result: undefined };
} catch (error) {
return {
succeeded: false,
error: `An error occurred while deleting ACR "${acrName}" in resource group "${resourceGroup}" under subscription "${subscriptionId}": ${getErrorMessage(error)}`,
};
}
}

function isDefinedRegistry(rg: Registry): rg is DefinedRegistry {
return rg.id !== undefined && rg.name !== undefined && rg.location !== undefined && rg.loginServer !== undefined;
}
231 changes: 231 additions & 0 deletions src/commands/utils/graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { AuthenticationProvider, Client } from "@microsoft/microsoft-graph-client";
import { Errorable, getErrorMessage } from "./errorable";
import { getDefaultScope, getEnvironment } from "../../auth/azureAuth";
import { GetAuthSessionOptions, ReadyAzureSessionProvider } from "../../auth/types";

const federatedIdentityCredentialIssuer = "https://token.actions.githubusercontent.com";
const federatedIdentityCredentialAudience = "api://AzureADTokenExchange";

type GraphListResult<T> = {
value: T[];
};

export type ApplicationParams = {
displayName: string;
};

export type Application = ApplicationParams & {
appId: string;
id: string;
};

export type ServicePrincipalParams = {
appId: string;
displayName?: string;
};

export type ServicePrincipal = ServicePrincipalParams & {
id: string;
displayName: string;
};

export type FederatedIdentityCredentialParams = {
name: string;
subject: string;
issuer: string;
description: string;
audiences: string[];
};

export type FederatedIdentityCredential = FederatedIdentityCredentialParams & {
id: string;
};

export function createGraphClient(sessionProvider: ReadyAzureSessionProvider): Client {
// The "Visual Studio Code" application id.
// ClientID seen on auth login for azure sign-in on vscode.
// Referenced here in azure identity source code: https://github.com/Azure/azure-sdk-for-net/blob/bba9347edf324ec3731cb31d5600fd379a76a20c/sdk/identity/Azure.Identity/src/Credentials/VisualStudioCodeCredential.cs#L29
const applicationClientId = "aebc6443-996d-45c2-90f0-388ff96faa56";

const baseUrl = getMicrosoftGraphClientBaseUrl();
const authProvider: AuthenticationProvider = {
getAccessToken: async (options) => {
const authSessionOptions: GetAuthSessionOptions = {
scopes: options?.scopes || [getDefaultScope(baseUrl)],
applicationClientId,
};

const session = await sessionProvider.getAuthSession(authSessionOptions);
return session.succeeded ? session.result.accessToken : "";
},
};

return Client.initWithMiddleware({ baseUrl, authProvider });
}

function getMicrosoftGraphClientBaseUrl(): string {
const environment = getEnvironment();
// Environments are from here: https://github.com/Azure/ms-rest-azure-env/blob/6fa17ce7f36741af6ce64461735e6c7c0125f0ed/lib/azureEnvironment.ts#L266-L346
// They do not contain the MS Graph endpoints, whose values are here:
// https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/d365ab1d68f90f2c38c67a5a7c7fe54acfc2584e/src/Constants.ts#L28
switch (environment.name) {
case "AzureChinaCloud":
return "https://microsoftgraph.chinacloudapi.cn";
case "AzureUSGovernment":
return "https://graph.microsoft.us";
case "AzureGermanCloud":
return "https://graph.microsoft.de";
}
return "https://graph.microsoft.com";
}

export async function getCurrentUserId(graphClient: Client): Promise<Errorable<string>> {
try {
const me = await graphClient.api("/me").get();
return { succeeded: true, result: me.id };
} catch (e) {
return { succeeded: false, error: getErrorMessage(e) };
}
}

export async function getOwnedApplications(graphClient: Client): Promise<Errorable<Application[]>> {
try {
const appSearchResults: GraphListResult<Application> = await graphClient
.api("/me/ownedObjects/microsoft.graph.application")
.select(["id", "appId", "displayName"])
.get();

return { succeeded: true, result: appSearchResults.value };
} catch (e) {
return { succeeded: false, error: getErrorMessage(e) };
}
}

export async function createApplication(graphClient: Client, applicationName: string): Promise<Errorable<Application>> {
const newApp: ApplicationParams = {
displayName: applicationName,
};

try {
const application: Application = await graphClient.api("/applications").post(newApp);

return { succeeded: true, result: application };
} catch (e) {
return { succeeded: false, error: getErrorMessage(e) };
}
}

export async function deleteApplication(graphClient: Client, applicationObjectId: string): Promise<Errorable<void>> {
try {
await graphClient.api(`/applications/${applicationObjectId}`).delete();
return { succeeded: true, result: undefined };
} catch (e) {
return { succeeded: false, error: getErrorMessage(e) };
}
}

export async function createServicePrincipal(
graphClient: Client,
applicationId: string,
): Promise<Errorable<ServicePrincipal>> {
const newServicePrincipal: ServicePrincipalParams = {
appId: applicationId,
};

try {
const servicePrincipal: ServicePrincipal = await graphClient
.api("/servicePrincipals")
.post(newServicePrincipal);
return { succeeded: true, result: servicePrincipal };
} catch (e) {
return { succeeded: false, error: getErrorMessage(e) };
}
}

export async function getServicePrincipalsForApp(
graphClient: Client,
appId: string,
): Promise<Errorable<ServicePrincipal[]>> {
try {
const spSearchResults: GraphListResult<ServicePrincipal> = await graphClient
.api("/servicePrincipals")
.filter(`appId eq '${appId}'`)
.get();

return { succeeded: true, result: spSearchResults.value };
} catch (e) {
return { succeeded: false, error: getErrorMessage(e) };
}
}

export async function getFederatedIdentityCredentials(
graphClient: Client,
applicationId: string,
): Promise<Errorable<FederatedIdentityCredential[]>> {
try {
const identityResults: GraphListResult<FederatedIdentityCredential> = await graphClient
.api(`/applications/${applicationId}/federatedIdentityCredentials`)
.get();

return { succeeded: true, result: identityResults.value };
} catch (e) {
return { succeeded: false, error: getErrorMessage(e) };
}
}

export async function createFederatedIdentityCredential(
graphClient: Client,
applicationId: string,
subject: string,
name: string,
description: string,
): Promise<Errorable<FederatedIdentityCredential>> {
const newCred: FederatedIdentityCredentialParams = {
name,
subject,
issuer: federatedIdentityCredentialIssuer,
description,
audiences: [federatedIdentityCredentialAudience],
};

try {
const cred: FederatedIdentityCredential = await graphClient
.api(`/applications/${applicationId}/federatedIdentityCredentials`)
.post(newCred);

return { succeeded: true, result: cred };
} catch (e) {
return { succeeded: false, error: getErrorMessage(e) };
}
} ////////TODO: catch fail logic if the same cred already exists

export async function createGitHubActionFederatedIdentityCredential(
graphClient: Client,
applicationId: string,
organization: string,
repository: string,
branch: string,
): Promise<Errorable<FederatedIdentityCredential>> {
const subject = `repo:${organization}/${repository}:ref:refs/heads/${branch}`;
return createFederatedIdentityCredential(graphClient, applicationId, subject, "gitHub_actions", "");
}

export async function deleteFederatedIdentityCredential(
graphClient: Client,
applicationId: string,
credId: string,
): Promise<Errorable<void>> {
try {
await graphClient.api(`/applications/${applicationId}/federatedIdentityCredentials/${credId}`).delete();
return { succeeded: true, result: undefined };
} catch (e) {
return { succeeded: false, error: getErrorMessage(e) };
}
}

export function findFederatedIdentityCredential(
subject: string,
creds: FederatedIdentityCredential[],
): FederatedIdentityCredential | undefined {
return creds.find((c) => c.subject === subject && c.issuer === federatedIdentityCredentialIssuer);
}
7 changes: 7 additions & 0 deletions src/commands/utils/roleAssignments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ export function getPrincipalRoleAssignmentsForAcr(

export function getScopeForAcr(subscriptionId: string, resourceGroup: string, acrName: string): string {
// ARM resource ID for ACR
// Doc reference: https://learn.microsoft.com/en-us/azure/templates/microsoft.containerregistry/registries?pivots=deployment-language-arm-template#resource-format-1
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/${acrProvider}/registries/${acrName}`;
}

export function getScopeForCluster(subscriptionId: string, resourceGroup: string, clusterName: string): string {
// ARM resource ID for AKS
// Doc reference: https://learn.microsoft.com/en-us/azure/templates/microsoft.containerservice/managedclusters?pivots=deployment-language-arm-template#resource-format-1
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.ContainerService/managedClusters/${clusterName}`;
}

// There are several permitted principal types, see: https://learn.microsoft.com/en-us/rest/api/authorization/role-assignments/create?view=rest-authorization-2022-04-01&tabs=HTTP#principaltype
// For now, 'ServicePrincipal' and 'User' are the ones we're most likely to use here,
// but we can add more as needed.
Expand Down

0 comments on commit db9bf62

Please sign in to comment.