Support agents with AWS keys (#361)
* Add some tracing to S3 reading and writing * Add AgentAwsUser, refactor into aws dir * Some signer changes * Add AWS credentials to relayer external secret * Redeploy dev * upgrade agents * Log when indexed messages * Looks like relayer is all good * Validator workin * Needs cleanup but nearly there * Get AWS credentials when needed * cleaning * mv some common fns * Fix output env vars * Final clean (I think) * Better fix for annoying race cond * prettier * rm some tracing * Add message_count to messages indexed logpull/372/head
parent
2d7234455d
commit
6593e47f1a
@ -1,7 +1,23 @@ |
||||
import { KEY_ROLE_ENUM } from '../agents'; |
||||
|
||||
export abstract class AgentKey { |
||||
abstract get identifier(): string; |
||||
abstract get address(): string; |
||||
abstract get credentialsAsHelmValue(): any; |
||||
|
||||
abstract fetch(): Promise<void>; |
||||
} |
||||
|
||||
export function isValidatorKey(role: string) { |
||||
return role === KEY_ROLE_ENUM.Validator; |
||||
} |
||||
|
||||
export function identifier( |
||||
environment: string, |
||||
role: string, |
||||
chainName: string, |
||||
index: number | undefined, |
||||
) { |
||||
return isValidatorKey(role) |
||||
? `abacus-${environment}-key-${chainName}-${role}-${index}` |
||||
: `abacus-${environment}-key-${role}`; |
||||
} |
||||
|
@ -0,0 +1,3 @@ |
||||
export { AgentAwsKey } from './key'; |
||||
export { AgentAwsUser } from './user'; |
||||
export { ValidatorAgentAwsUser } from './validator-user'; |
@ -0,0 +1,140 @@ |
||||
import { ChainName } from '@abacus-network/sdk'; |
||||
import { |
||||
IAMClient, |
||||
CreateAccessKeyCommand, |
||||
CreateUserCommand, |
||||
ListUsersCommand, |
||||
} from '@aws-sdk/client-iam'; |
||||
import { KEY_ROLE_ENUM } from '../../agents'; |
||||
import { AgentConfig } from '../../config'; |
||||
import { |
||||
fetchGCPSecret, |
||||
gcpSecretExists, |
||||
setGCPSecret, |
||||
} from '../../utils/gcloud'; |
||||
import { AgentAwsKey } from './key'; |
||||
|
||||
export class AgentAwsUser<Networks extends ChainName> { |
||||
private adminIamClient: IAMClient; |
||||
|
||||
private _arn: string | undefined; |
||||
|
||||
constructor( |
||||
public readonly environment: string, |
||||
public readonly chainName: Networks, |
||||
public readonly role: KEY_ROLE_ENUM, |
||||
public readonly region: string, |
||||
) { |
||||
this.adminIamClient = new IAMClient({ region }); |
||||
} |
||||
|
||||
// Creates the AWS user if it doesn't exist.
|
||||
// Gets API access keys and saves them in GCP secret manager if they do not already exist.
|
||||
// Populates `this._arn` with the ARN of the user.
|
||||
async createIfNotExists() { |
||||
const cmd = new ListUsersCommand({}); |
||||
const result = await this.adminIamClient.send(cmd); |
||||
const match = result.Users?.find((user) => user.UserName === this.userName); |
||||
if (match) { |
||||
this._arn = match?.Arn; |
||||
} else { |
||||
this._arn = await this.create(); |
||||
} |
||||
if (!(await this.accessKeysExist())) { |
||||
await this.createAndSaveAccessKey(); |
||||
} |
||||
} |
||||
|
||||
// Creates the AWS user
|
||||
async create() { |
||||
const cmd = new CreateUserCommand({ |
||||
UserName: this.userName, |
||||
Tags: this.awsTags, |
||||
}); |
||||
const response = await this.adminIamClient.send(cmd); |
||||
if (!response.User) { |
||||
throw Error('Expected User'); |
||||
} |
||||
return response.User.Arn; |
||||
} |
||||
|
||||
async accessKeysExist(): Promise<boolean> { |
||||
const accessKeyIdExists = await gcpSecretExists(this.accessKeyIdSecretName); |
||||
const secretAccessKeyExists = await gcpSecretExists( |
||||
this.secretAccessKeySecretName, |
||||
); |
||||
return accessKeyIdExists && secretAccessKeyExists; |
||||
} |
||||
|
||||
async getAccessKeys(): Promise<{ |
||||
accessKeyId: string; |
||||
secretAccessKey: string; |
||||
}> { |
||||
return { |
||||
accessKeyId: await fetchGCPSecret(this.accessKeyIdSecretName, false), |
||||
secretAccessKey: await fetchGCPSecret( |
||||
this.secretAccessKeySecretName, |
||||
false, |
||||
), |
||||
}; |
||||
} |
||||
|
||||
async createAndSaveAccessKey() { |
||||
const cmd = new CreateAccessKeyCommand({ |
||||
UserName: this.userName, |
||||
}); |
||||
const { AccessKey: accessKey } = await this.adminIamClient.send(cmd); |
||||
if (!accessKey || !accessKey.AccessKeyId || !accessKey.SecretAccessKey) { |
||||
throw Error('Expected fully defined access key'); |
||||
} |
||||
await setGCPSecret( |
||||
this.accessKeyIdSecretName, |
||||
accessKey.AccessKeyId, |
||||
this.tags, |
||||
); |
||||
await setGCPSecret( |
||||
this.secretAccessKeySecretName, |
||||
accessKey.SecretAccessKey, |
||||
this.tags, |
||||
); |
||||
} |
||||
|
||||
key(agentConfig: AgentConfig<Networks>): AgentAwsKey<Networks> { |
||||
return new AgentAwsKey<Networks>(agentConfig, this.chainName, this.role); |
||||
} |
||||
|
||||
get awsTags() { |
||||
const tags = this.tags; |
||||
return Object.keys(tags).map((key) => ({ |
||||
Key: key, |
||||
Value: tags[key], |
||||
})); |
||||
} |
||||
|
||||
get tags(): Record<string, string> { |
||||
return { |
||||
environment: this.environment, |
||||
role: this.role, |
||||
chain: this.chainName, |
||||
}; |
||||
} |
||||
|
||||
get userName() { |
||||
return `abacus-${this.environment}-${this.role}`; |
||||
} |
||||
|
||||
get accessKeyIdSecretName() { |
||||
return `${this.userName}-aws-access-key-id`; |
||||
} |
||||
|
||||
get secretAccessKeySecretName() { |
||||
return `${this.userName}-aws-secret-access-key`; |
||||
} |
||||
|
||||
get arn(): string { |
||||
if (!this._arn) { |
||||
throw Error('ARN is undefined'); |
||||
} |
||||
return this._arn; |
||||
} |
||||
} |
@ -0,0 +1,104 @@ |
||||
import { ChainName } from '@abacus-network/sdk'; |
||||
import { |
||||
S3Client, |
||||
CreateBucketCommand, |
||||
ListBucketsCommand, |
||||
PutBucketPolicyCommand, |
||||
} from '@aws-sdk/client-s3'; |
||||
import { KEY_ROLE_ENUM } from '../../agents'; |
||||
import { AgentConfig } from '../../config'; |
||||
import { AgentAwsKey } from './key'; |
||||
import { AgentAwsUser } from './user'; |
||||
|
||||
export class ValidatorAgentAwsUser< |
||||
Networks extends ChainName, |
||||
> extends AgentAwsUser<Networks> { |
||||
private adminS3Client: S3Client; |
||||
|
||||
constructor( |
||||
environment: string, |
||||
chainName: Networks, |
||||
public readonly index: number, |
||||
region: string, |
||||
public readonly bucket: string, |
||||
) { |
||||
super(environment, chainName, KEY_ROLE_ENUM.Validator, region); |
||||
this.adminS3Client = new S3Client({ region }); |
||||
} |
||||
|
||||
async createBucketIfNotExists() { |
||||
if (!(await this.bucketExists())) { |
||||
await this.createBucket(); |
||||
} |
||||
await this.putBucketAccessPolicy(); |
||||
} |
||||
|
||||
async bucketExists(): Promise<boolean> { |
||||
const cmd = new ListBucketsCommand({}); |
||||
const result = await this.adminS3Client.send(cmd); |
||||
return ( |
||||
result.Buckets?.find((bucket) => bucket.Name === this.bucket) !== |
||||
undefined |
||||
); |
||||
} |
||||
|
||||
async createBucket() { |
||||
const cmd = new CreateBucketCommand({ |
||||
Bucket: this.bucket, |
||||
}); |
||||
await this.adminS3Client.send(cmd); |
||||
} |
||||
|
||||
async putBucketAccessPolicy() { |
||||
const policy = { |
||||
Statement: [ |
||||
// Make the bucket publicly readable
|
||||
{ |
||||
Effect: 'Allow', |
||||
Principal: '*', |
||||
Action: ['s3:GetObject', 's3:ListBucket'], |
||||
Resource: [ |
||||
`arn:aws:s3:::${this.bucket}`, |
||||
`arn:aws:s3:::${this.bucket}/*`, |
||||
], |
||||
}, |
||||
// Allow the user to modify objects
|
||||
{ |
||||
Effect: 'Allow', |
||||
Principal: { |
||||
AWS: this.arn, |
||||
}, |
||||
Action: ['s3:DeleteObject', 's3:PutObject'], |
||||
Resource: `arn:aws:s3:::${this.bucket}/*`, |
||||
}, |
||||
], |
||||
}; |
||||
const cmd = new PutBucketPolicyCommand({ |
||||
Bucket: this.bucket, |
||||
Policy: JSON.stringify(policy), |
||||
}); |
||||
await this.adminS3Client.send(cmd); |
||||
} |
||||
|
||||
key(agentConfig: AgentConfig<Networks>): AgentAwsKey<Networks> { |
||||
return new AgentAwsKey<Networks>( |
||||
agentConfig, |
||||
this.chainName, |
||||
this.role, |
||||
this.index, |
||||
); |
||||
} |
||||
|
||||
get tags(): Record<string, string> { |
||||
return { |
||||
environment: this.environment, |
||||
role: this.role, |
||||
chain: this.chainName, |
||||
index: this.index!.toString(), |
||||
}; |
||||
} |
||||
|
||||
get userName() { |
||||
return `abacus-${this.environment}-${this.chainName}-${this.role}-${this.index}`; |
||||
} |
||||
} |
Loading…
Reference in new issue