feat(infra): Command to create endpoint secrets; cleaning up endpoints (#3922)

### Description

- clean up references to the single rpc secrets which are redundant as
the multi rpc secret includes the single one
- add get-rpc-urls script to get secret rpc urls for a given environment
and chain
- add set-rpc-urls script to: 1) create the secret if it does not exist
and add the first secret version or 2) add new secret version to an
existing secret and disable the previous version of the secret. The
script with test all rpc urls to ensure that they are valid


example usage:

get-rpc-urls 

`yarn tsx scripts/secret-rpc-urls/get-rpc-urls.ts -e testnet2 -c fuji`

set-rpc-urls

`yarn tsx scripts/secret-rpc-urls/set-rpc-urls.ts -e testnet2 -c fuji -r
https://api.avax-test.network/ext/bc/C/rpc,https://rpc.ankr.com/avalanche_fuji`

<!--
What's included in this PR?
-->

### Drive-by changes

<!--
Are there any minor or drive-by changes also included?
-->

- replace `withNetwork` util with `withChain` as we rarely use network
internally to reference a chain

### Related issues

<!--
- Fixes #[issue number here]
-->

### Backward compatibility

<!--
Are these changes backward compatible? Are there any infrastructure
implications, e.g. changes that would prohibit deploying older commits
using this infra tooling?

Yes/No
-->

### Testing
- manual

<!--
What kind of testing have these changes undergone?

None/Manual/Unit Tests
-->
pull/3928/head
Mohammed Hussan 6 months ago committed by GitHub
parent 942aba8e55
commit 36e9a2e783
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml
  2. 4
      typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml
  3. 4
      typescript/infra/helm/liquidity-layer-relayers/templates/env-var-external-secret.yaml
  4. 29
      typescript/infra/scripts/agent-utils.ts
  5. 5
      typescript/infra/scripts/check-rpc-urls.ts
  6. 10
      typescript/infra/scripts/deploy.ts
  7. 26
      typescript/infra/scripts/secret-rpc-urls/get-rpc-urls.ts
  8. 120
      typescript/infra/scripts/secret-rpc-urls/set-rpc-urls.ts
  9. 8
      typescript/infra/scripts/verify.ts
  10. 49
      typescript/infra/src/agents/index.ts
  11. 2
      typescript/infra/src/config/chain.ts
  12. 117
      typescript/infra/src/utils/gcloud.ts

@ -33,7 +33,6 @@ spec:
*/}}
{{- range .Values.hyperlane.chains }}
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }}
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINT_{{ . | upper }}: {{ printf "'{{ .%s_rpc | toString }}'" . }}
{{- end }}
{{- if .Values.hyperlane.aws }}
AWS_ACCESS_KEY_ID: {{ print "'{{ .aws_access_key_id | toString }}'" }}
@ -51,9 +50,6 @@ spec:
- secretKey: {{ printf "%s_rpcs" . }}
remoteRef:
key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }}
- secretKey: {{ printf "%s_rpc" . }}
remoteRef:
key: {{ printf "%s-rpc-endpoint-%s" $.Values.hyperlane.runEnv . }}
{{- end }}
{{- if .Values.hyperlane.aws }}
- secretKey: aws_access_key_id

@ -29,7 +29,6 @@ spec:
*/}}
{{- range .Values.hyperlane.chains }}
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }}
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINT_{{ . | upper }}: {{ printf "'{{ .%s_rpc | toString }}'" . }}
{{- end }}
data:
- secretKey: deployer_key
@ -43,7 +42,4 @@ spec:
- secretKey: {{ printf "%s_rpcs" . }}
remoteRef:
key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }}
- secretKey: {{ printf "%s_rpc" . }}
remoteRef:
key: {{ printf "%s-rpc-endpoint-%s" $.Values.hyperlane.runEnv . }}
{{- end }}

