import sleep from 'sleep-promise' ; import { execCmdWithExitOnFailure } from 'src/lib/cmd-utils' ; import { retryCmd } from 'src/lib/utils' ; import { getAksClusterConfig } from './context-utils' ; import { AksClusterConfig } from './k8s-cluster/aks' ; /**
 * getIdentity gets basic info on an existing identity. If the identity doesn't
 * exist, undefined is returned
 */ export async function getIdentity ( clusterConfig , identityName ) { const [ matchingIdentitiesStr ] = await execCmdWithExitOnFailure ( `az identity list -g ${ clusterConfig . resourceGroup } --query "[?name == '${ identityName }']" -o json` ) ; const matchingIdentities = JSON . parse ( matchingIdentitiesStr ) ; if ( ! matchingIdentities . length ) { return ; } return matchingIdentities [ 0 ] ; } export async function createIdentityIdempotent ( clusterConfig , identityName ) { const identity = await getIdentity ( clusterConfig , identityName ) ; if ( identity ) { console . info ( `Skipping identity creation, ${ identityName } in resource group ${ clusterConfig . resourceGroup } already exists` ) ; return identity ; } console . info ( `Creating identity ${ identityName } in resource group ${ clusterConfig . resourceGroup }` ) ; const [ results ] = await execCmdWithExitOnFailure ( `az identity create -n ${ identityName } -g ${ clusterConfig . resourceGroup } -o json` ) ; return JSON . parse ( results ) ; } /**
 * deleteIdentity gets basic info on an existing identity
 */ export function deleteIdentity ( clusterConfig , identityName ) { return execCmdWithExitOnFailure ( `az identity delete -n ${ identityName } -g ${ clusterConfig . resourceGroup } -o json` ) ; } async function roleIsAssigned ( assignee , scope , role ) { const [ matchingAssignedRoles ] = await retryCmd ( ( ) => execCmdWithExitOnFailure ( `az role assignment list --assignee ${ assignee } --scope ${ scope } --query "length([?roleDefinitionName == '${ role }'])" -o tsv` ) , 10 ) ; return parseInt ( matchingAssignedRoles . trim ( ) , 10 ) > 0 ; } export async function assignRoleIdempotent ( assigneeObjectId , assigneePrincipalType , scope , role ) { if ( await roleIsAssigned ( assigneeObjectId , scope , role ) ) { console . info ( `Skipping role assignment, role ${ role } already assigned to ${ assigneeObjectId } for scope ${ scope }` ) ; return ; } console . info ( `Assigning role ${ role } to ${ assigneeObjectId } type ${ assigneePrincipalType } for scope ${ scope }` ) ; await retryCmd ( ( ) => execCmdWithExitOnFailure ( `az role assignment create --role "${ role }" --assignee-object-id ${ assigneeObjectId } --assignee-principal-type ${ assigneePrincipalType } --scope ${ scope }` ) , 10 ) ; } export async function getAKSNodeResourceGroup ( clusterConfig ) { const [ nodeResourceGroup ] = await execCmdWithExitOnFailure ( `az aks show --name ${ clusterConfig . clusterName } --resource-group ${ clusterConfig . resourceGroup } --query nodeResourceGroup -o tsv` ) ; return nodeResourceGroup . trim ( ) ; } /**
 * Gets the AKS Service Principal Object ID if one exists. Otherwise, an empty string is given.
 */ export async function getAKSServicePrincipalObjectId ( clusterConfig ) { const [ rawServicePrincipalClientId ] = await execCmdWithExitOnFailure ( `az aks show -n ${ clusterConfig . clusterName } --query servicePrincipalProfile.clientId -g ${ clusterConfig . resourceGroup } -o tsv` ) ; const servicePrincipalClientId = rawServicePrincipalClientId . trim ( ) ; if ( servicePrincipalClientId === 'msi' ) { return '' ; } const [ rawObjectId ] = await execCmdWithExitOnFailure ( `az ad sp show --id ${ servicePrincipalClientId } --query objectId -o tsv` ) ; return rawObjectId . trim ( ) ; } /**
 * If an AKS cluster is using a managed service identity, the objectId is returned.
 * Otherwise, an empty string is given.
 */ export async function getAKSManagedServiceIdentityObjectId ( clusterConfig ) { const [ managedIdentityObjectId ] = await execCmdWithExitOnFailure ( `az aks show -n ${ clusterConfig . clusterName } --query identityProfile.kubeletidentity.objectId -g ${ clusterConfig . resourceGroup } -o tsv` ) ; return managedIdentityObjectId . trim ( ) ; } export async function registerStaticIPIfNotRegistered ( name , resourceGroupIP ) { const [ existingIpsStr ] = await execCmdWithExitOnFailure ( `az network public-ip list --resource-group ${ resourceGroupIP } --query "[?name == '${ name }' && sku.name == 'Standard'].ipAddress" -o json` ) ; const existingIps = JSON . parse ( existingIpsStr ) ; if ( existingIps . length ) { console . info ( `Skipping IP address registration, ${ name } on ${ resourceGroupIP } exists` ) ; return existingIps [ 0 ] ; } console . info ( `Registering IP address ${ name } on ${ resourceGroupIP }` ) ; const [ address ] = await execCmdWithExitOnFailure ( `az network public-ip create --resource-group ${ resourceGroupIP } --name ${ name } --allocation-method Static --sku Standard --query publicIp.ipAddress -o tsv` ) ; return address . trim ( ) ; } export async function deallocateStaticIP ( name , resourceGroupIP ) { console . info ( `Deallocating IP address ${ name } on ${ resourceGroupIP }` ) ; return execCmdWithExitOnFailure ( `az network public-ip delete --resource-group ${ resourceGroupIP } --name ${ name }` ) ; } export async function waitForStaticIPDetachment ( name , resourceGroup ) { const maxTryCount = 15 ; const tryIntervalMs = 3000 ; for ( let tryCount = 0 ; tryCount < maxTryCount ; tryCount ++ ) { const [ allocated ] = await execCmdWithExitOnFailure ( `az network public-ip show --resource-group ${ resourceGroup } --name ${ name } --query ipConfiguration.id -o tsv` ) ; if ( allocated . trim ( ) === '' ) { return true ; } await sleep ( tryIntervalMs ) ; } throw Error ( `Too many tries waiting for static IP association ID removal` ) ; } /**
 * This creates an Azure identity to access a key vault
 */ export async function createKeyVaultIdentityIfNotExists ( context , identityName , keyVaultName , keyVaultResourceGroup , keyPermissions , secretPermissions ) { const clusterConfig = getAksClusterConfig ( context ) ; const identity = await createIdentityIdempotent ( clusterConfig , identityName ) ; let assigneeObjectId = await getAKSServicePrincipalObjectId ( clusterConfig ) ; let assigneePrincipalType = 'ServicePrincipal' ; if ( ! assigneeObjectId ) { assigneeObjectId = await getAKSManagedServiceIdentityObjectId ( clusterConfig ) ; assigneePrincipalType = 'ServicePrincipal' ; } await assignRoleIdempotent ( assigneeObjectId , assigneePrincipalType , identity . id , 'Managed Identity Operator' ) ; await setKeyVaultPolicyIfNotSet ( clusterConfig , keyVaultName , keyVaultResourceGroup , identity , keyPermissions , secretPermissions ) ; return identity ; } async function setKeyVaultPolicyIfNotSet ( clusterConfig , keyVaultName , keyVaultResourceGroup , azureIdentity , keyPermissions , secretPermissions ) { const kvResourceGroup = keyVaultResourceGroup ? keyVaultResourceGroup : clusterConfig . resourceGroup ; const queryFilters = [ `?objectId == '${ azureIdentity . principalId }'` ] ; if ( keyPermissions ) { queryFilters . push ( `sort(permissions.keys) == [${ keyPermissions . map ( ( perm ) => `'${ perm }'` ) . join ( ', ' ) }]` ) ; } if ( secretPermissions ) { queryFilters . push ( `sort(permissions.secrets) == [${ secretPermissions . map ( ( perm ) => `'${ perm }'` ) . join ( ', ' ) }]` ) ; } const [ keyVaultPoliciesStr ] = await execCmdWithExitOnFailure ( `az keyvault show --name ${ keyVaultName } -g ${ kvResourceGroup } --query "properties.accessPolicies[${ queryFilters . join ( ' && ' ) }]"` ) ; const keyVaultPolicies = JSON . parse ( keyVaultPoliciesStr ) ; if ( keyVaultPolicies . length ) { const keyPermStr = keyPermissions ? `key permissions: ${ keyPermissions . join ( ' ' ) }` : '' ; const secretPermStr = secretPermissions ? `secret permissions: ${ secretPermissions . join ( ' ' ) }` : '' ; console . info ( `Skipping setting policy {${ keyPermStr }, ${ secretPermStr }}. Already set for vault ${ keyVaultName } and identity objectId ${ azureIdentity . principalId }` ) ; return ; } if ( keyPermissions ) { console . info ( `Setting key permissions ${ keyPermissions . join ( ' ' ) } for vault ${ keyVaultName } and identity objectId ${ azureIdentity . principalId }` ) ; return execCmdWithExitOnFailure ( `az keyvault set-policy --name ${ keyVaultName } --key-permissions ${ keyPermissions . join ( ' ' ) } --object-id ${ azureIdentity . principalId } -g ${ kvResourceGroup }` ) ; } if ( secretPermissions ) { console . info ( `Setting secret permissions ${ secretPermissions . join ( ' ' ) } for vault ${ keyVaultName } and identity objectId ${ azureIdentity . principalId }` ) ; return execCmdWithExitOnFailure ( `az keyvault set-policy --name ${ keyVaultName } --secret-permissions ${ secretPermissions . join ( ' ' ) } --object-id ${ azureIdentity . principalId } -g ${ kvResourceGroup }` ) ; } } /**
 * deleteAzureKeyVaultIdentity deletes the key vault policy and the managed identity
 */ export async function deleteAzureKeyVaultIdentity ( context , identityName , keyVaultName ) { const clusterConfig = getAksClusterConfig ( context ) ; await deleteKeyVaultPolicy ( clusterConfig , identityName , keyVaultName ) ; return deleteIdentity ( clusterConfig , identityName ) ; } async function deleteKeyVaultPolicy ( clusterConfig , identityName , keyVaultName ) { const azureIdentity = await getIdentity ( clusterConfig , identityName ) ; return execCmdWithExitOnFailure ( `az keyvault delete-policy --name ${ keyVaultName } --object-id ${ azureIdentity . principalId } -g ${ clusterConfig . resourceGroup }` ) ; } return export function getAzureKeyVaultIdentityName ( context , prefix , keyVaultName ) { const maxIdentityNameLength = 128 ; return `${ prefix }-${ keyVaultName }-${ context }` . substring ( 0 , maxIdentityNameLength ) ; }