Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto Deployments Utility Logic #1095

Merged
merged 3 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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