@ -29,7 +29,6 @@ spec:
*/}}
{{- range .Values.hyperlane.chains }}
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }}
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINT_{{ . | upper }}: {{ printf "'{{ .%s_rpc | toString }}'" . }}
{{- end }}
data:
- secretKey: deployer_key
@ -43,7 +42,4 @@ spec:
- secretKey: {{ printf "%s_rpcs" . }}
remoteRef:
key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }}
- secretKey: {{ printf "%s_rpc" . }}
remoteRef:
key: {{ printf "%s-rpc-endpoint-%s" $.Values.hyperlane.runEnv . }}
{{- end }}

@ -101,13 +101,6 @@ export function withModuleAndFork<T>(args: Argv<T>) {
.alias('f', 'fork');
}
export function withNetwork<T>(args: Argv<T>) {
return args
.describe('network', 'network to target')
.choices('network', getChains())
.alias('n', 'network');
}
export function withContext<T>(args: Argv<T>) {
return args
.describe('context', 'deploy context')
@ -117,6 +110,17 @@ export function withContext<T>(args: Argv<T>) {
.demandOption('context');
}
export function withChainRequired<T>(args: Argv<T>) {
return withChain(args).demandOption('chain');
}
export function withChain<T>(args: Argv<T>) {
return args
.describe('chain', 'chain name')
.choices('chain', getChains())
.alias('c', 'chain');
}
export function withProtocol<T>(args: Argv<T>) {
return args
.describe('protocol', 'protocol type')
@ -176,6 +180,17 @@ export function withConcurrentDeploy<T>(args: Argv<T>) {
.default('concurrentDeploy', false);
}
export function withRpcUrls<T>(args: Argv<T>) {
return args
.describe(
'rpcUrls',
'rpc urls in a comma separated list, in order of preference',
)
.string('rpcUrls')
.demandOption('rpcUrls')
.alias('r', 'rpcUrls');
}
// not requiring to build coreConfig to get agentConfig
export async function getAgentConfigsBasedOnArgs(argv?: {
environment: DeployEnvironment;

@ -15,10 +15,7 @@ async function main() {
const providers: [string, ethers.providers.JsonRpcProvider][] = [];
for (const chain of chains) {
rootLogger.debug(`Building providers for ${chain}`);
const rpcData = [
...(await getSecretRpcEndpoints(environment, chain, false)),
...(await getSecretRpcEndpoints(environment, chain, true)),
];
const rpcData = await getSecretRpcEndpoints(environment, chain);
for (const url of rpcData)
providers.push([chain, new ethers.providers.StaticJsonRpcProvider(url)]);
}

@ -42,10 +42,10 @@ import {
getArgs,
getModuleDirectory,
withBuildArtifactPath,
withChain,
withConcurrentDeploy,
withContext,
withModuleAndFork,
withNetwork,
} from './agent-utils.js';
import { getEnvironmentConfig, getHyperlaneCore } from './core-utils.js';
@ -55,12 +55,12 @@ async function main() {
module,
fork,
environment,
network,
chain,
buildArtifactPath,
concurrentDeploy,
} = await withContext(
withConcurrentDeploy(
withNetwork(withModuleAndFork(withBuildArtifactPath(getArgs()))),
withChain(withModuleAndFork(withBuildArtifactPath(getArgs()))),
),
).argv;
const envConfig = getEnvironmentConfig(environment);
@ -233,7 +233,7 @@ async function main() {
// prompt for confirmation in production environments
if (environment !== 'test' && !fork) {
const confirmConfig = network ? config[network] : config;
const confirmConfig = chain ? config[chain] : config;
console.log(JSON.stringify(confirmConfig, null, 2));
const { value: confirmed } = await prompts({
type: 'confirm',
@ -250,7 +250,7 @@ async function main() {
config,
deployer,
cache,
network ?? fork,
chain ?? fork,
agentConfig,
);
}

@ -0,0 +1,26 @@
import {
getSecretRpcEndpoints,
secretRpcEndpointsExist,
} from '../../src/agents/index.js';
import { getArgs, withChainRequired } from '../agent-utils.js';
async function main() {
const { environment, chain } = await withChainRequired(getArgs()).argv;
const secretExists = await secretRpcEndpointsExist(environment, chain);
if (!secretExists) {
console.log(
`No secret rpc urls found for ${chain} in ${environment} environment`,
);
process.exit(0);
}
const secrets = await getSecretRpcEndpoints(environment, chain);
console.log(secrets);
}
main()
.then()
.catch((e) => {
console.error(e);
process.exit(1);
});

@ -0,0 +1,120 @@
import { confirm } from '@inquirer/prompts';
import { ethers } from 'ethers';
import {
getSecretRpcEndpoints,
getSecretRpcEndpointsLatestVersionName,
secretRpcEndpointsExist,
setSecretRpcEndpoints,
} from '../../src/agents/index.js';
import { disableGCPSecretVersion } from '../../src/utils/gcloud.js';
import { isEthereumProtocolChain } from '../../src/utils/utils.js';
import { getArgs, withChainRequired, withRpcUrls } from '../agent-utils.js';
async function testProviders(rpcUrlsArray: string[]): Promise<boolean> {
let providersSucceeded = true;
for (const url of rpcUrlsArray) {
const provider = new ethers.providers.StaticJsonRpcProvider(url);
try {
const blockNumber = await provider.getBlockNumber();
console.log(`Valid provider for ${url} with block number ${blockNumber}`);
} catch (e) {
console.error(`Provider failed: ${url}`);
providersSucceeded = false;
}
}
return providersSucceeded;
}
async function main() {
const { environment, chain, rpcUrls } = await withRpcUrls(
withChainRequired(getArgs()),
).argv;
const rpcUrlsArray = rpcUrls
.split(/,\s*/)
.filter(Boolean) // filter out empty strings
.map((url) => url.trim());
if (!rpcUrlsArray.length) {
console.error('No rpc urls provided, Exiting.');
process.exit(1);
}
const secretPayload = JSON.stringify(rpcUrlsArray);
const secretExists = await secretRpcEndpointsExist(environment, chain);
if (!secretExists) {
console.log(
`No secret rpc urls found for ${chain} in ${environment} environment\n`,
);
} else {
const currentSecrets = await getSecretRpcEndpoints(environment, chain);
console.log(
`Current secrets found for ${chain} in ${environment} environment:\n${JSON.stringify(
currentSecrets,
null,
2,
)}\n`,
);
}
const confirmedSet = await confirm({
message: `Are you sure you want to set the following RPC URLs for ${chain} in ${environment}?\n${secretPayload}\n`,
});
if (!confirmedSet) {
console.log('Exiting without setting secret.');
process.exit(0);
}
if (isEthereumProtocolChain(chain)) {
console.log('\nTesting providers...');
const testPassed = await testProviders(rpcUrlsArray);
if (!testPassed) {
console.error('At least one provider failed. Exiting.');
process.exit(1);
}
const confirmedProviders = await confirm({
message: `All providers passed. Do you want to continue setting the secret?\n`,
});
if (!confirmedProviders) {
console.log('Exiting without setting secret.');
process.exit(0);
}
} else {
console.log(
'Skipping provider testing as chain is not an Ethereum protocol chain.',
);
}
let latestVersionName;
if (secretExists) {
latestVersionName = await getSecretRpcEndpointsLatestVersionName(
environment,
chain,
);
}
console.log(`Setting secret...`);
await setSecretRpcEndpoints(environment, chain, secretPayload);
console.log(`Added secret version!`);
if (latestVersionName) {
try {
await disableGCPSecretVersion(latestVersionName);
console.log(`Disabled previous version of the secret!`);
} catch (e) {
console.log(`Could not disable previous version of the secret`);
}
}
}
main()
.then()
.catch((e) => {
console.error(e);
process.exit(1);
});

@ -12,12 +12,12 @@ import {
} from '../src/deployment/verify.js';
import { readJSONAtPath } from '../src/utils/utils.js';
import { getArgs, withBuildArtifactPath, withNetwork } from './agent-utils.js';
import { getArgs, withBuildArtifactPath, withChain } from './agent-utils.js';
import { getEnvironmentConfig } from './core-utils.js';
async function main() {
const { environment, buildArtifactPath, verificationArtifactPath, network } =
await withNetwork(withBuildArtifactPath(getArgs()))
const { environment, buildArtifactPath, verificationArtifactPath, chain } =
await withChain(withBuildArtifactPath(getArgs()))
.string('verificationArtifactPath')
.describe(
'verificationArtifactPath',
@ -54,7 +54,7 @@ async function main() {
// verify all the things
const failedResults = (
await verifier.verify(network ? [network] : undefined)
await verifier.verify(chain ? [chain] : undefined)
).filter((result) => result.status === 'rejected');
// only log the failed verifications to console

@ -17,7 +17,12 @@ import { ScraperConfigHelper } from '../config/agent/scraper.js';
import { ValidatorConfigHelper } from '../config/agent/validator.js';
import { DeployEnvironment } from '../config/environment.js';
import { AgentRole, Role } from '../roles.js';
import { fetchGCPSecret } from '../utils/gcloud.js';
import {
fetchGCPSecret,
gcpSecretExistsUsingClient,
getGcpSecretLatestVersionName,
setGCPSecretUsingClient,
} from '../utils/gcloud.js';
import {
HelmCommand,
buildHelmChartDependencies,
@ -287,6 +292,13 @@ export class ValidatorHelmManager extends MultichainAgentHelmManager {
}
}
export function getSecretName(
environment: string,
chainName: ChainName,
): string {
return `${environment}-rpc-endpoints-${chainName}`;
}
export async function getSecretAwsCredentials(agentConfig: AgentContextConfig) {
return {
accessKeyId: await fetchGCPSecret(
@ -303,17 +315,11 @@ export async function getSecretAwsCredentials(agentConfig: AgentContextConfig) {
export async function getSecretRpcEndpoints(
environment: string,
chainName: ChainName,
multipleEndpoints = false,
): Promise<string[]> {
const secret = await fetchGCPSecret(
`${environment}-rpc-endpoint${multipleEndpoints ? 's' : ''}-${chainName}`,
multipleEndpoints,
);
if (typeof secret != 'string' && !Array.isArray(secret)) {
throw Error(`Expected secret for ${chainName} rpc endpoint`);
}
const secret = await fetchGCPSecret(getSecretName(environment, chainName));
if (!Array.isArray(secret)) {
return [secret.trimEnd()];
throw Error(`Expected secret for ${chainName} rpc endpoint`);
}
return secret.map((i) => {
@ -323,6 +329,29 @@ export async function getSecretRpcEndpoints(
});
}
export async function getSecretRpcEndpointsLatestVersionName(
environment: string,
chainName: ChainName,
) {
return getGcpSecretLatestVersionName(getSecretName(environment, chainName));
}
export async function secretRpcEndpointsExist(
environment: string,
chainName: ChainName,
): Promise<boolean> {
return gcpSecretExistsUsingClient(getSecretName(environment, chainName));
}
export async function setSecretRpcEndpoints(
environment: string,
chainName: ChainName,
endpoints: string,
) {
const secretName = getSecretName(environment, chainName);
await setGCPSecretUsingClient(secretName, endpoints);
}
export async function getSecretDeployerKey(
environment: DeployEnvironment,
context: Contexts,

@ -106,7 +106,7 @@ export async function getSecretMetadataOverrides(
const secretRpcUrls = await Promise.all(
chains.map(async (chain) => {
const rpcUrls = await getSecretRpcEndpoints(deployEnv, chain, true);
const rpcUrls = await getSecretRpcEndpoints(deployEnv, chain);
return {
chain,
rpcUrls,

@ -45,9 +45,7 @@ export async function fetchGCPSecret(
}
export async function fetchLatestGCPSecret(secretName: string) {
const client = new SecretManagerServiceClient({
projectId: GCP_PROJECT_ID,
});
const client = await getSecretManagerServiceClient();
const [secretVersion] = await client.accessSecretVersion({
name: `projects/${GCP_PROJECT_ID}/secrets/${secretName}/versions/latest`,
});
@ -84,6 +82,12 @@ function tryGCPSecretFromEnvVariable(gcpSecretName: string) {
return process.env[overrideEnvVarName];
}
/**
* Checks if a secret exists in GCP using the gcloud CLI.
* @deprecated Use gcpSecretExistsUsingClient instead.
* @param secretName The name of the secret to check.
* @returns A boolean indicating whether the secret exists.
*/
export async function gcpSecretExists(secretName: string) {
const fullName = `projects/${await getCurrentProjectNumber()}/secrets/${secretName}`;
debugLog(`Checking if GCP secret exists for ${fullName}`);
@ -95,6 +99,55 @@ export async function gcpSecretExists(secretName: string) {
return matches.length > 0;
}
/**
* Uses the SecretManagerServiceClient to check if a secret exists.
* @param secretName The name of the secret to check.
* @returns A boolean indicating whether the secret exists.
*/
export async function gcpSecretExistsUsingClient(
secretName: string,
client?: SecretManagerServiceClient,
): Promise<boolean> {
if (!client) {
client = await getSecretManagerServiceClient();
}
try {
const fullSecretName = `projects/${await getCurrentProjectNumber()}/secrets/${secretName}`;
const [secrets] = await client.listSecrets({
parent: `projects/${GCP_PROJECT_ID}`,
filter: `name=${fullSecretName}`,
});
return secrets.length > 0;
} catch (e) {
debugLog(`Error checking if secret exists: ${e}`);
throw e;
}
}
export async function getGcpSecretLatestVersionName(secretName: string) {
const client = await getSecretManagerServiceClient();
const [version] = await client.getSecretVersion({
name: `projects/${GCP_PROJECT_ID}/secrets/${secretName}/versions/latest`,
});
return version?.name;
}
export async function getSecretManagerServiceClient() {
return new SecretManagerServiceClient({
projectId: GCP_PROJECT_ID,
});
}
/**
* Sets a GCP secret using the gcloud CLI. Create secret if it doesn't exist and add a new version or update the existing one.
* @deprecated Use setGCPSecretUsingClient instead.
* @param secretName The name of the secret to set.
* @param secret The secret to set.
* @param labels The labels to set on the secret.
*/
export async function setGCPSecret(
secretName: string,
secret: string,
@ -121,6 +174,64 @@ export async function setGCPSecret(
await rm(fileName);
}
/**
* Sets a GCP secret using the SecretManagerServiceClient. Create secret if it doesn't exist and add a new version or update the existing one.
* @param secretName The name of the secret to set.
* @param secret The secret to set.
*/
export async function setGCPSecretUsingClient(
secretName: string,
secret: string,
labels?: Record<string, string>,
) {
const client = await getSecretManagerServiceClient();
const exists = await gcpSecretExistsUsingClient(secretName, client);
if (!exists) {
// Create the secret
await client.createSecret({
parent: `projects/${GCP_PROJECT_ID}`,
secretId: secretName,
secret: {
name: secretName,
replication: {
automatic: {},
},
labels,
},
});
debugLog(`Created new GCP secret for ${secretName}`);
}
await addGCPSecretVersion(secretName, secret, client);
}
export async function addGCPSecretVersion(
secretName: string,
secret: string,
client?: SecretManagerServiceClient,
) {
if (!client) {
client = await getSecretManagerServiceClient();
}
const [version] = await client.addSecretVersion({
parent: `projects/${GCP_PROJECT_ID}/secrets/${secretName}`,
payload: {
data: Buffer.from(secret, 'utf8'),
},
});
debugLog(`Added secret version ${version?.name}`);
}
export async function disableGCPSecretVersion(secretName: string) {
const client = await getSecretManagerServiceClient();
const [version] = await client.disableSecretVersion({
name: secretName,
});
debugLog(`Disabled secret version ${version?.name}`);
}
// Returns the email of the service account
export async function createServiceAccountIfNotExists(
serviceAccountName: string,

Loading…
Cancel
Save