/**
 * Copyright 2020 Opstrace, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ import { log , SECOND } from "@opstrace/utils" ; import { google , iam_v1 } from "googleapis" ; import { delay , call , CallEffect } from "redux-saga/effects" ; const iam = google . iam ( "v1" ) ; const resourcemanager = google . cloudresourcemanager ( "v1" ) ; async function authorize ( ) { const auth = new google . auth . GoogleAuth ( { scopes : [ "https://www.googleapis.com/auth/cloud-platform" ] } ) ; const authClient = await auth . getClient ( ) ; google . options ( { auth : authClient } ) ; } async function getServiceAccount ( { projectId , name } ) { const resource = `projects/${ projectId }/serviceAccounts/${ name }@${ projectId }.iam.gserviceaccount.com` ; try { await authorize ( ) ; const resp = await iam . projects . serviceAccounts . get ( { name : resource } ) ; return resp . data ; } catch ( e ) { if ( e . code === undefined || e . code !== 404 ) { throw e ; } } return undefined ; } async function createServiceAccount ( { accountId , projectId } ) { await authorize ( ) ; const params = { name : `projects/${ projectId }` , accountId : accountId } ; const sa = await iam . projects . serviceAccounts . create ( params ) ; return sa . data ; } async function getIAMPolicy ( { sa } ) { try { await authorize ( ) ; const resp = await resourcemanager . projects . getIamPolicy ( { resource : sa . projectId ?? undefined } ) ; return resp . data ; } catch ( e ) { if ( e . code === undefined || e . code !== 404 ) { throw e ; } } return undefined ; } async function ensurePolicyBindingExists ( { sa , role } ) { await authorize ( ) ; log . debug ( `apply policy to sa: ${ JSON . stringify ( sa , null , 2 ) }` ) ; const policy = ( await getIAMPolicy ( { sa } ) ) ?? { } ; if ( policy . bindings === undefined ) { policy . bindings = [ ] ; } const member = `serviceAccount:${ sa . email }` ; let done = false ; for ( const b of policy . bindings ) { if ( b . role === role ) { for ( const m of b . members ?? [ ] ) { if ( m === member ) { log . debug ( "policy is already set, nothing to do" ) ; done = true ; } } if ( ! done ) { log . debug ( "adding member to existing role" ) ; done = true ; const members = b . members ?? [ ] ; members . push ( member ) ; b . members = members ; } } } if ( ! done ) { log . debug ( "adding new policy binding" ) ; policy . bindings . push ( { members : [ member ] , role : role } ) ; } const request = { resource : sa . projectId ?? undefined , requestBody : { policy : policy } } ; log . debug ( `policy request: ${ JSON . stringify ( request , null , 2 ) }` ) ; await resourcemanager . projects . setIamPolicy ( request ) ; } async function ensureGSAKSALinkExists ( { kubernetesServiceAccount , sa } ) { await authorize ( ) ; const role = "roles/iam.workloadIdentityUser" ; const member = `serviceAccount:${ sa . projectId }.svc.id.goog[${ kubernetesServiceAccount }]` ; const policy = { bindings : [ { members : [ member ] , role : role } ] } ; await iam . projects . serviceAccounts . setIamPolicy ( { resource : sa . name ?? undefined , requestBody : { policy : policy } } ) ; } export function * ensureServiceAccountExists ( { name , projectId , role , kubernetesServiceAccount } ) { while ( true ) { try { const sa = ( yield call ( getServiceAccount , { projectId , name } ) ) ?? ( yield call ( createServiceAccount , { projectId , accountId : name } ) ) ; yield call ( ensurePolicyBindingExists , { sa , role } ) ; yield call ( ensureGSAKSALinkExists , { kubernetesServiceAccount , sa } ) ; return sa . email ; } catch ( e ) { log . error ( `caught error and will retry later: ${ JSON . stringify ( e , null , 2 ) }` ) ; yield delay ( 5 * SECOND ) ; } } } async function ensurePolicyBindingDoesNotExist ( { sa , role } ) { await authorize ( ) ; const policy = await getIAMPolicy ( { sa } ) ; if ( policy === undefined ) { throw new Error ( "failed to fetch IAM policy" ) ; } const member = `serviceAccount:${ sa . email }` ; for ( const b of policy . bindings ?? [ ] ) { if ( b . role === role ) { b . members = b . members ?. filter ( m => m !== member ) ; } } const request = { resource : sa . projectId ?? undefined , requestBody : { policy : policy } } ; log . debug ( `policy request: ${ JSON . stringify ( request , null , 2 ) }` ) ; await resourcemanager . projects . setIamPolicy ( request ) ; } async function deleteServiceAccount ( { sa } ) { await authorize ( ) ; await iam . projects . serviceAccounts . delete ( { name : sa . name ?? undefined } ) ; } export function * ensureServiceAccountDoesNotExist ( { name , projectId , role } ) { while ( true ) { try { const sa = yield call ( getServiceAccount , { projectId , name } ) ; if ( sa !== undefined ) { yield call ( ensurePolicyBindingDoesNotExist , { sa , role } ) ; yield call ( deleteServiceAccount , { sa } ) ; } return ; } catch ( e ) { log . error ( `caught error and will retry later: ${ JSON . stringify ( e , null , 2 ) }` ) ; yield delay ( 5 * SECOND ) ; } } }