feat: more ergonomic RPC URL setting (#4413)

### Description

- Updates `scripts/secret-rpc-urls/set-rpc-urls.ts` to:
  - Interactively prompt for what RPC URLs to use
  - If desired, update K8s consumers (secrets & reset pods)

Usage:
```
yarn tsx ./scripts/secret-rpc-urls/set-rpc-urls.ts -e mainnet3 --chain lisk
```

or even
```
yarn tsx ./scripts/secret-rpc-urls/set-rpc-urls.ts -e mainnet3 --chains lisk zircuit
```

Removes `set-rpc-urls-from-registry` in favor of this new command, which
lets you do this as well.

Note that atm it doesn't consider warp monitors as dependent k8s
resources. This is because we haven't really defined infrastructure as
code for these -- the deployments that exist are kinda manually created
by passing in a config file. This should change but didn't feel
appropriate in this PR

The meatiest changes are in utils/rpcUrls.ts and utils/k8s.ts, the rest
were kinda drivebys to accommodate this

### Drive-by changes

- Various tweaks to labels used throughout k8s to be a bit more uniform
- Refactored a lot of helm related scripts to use `HelmManager` as a
superclass

### 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

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

None/Manual/Unit Tests
-->
pull/4433/head
Trevor Porter 3 months ago committed by GitHub
parent 88589eb2a3
commit b1d8bb8777
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      typescript/infra/helm/helloworld-kathy/templates/_helpers.tpl
  2. 4
      typescript/infra/helm/helloworld-kathy/templates/cycle-once-pod.yaml
  3. 4
      typescript/infra/helm/helloworld-kathy/templates/stateful-set.yaml
  4. 3
      typescript/infra/helm/key-funder/templates/cron-job.yaml
  5. 2
      typescript/infra/helm/warp-routes/templates/_helpers.tpl
  6. 4
      typescript/infra/helm/warp-routes/templates/stateful-set.yaml
  7. 1
      typescript/infra/package.json
  8. 4
      typescript/infra/scripts/agent-utils.ts
  9. 17
      typescript/infra/scripts/funding/deploy-key-funder.ts
  10. 14
      typescript/infra/scripts/helloworld/deploy-kathy.ts
  11. 44
      typescript/infra/scripts/secret-rpc-urls/set-rpc-urls-from-registry.ts
  12. 30
      typescript/infra/scripts/secret-rpc-urls/set-rpc-urls.ts
  13. 14
      typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts
  14. 44
      typescript/infra/scripts/warp-routes/helm.ts
  15. 76
      typescript/infra/src/agents/index.ts
  16. 109
      typescript/infra/src/funding/key-funder.ts
  17. 174
      typescript/infra/src/helloworld/kathy.ts
  18. 95
      typescript/infra/src/utils/helm.ts
  19. 140
      typescript/infra/src/utils/k8s.ts
  20. 386
      typescript/infra/src/utils/rpcUrls.ts
  21. 45
      typescript/infra/src/warp/helm.ts
  22. 202
      yarn.lock

@ -36,6 +36,8 @@ Common labels
{{- define "hyperlane.labels" -}} {{- define "hyperlane.labels" -}}
helm.sh/chart: {{ include "hyperlane.chart" . }} helm.sh/chart: {{ include "hyperlane.chart" . }}
hyperlane/deployment: {{ .Values.hyperlane.runEnv | quote }} hyperlane/deployment: {{ .Values.hyperlane.runEnv | quote }}
hyperlane/context: {{ .Values.hyperlane.context | quote }}
app.kubernetes.io/component: kathy
{{ include "hyperlane.selectorLabels" . }} {{ include "hyperlane.selectorLabels" . }}
{{- if .Chart.AppVersion }} {{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}

@ -4,9 +4,7 @@ kind: Pod
metadata: metadata:
name: {{ include "hyperlane.fullname" . }}-cycle-once-{{ (randAlphaNum 4 | nospace | lower) }} name: {{ include "hyperlane.fullname" . }}-cycle-once-{{ (randAlphaNum 4 | nospace | lower) }}
labels: &metadata_labels labels: &metadata_labels
hyperlane/deployment: {{ .Values.hyperlane.runEnv | quote }} {{- include "hyperlane.labels" . | nindent 4 }}
hyperlane/context: {{ .Values.hyperlane.context | quote }}
app.kubernetes.io/component: kathy
spec: spec:
restartPolicy: Never restartPolicy: Never
containers: containers:

@ -4,9 +4,7 @@ kind: StatefulSet
metadata: metadata:
name: {{ include "hyperlane.fullname" . }} name: {{ include "hyperlane.fullname" . }}
labels: &metadata_labels labels: &metadata_labels
hyperlane/deployment: {{ .Values.hyperlane.runEnv | quote }} {{- include "hyperlane.labels" . | nindent 4 }}
hyperlane/context: {{ .Values.hyperlane.context | quote }}
app.kubernetes.io/component: kathy
spec: spec:
selector: selector:
matchLabels: *metadata_labels matchLabels: *metadata_labels

@ -12,6 +12,9 @@ spec:
backoffLimit: 0 backoffLimit: 0
activeDeadlineSeconds: 14400 # 60 * 60 * 4 seconds = 4 hours activeDeadlineSeconds: 14400 # 60 * 60 * 4 seconds = 4 hours
template: template:
metadata:
labels:
{{- include "hyperlane.labels" . | nindent 12 }}
spec: spec:
restartPolicy: Never restartPolicy: Never
containers: containers:

@ -36,6 +36,8 @@ Common labels
{{- define "hyperlane.labels" -}} {{- define "hyperlane.labels" -}}
helm.sh/chart: {{ include "hyperlane.chart" . }} helm.sh/chart: {{ include "hyperlane.chart" . }}
hyperlane/deployment: {{ .Values.hyperlane.runEnv | quote }} hyperlane/deployment: {{ .Values.hyperlane.runEnv | quote }}
hyperlane/context: {{ .Values.hyperlane.context | quote }}
app.kubernetes.io/component: warp-routes
{{ include "hyperlane.selectorLabels" . }} {{ include "hyperlane.selectorLabels" . }}
{{- if .Chart.AppVersion }} {{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}

@ -4,9 +4,7 @@ kind: StatefulSet
metadata: metadata:
name: {{ include "hyperlane.fullname" . }} name: {{ include "hyperlane.fullname" . }}
labels: &metadata_labels labels: &metadata_labels
hyperlane/deployment: {{ .Values.hyperlane.runEnv | quote }} {{- include "hyperlane.labels" . | nindent 4 }}
hyperlane/context: {{ .Values.hyperlane.context | quote }}
app.kubernetes.io/component: warp-routes
spec: spec:
selector: selector:
matchLabels: *metadata_labels matchLabels: *metadata_labels

@ -17,6 +17,7 @@
"@hyperlane-xyz/registry": "2.5.0", "@hyperlane-xyz/registry": "2.5.0",
"@hyperlane-xyz/sdk": "5.1.0", "@hyperlane-xyz/sdk": "5.1.0",
"@hyperlane-xyz/utils": "5.1.0", "@hyperlane-xyz/utils": "5.1.0",
"@inquirer/prompts": "^5.3.8",
"@nomiclabs/hardhat-etherscan": "^3.0.3", "@nomiclabs/hardhat-etherscan": "^3.0.3",
"@safe-global/api-kit": "1.3.0", "@safe-global/api-kit": "1.3.0",
"@safe-global/protocol-kit": "1.3.0", "@safe-global/protocol-kit": "1.3.0",

@ -168,6 +168,10 @@ export function withChains<T>(args: Argv<T>) {
); );
} }
export function withChainsRequired<T>(args: Argv<T>) {
return withChains(args).demandOption('chains');
}
export function withWarpRouteId<T>(args: Argv<T>) { export function withWarpRouteId<T>(args: Argv<T>) {
return args.describe('warpRouteId', 'warp route id').string('warpRouteId'); return args.describe('warpRouteId', 'warp route id').string('warpRouteId');
} }

@ -1,14 +1,12 @@
import { Contexts } from '../../config/contexts.js'; import { Contexts } from '../../config/contexts.js';
import { import { environment } from '../../config/environments/mainnet3/chains.js';
getKeyFunderConfig, import { KeyFunderHelmManager } from '../../src/funding/key-funder.js';
runKeyFunderHelmCommand,
} from '../../src/funding/key-funder.js';
import { HelmCommand } from '../../src/utils/helm.js'; import { HelmCommand } from '../../src/utils/helm.js';
import { assertCorrectKubeContext } from '../agent-utils.js'; import { assertCorrectKubeContext } from '../agent-utils.js';
import { getConfigsBasedOnArgs } from '../core-utils.js'; import { getConfigsBasedOnArgs } from '../core-utils.js';
async function main() { async function main() {
const { agentConfig, envConfig } = await getConfigsBasedOnArgs(); const { agentConfig, envConfig, context } = await getConfigsBasedOnArgs();
if (agentConfig.context != Contexts.Hyperlane) if (agentConfig.context != Contexts.Hyperlane)
throw new Error( throw new Error(
`Invalid context ${agentConfig.context}, must be ${Contexts.Hyperlane}`, `Invalid context ${agentConfig.context}, must be ${Contexts.Hyperlane}`,
@ -16,13 +14,8 @@ async function main() {
await assertCorrectKubeContext(envConfig); await assertCorrectKubeContext(envConfig);
const keyFunderConfig = getKeyFunderConfig(envConfig); const manager = KeyFunderHelmManager.forEnvironment(environment);
await manager.runHelmCommand(HelmCommand.InstallOrUpgrade);
await runKeyFunderHelmCommand(
HelmCommand.InstallOrUpgrade,
agentConfig,
keyFunderConfig,
);
} }
main() main()

@ -1,20 +1,14 @@
import { runHelloworldKathyHelmCommand } from '../../src/helloworld/kathy.js'; import { KathyHelmManager } from '../../src/helloworld/kathy.js';
import { HelmCommand } from '../../src/utils/helm.js'; import { HelmCommand } from '../../src/utils/helm.js';
import { assertCorrectKubeContext } from '../agent-utils.js'; import { assertCorrectKubeContext } from '../agent-utils.js';
import { getConfigsBasedOnArgs } from '../core-utils.js'; import { getConfigsBasedOnArgs } from '../core-utils.js';
import { getHelloWorldConfig } from './utils.js';
async function main() { async function main() {
const { agentConfig, envConfig, context } = await getConfigsBasedOnArgs(); const { envConfig, environment, context } = await getConfigsBasedOnArgs();
await assertCorrectKubeContext(envConfig); await assertCorrectKubeContext(envConfig);
const kathyConfig = getHelloWorldConfig(envConfig, context).kathy;
await runHelloworldKathyHelmCommand( const manager = KathyHelmManager.forEnvironment(environment, context);
HelmCommand.InstallOrUpgrade, await manager.runHelmCommand(HelmCommand.InstallOrUpgrade);
agentConfig,
kathyConfig,
);
} }
main() main()

@ -1,44 +0,0 @@
import { getRegistryForEnvironment } from '../../src/config/chain.js';
import { setAndVerifyRpcUrls } from '../../src/utils/rpcUrls.js';
import { getArgs, withChains } from '../agent-utils.js';
async function main() {
const { environment, chains } = await withChains(getArgs()).argv;
if (!chains || chains.length === 0) {
console.error('No chains provided, Exiting.');
process.exit(1);
}
console.log(
`Setting RPC URLs for chains: ${chains.join(
', ',
)} in ${environment} environment`,
);
const registry = await getRegistryForEnvironment(
environment,
chains,
undefined,
false,
);
for (const chain of chains) {
console.log(`\nSetting RPC URLs for chain: ${chain}`);
const chainMetadata = await registry.getChainMetadata(chain);
if (!chainMetadata) {
console.error(`Chain ${chain} not found in registry. Continuing...`);
continue;
}
const rpcUrlsArray = chainMetadata.rpcUrls.map((rpc) => rpc.http);
await setAndVerifyRpcUrls(environment, chain, rpcUrlsArray);
}
}
main()
.then()
.catch((e) => {
console.error(e);
process.exit(1);
});

@ -1,22 +1,28 @@
import { setAndVerifyRpcUrls } from '../../src/utils/rpcUrls.js'; import { setRpcUrlsInteractive } from '../../src/utils/rpcUrls.js';
import { getArgs, withChainRequired, withRpcUrls } from '../agent-utils.js'; import {
assertCorrectKubeContext,
getArgs,
withChainsRequired,
} from '../agent-utils.js';
import { getEnvironmentConfig } from '../core-utils.js';
async function main() { async function main() {
const { environment, chain, rpcUrls } = await withRpcUrls( const { environment, chains } = await withChainsRequired(getArgs())
withChainRequired(getArgs()), // For ease of use and backward compatibility, we allow the `chain` argument to be
).argv; // singular or plural.
.alias('chain', 'chains').argv;
const rpcUrlsArray = rpcUrls await assertCorrectKubeContext(getEnvironmentConfig(environment));
.split(/,\s*/)
.filter(Boolean) // filter out empty strings
.map((url) => url.trim());
if (!rpcUrlsArray.length) { if (!chains || chains.length === 0) {
console.error('No rpc urls provided, Exiting.'); console.error('No chains provided, Exiting.');
process.exit(1); process.exit(1);
} }
await setAndVerifyRpcUrls(environment, chain, rpcUrlsArray); for (const chain of chains) {
console.log(`Setting RPC URLs for chain: ${chain}`);
await setRpcUrlsInteractive(environment, chain);
}
} }
main() main()

@ -1,8 +1,9 @@
import yargs from 'yargs'; import yargs from 'yargs';
import { HelmCommand } from '../../src/utils/helm.js'; import { HelmCommand } from '../../src/utils/helm.js';
import { WarpRouteMonitorHelmManager } from '../../src/warp/helm.js';
import { runWarpRouteHelmCommand } from './helm.js'; import { assertCorrectKubeContext } from '../agent-utils.js';
import { getEnvironmentConfig } from '../core-utils.js';
async function main() { async function main() {
const { filePath } = await yargs(process.argv.slice(2)) const { filePath } = await yargs(process.argv.slice(2))
@ -15,11 +16,10 @@ async function main() {
.string('filePath') .string('filePath')
.parse(); .parse();
await runWarpRouteHelmCommand( await assertCorrectKubeContext(getEnvironmentConfig('mainnet3'));
HelmCommand.InstallOrUpgrade,
'mainnet3', const helmManager = new WarpRouteMonitorHelmManager(filePath, 'mainnet3');
filePath, await helmManager.runHelmCommand(HelmCommand.InstallOrUpgrade);
);
} }
main() main()

@ -1,44 +0,0 @@
import path from 'path';
import { DeployEnvironment } from '../../src/config/environment.js';
import { HelmCommand, helmifyValues } from '../../src/utils/helm.js';
import { execCmd } from '../../src/utils/utils.js';
import { assertCorrectKubeContext } from '../agent-utils.js';
import { getEnvironmentConfig } from '../core-utils.js';
export async function runWarpRouteHelmCommand(
helmCommand: HelmCommand,
runEnv: DeployEnvironment,
configFilePath: string,
) {
const envConfig = getEnvironmentConfig(runEnv);
await assertCorrectKubeContext(envConfig);
const values = getWarpRoutesHelmValues(configFilePath);
const releaseName = getHelmReleaseName(configFilePath);
return execCmd(
`helm ${helmCommand} ${releaseName} ./helm/warp-routes --namespace ${runEnv} ${values.join(
' ',
)} --set fullnameOverride="${releaseName}"`,
);
}
function getHelmReleaseName(route: string): string {
const match = route.match(/\/([^/]+)-deployments\.yaml$/);
const name = match ? match[1] : route;
return `hyperlane-warp-route-${name.toLowerCase()}`; // helm requires lower case release names
}
function getWarpRoutesHelmValues(configFilePath: string) {
// The path should be relative to the monorepo root
const pathRelativeToMonorepoRoot = configFilePath.includes('typescript/infra')
? configFilePath
: path.join('typescript/infra', configFilePath);
const values = {
image: {
repository: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: '38ff1c4-20240823-093934',
},
configFilePath: pathRelativeToMonorepoRoot,
};
return helmifyValues(values);
}

@ -24,11 +24,7 @@ import {
getGcpSecretLatestVersionName, getGcpSecretLatestVersionName,
setGCPSecretUsingClient, setGCPSecretUsingClient,
} from '../utils/gcloud.js'; } from '../utils/gcloud.js';
import { import { HelmManager } from '../utils/helm.js';
HelmCommand,
buildHelmChartDependencies,
helmifyValues,
} from '../utils/helm.js';
import { import {
execCmd, execCmd,
getInfraPath, getInfraPath,
@ -47,9 +43,8 @@ if (!fs.existsSync(HELM_CHART_PATH + 'Chart.yaml'))
`Could not find helm chart at ${HELM_CHART_PATH}; the relative path may have changed.`, `Could not find helm chart at ${HELM_CHART_PATH}; the relative path may have changed.`,
); );
export abstract class AgentHelmManager { export abstract class AgentHelmManager extends HelmManager<HelmRootAgentValues> {
abstract readonly role: AgentRole; abstract readonly role: AgentRole;
abstract readonly helmReleaseName: string;
readonly helmChartPath: string = HELM_CHART_PATH; readonly helmChartPath: string = HELM_CHART_PATH;
protected abstract readonly config: AgentConfigHelper; protected abstract readonly config: AgentConfigHelper;
@ -70,55 +65,8 @@ export abstract class AgentHelmManager {
return this.config.namespace; return this.config.namespace;
} }
async runHelmCommand(action: HelmCommand, dryRun?: boolean): Promise<void> {
const cmd = ['helm', action];
if (dryRun) cmd.push('--dry-run');
if (action == HelmCommand.Remove) {
if (dryRun) cmd.push('--dry-run');
cmd.push(this.helmReleaseName, this.namespace);
await execCmd(cmd, {}, false, true);
return;
}
const values = helmifyValues(await this.helmValues());
if (action == HelmCommand.InstallOrUpgrade && !dryRun) {
// Delete secrets to avoid them being stale
const cmd = [
'kubectl',
'delete',
'secrets',
'--namespace',
this.namespace,
'--selector',
`app.kubernetes.io/instance=${this.helmReleaseName}`,
];
try {
await execCmd(cmd, {}, false, false);
} catch (e) {
console.error(e);
}
}
await buildHelmChartDependencies(this.helmChartPath);
cmd.push(
this.helmReleaseName,
this.helmChartPath,
'--create-namespace',
'--namespace',
this.namespace,
...values,
);
if (action == HelmCommand.UpgradeDiff) {
cmd.push(
`| kubectl diff --namespace ${this.namespace} --field-manager="Go-http-client" -f - || true`,
);
}
await execCmd(cmd, {}, false, true);
}
async helmValues(): Promise<HelmRootAgentValues> { async helmValues(): Promise<HelmRootAgentValues> {
const dockerImage = this.dockerImage(); const dockerImage = this.dockerImage;
return { return {
image: { image: {
repository: dockerImage.repo, repository: dockerImage.repo,
@ -155,21 +103,7 @@ export abstract class AgentHelmManager {
return this.config.agentRoleConfig.rpcConsensusType; return this.config.agentRoleConfig.rpcConsensusType;
} }
async doesAgentReleaseExist() { get dockerImage(): DockerConfig {
try {
await execCmd(
`helm status ${this.helmReleaseName} --namespace ${this.namespace}`,
{},
false,
false,
);
return true;
} catch (error) {
return false;
}
}
dockerImage(): DockerConfig {
return this.config.agentRoleConfig.docker; return this.config.agentRoleConfig.docker;
} }
@ -201,7 +135,7 @@ abstract class MultichainAgentHelmManager extends AgentHelmManager {
return parts.join('-'); return parts.join('-');
} }
dockerImage(): DockerConfig { get dockerImage(): DockerConfig {
return this.config.dockerImageForChain(this.chainName); return this.config.dockerImageForChain(this.chainName);
} }
} }

@ -1,66 +1,61 @@
import { join } from 'path';
import { Contexts } from '../../config/contexts.js';
import { getAgentConfig } from '../../scripts/agent-utils.js';
import { getEnvironmentConfig } from '../../scripts/core-utils.js';
import { AgentContextConfig } from '../config/agent/agent.js'; import { AgentContextConfig } from '../config/agent/agent.js';
import { EnvironmentConfig } from '../config/environment.js'; import { DeployEnvironment, EnvironmentConfig } from '../config/environment.js';
import { KeyFunderConfig } from '../config/funding.js'; import { KeyFunderConfig } from '../config/funding.js';
import { HelmCommand, helmifyValues } from '../utils/helm.js'; import { HelmCommand, HelmManager, helmifyValues } from '../utils/helm.js';
import { execCmd } from '../utils/utils.js'; import { execCmd, getInfraPath } from '../utils/utils.js';
export class KeyFunderHelmManager extends HelmManager {
readonly helmReleaseName: string = 'key-funder';
readonly helmChartPath: string = join(getInfraPath(), './helm/key-funder/');
export async function runKeyFunderHelmCommand( constructor(
helmCommand: HelmCommand, readonly config: KeyFunderConfig<string[]>,
agentConfig: AgentContextConfig, readonly agentConfig: AgentContextConfig,
keyFunderConfig: KeyFunderConfig<string[]>, ) {
) { super();
const values = getKeyFunderHelmValues(agentConfig, keyFunderConfig);
if (helmCommand === HelmCommand.InstallOrUpgrade) {
// Delete secrets to avoid them being stale
try {
await execCmd(
`kubectl delete secrets --namespace ${agentConfig.namespace} --selector app.kubernetes.io/instance=key-funder`,
{},
false,
false,
);
} catch (e) {
console.error(e);
}
} }
return execCmd( static forEnvironment(environment: DeployEnvironment): KeyFunderHelmManager {
`helm ${helmCommand} key-funder ./helm/key-funder --namespace ${ const envConfig = getEnvironmentConfig(environment);
keyFunderConfig.namespace const keyFunderConfig = getKeyFunderConfig(envConfig);
} ${values.join(' ')}`, // Always use Hyperlane context for key funder
{}, const agentConfig = getAgentConfig(Contexts.Hyperlane, environment);
false, return new KeyFunderHelmManager(keyFunderConfig, agentConfig);
true, }
);
}
function getKeyFunderHelmValues( get namespace() {
agentConfig: AgentContextConfig, return this.config.namespace;
keyFunderConfig: KeyFunderConfig<string[]>, }
) {
const values = { async helmValues() {
cronjob: { return {
schedule: keyFunderConfig.cronSchedule, cronjob: {
}, schedule: this.config.cronSchedule,
hyperlane: { },
runEnv: agentConfig.runEnv, hyperlane: {
// Only used for fetching RPC urls as env vars runEnv: this.agentConfig.runEnv,
chains: agentConfig.environmentChainNames, // Only used for fetching RPC urls as env vars
contextFundingFrom: keyFunderConfig.contextFundingFrom, chains: this.agentConfig.environmentChainNames,
contextsAndRolesToFund: keyFunderConfig.contextsAndRolesToFund, contextFundingFrom: this.config.contextFundingFrom,
desiredBalancePerChain: keyFunderConfig.desiredBalancePerChain, contextsAndRolesToFund: this.config.contextsAndRolesToFund,
desiredKathyBalancePerChain: keyFunderConfig.desiredKathyBalancePerChain, desiredBalancePerChain: this.config.desiredBalancePerChain,
igpClaimThresholdPerChain: keyFunderConfig.igpClaimThresholdPerChain, desiredKathyBalancePerChain: this.config.desiredKathyBalancePerChain,
}, igpClaimThresholdPerChain: this.config.igpClaimThresholdPerChain,
image: { },
repository: keyFunderConfig.docker.repo, image: {
tag: keyFunderConfig.docker.tag, repository: this.config.docker.repo,
}, tag: this.config.docker.tag,
infra: { },
prometheusPushGateway: keyFunderConfig.prometheusPushGateway, infra: {
}, prometheusPushGateway: this.config.prometheusPushGateway,
}; },
return helmifyValues(values); };
}
} }
export function getKeyFunderConfig( export function getKeyFunderConfig(

@ -1,97 +1,117 @@
import { join } from 'path';
import { Contexts } from '../../config/contexts.js'; import { Contexts } from '../../config/contexts.js';
import { getAgentConfig } from '../../scripts/agent-utils.js';
import { getEnvironmentConfig } from '../../scripts/core-utils.js';
import { getHelloWorldConfig } from '../../scripts/helloworld/utils.js';
import { AgentAwsUser } from '../agents/aws/user.js'; import { AgentAwsUser } from '../agents/aws/user.js';
import { AgentGCPKey } from '../agents/gcp.js'; import { AgentGCPKey } from '../agents/gcp.js';
import { AgentContextConfig } from '../config/agent/agent.js'; import { AgentContextConfig } from '../config/agent/agent.js';
import { DeployEnvironment } from '../config/environment.js';
import { import {
HelloWorldKathyConfig, HelloWorldKathyConfig,
HelloWorldKathyRunMode, HelloWorldKathyRunMode,
} from '../config/helloworld/types.js'; } from '../config/helloworld/types.js';
import { Role } from '../roles.js'; import { Role } from '../roles.js';
import { HelmCommand, helmifyValues } from '../utils/helm.js'; import {
import { execCmd } from '../utils/utils.js'; HelmCommand,
HelmManager,
HelmValues,
helmifyValues,
} from '../utils/helm.js';
import { execCmd, getInfraPath } from '../utils/utils.js';
export async function runHelloworldKathyHelmCommand( export class KathyHelmManager extends HelmManager<HelmValues> {
helmCommand: HelmCommand, readonly helmChartPath: string = join(
agentConfig: AgentContextConfig, getInfraPath(),
kathyConfig: HelloWorldKathyConfig, './helm/helloworld-kathy/',
) { );
// If using AWS keys, ensure the Kathy user and key has been created
if (agentConfig.aws) { constructor(
const awsUser = new AgentAwsUser( readonly config: HelloWorldKathyConfig,
agentConfig.runEnv, readonly agentConfig: AgentContextConfig,
agentConfig.context, ) {
Role.Kathy, super();
agentConfig.aws.region,
);
await awsUser.createIfNotExists();
await awsUser.createKeyIfNotExists(agentConfig);
} }
// Also ensure a GCP key exists, which is used for non-EVM chains even if static forEnvironment(
// the agent config is AWS-based environment: DeployEnvironment,
const kathyKey = new AgentGCPKey( context: Contexts,
agentConfig.runEnv, ): KathyHelmManager {
agentConfig.context, const envConfig = getEnvironmentConfig(environment);
Role.Kathy, const helloWorldConfig = getHelloWorldConfig(envConfig, context);
); const agentConfig = getAgentConfig(Contexts.Hyperlane, environment);
await kathyKey.createIfNotExists(); return new KathyHelmManager(helloWorldConfig.kathy, agentConfig);
}
get namespace() {
return this.config.namespace;
}
const values = getHelloworldKathyHelmValues(agentConfig, kathyConfig); get helmReleaseName(): string {
// For backward compatibility, keep the hyperlane context release name as
// 'helloworld-kathy', and add `-${context}` as a suffix for any other contexts
return `helloworld-kathy${
this.agentConfig.context === Contexts.Hyperlane ? '' : `-${context}`
}`;
}
return execCmd( async helmValues(): Promise<HelmValues> {
`helm ${helmCommand} ${getHelmReleaseName( const cycleOnce =
agentConfig.context, this.config.runConfig.mode === HelloWorldKathyRunMode.CycleOnce;
)} ./helm/helloworld-kathy --namespace ${ const fullCycleTime =
kathyConfig.namespace this.config.runConfig.mode === HelloWorldKathyRunMode.Service
} ${values.join(' ')}`, ? this.config.runConfig.fullCycleTime
{}, : '';
false,
true,
);
}
function getHelmReleaseName(context: Contexts): string { return {
// For backward compatibility, keep the hyperlane context release name as hyperlane: {
// 'helloworld-kathy', and add `-${context}` as a suffix for any other contexts runEnv: this.config.runEnv,
return `helloworld-kathy${ context: this.agentConfig.context,
context === Contexts.Hyperlane ? '' : `-${context}` // This is just used for fetching secrets, and is not actually
}`; // the list of chains that kathy will send to. Because Kathy
} // will fetch secrets for all chains in the environment, regardless
// of skipping them or not, we pass in all chains
chains: this.agentConfig.environmentChainNames,
aws: this.agentConfig.aws !== undefined,
function getHelloworldKathyHelmValues( chainsToSkip: this.config.chainsToSkip,
agentConfig: AgentContextConfig, messageSendTimeout: this.config.messageSendTimeout,
kathyConfig: HelloWorldKathyConfig, messageReceiptTimeout: this.config.messageReceiptTimeout,
) { cycleOnce,
const cycleOnce = fullCycleTime,
kathyConfig.runConfig.mode === HelloWorldKathyRunMode.CycleOnce; cyclesBetweenEthereumMessages:
const fullCycleTime = this.config.cyclesBetweenEthereumMessages,
kathyConfig.runConfig.mode === HelloWorldKathyRunMode.Service },
? kathyConfig.runConfig.fullCycleTime image: {
: ''; repository: this.config.docker.repo,
tag: this.config.docker.tag,
},
};
}
const values = { async runHelmCommand(action: HelmCommand, dryRun?: boolean): Promise<void> {
hyperlane: { // If using AWS keys, ensure the Kathy user and key has been created
runEnv: kathyConfig.runEnv, if (this.agentConfig.aws) {
context: agentConfig.context, const awsUser = new AgentAwsUser(
// This is just used for fetching secrets, and is not actually this.agentConfig.runEnv,
// the list of chains that kathy will send to. Because Kathy this.agentConfig.context,
// will fetch secrets for all chains in the environment, regardless Role.Kathy,
// of skipping them or not, we pass in all chains this.agentConfig.aws.region,
chains: agentConfig.environmentChainNames, );
aws: agentConfig.aws !== undefined, await awsUser.createIfNotExists();
await awsUser.createKeyIfNotExists(this.agentConfig);
}
chainsToSkip: kathyConfig.chainsToSkip, // Also ensure a GCP key exists, which is used for non-EVM chains even if
messageSendTimeout: kathyConfig.messageSendTimeout, // the agent config is AWS-based
messageReceiptTimeout: kathyConfig.messageReceiptTimeout, const kathyKey = new AgentGCPKey(
cycleOnce, this.agentConfig.runEnv,
fullCycleTime, this.agentConfig.context,
cyclesBetweenEthereumMessages: kathyConfig.cyclesBetweenEthereumMessages, Role.Kathy,
}, );
image: { await kathyKey.createIfNotExists();
repository: kathyConfig.docker.repo,
tag: kathyConfig.docker.tag,
},
};
return helmifyValues(values); super.runHelmCommand(action, dryRun);
}
} }

@ -1,3 +1,4 @@
import { DockerConfig } from '../config/agent/agent.js';
import { import {
HelmChartConfig, HelmChartConfig,
HelmChartRepositoryConfig, HelmChartRepositoryConfig,
@ -91,3 +92,97 @@ export function getDeployableHelmChartName(helmChartConfig: HelmChartConfig) {
export function buildHelmChartDependencies(chartPath: string) { export function buildHelmChartDependencies(chartPath: string) {
return execCmd(`cd ${chartPath} && helm dependency build`, {}, false, true); return execCmd(`cd ${chartPath} && helm dependency build`, {}, false, true);
} }
export type HelmValues = Record<string, any>;
export abstract class HelmManager<T = HelmValues> {
abstract readonly helmReleaseName: string;
abstract readonly helmChartPath: string;
abstract readonly namespace: string;
/**
* Returns the values to be passed to the helm chart.
* Expected to be an object of values.
*/
abstract helmValues(): Promise<T>;
async runHelmCommand(action: HelmCommand, dryRun?: boolean): Promise<void> {
const cmd = ['helm', action];
if (dryRun) cmd.push('--dry-run');
if (action == HelmCommand.Remove) {
if (dryRun) cmd.push('--dry-run');
cmd.push(this.helmReleaseName, this.namespace);
await execCmd(cmd, {}, false, true);
return;
}
const values = helmifyValues(await this.helmValues());
if (action == HelmCommand.InstallOrUpgrade && !dryRun) {
// Delete secrets to avoid them being stale
const cmd = [
'kubectl',
'delete',
'secrets',
'--namespace',
this.namespace,
'--selector',
`app.kubernetes.io/instance=${this.helmReleaseName}`,
];
try {
await execCmd(cmd, {}, false, false);
} catch (e) {
console.error(e);
}
}
await buildHelmChartDependencies(this.helmChartPath);
cmd.push(
this.helmReleaseName,
this.helmChartPath,
'--create-namespace',
'--namespace',
this.namespace,
...values,
);
if (action == HelmCommand.UpgradeDiff) {
cmd.push(
`| kubectl diff --namespace ${this.namespace} --field-manager="Go-http-client" -f - || true`,
);
}
await execCmd(cmd, {}, false, true);
}
async doesHelmReleaseExist() {
try {
await execCmd(
`helm status ${this.helmReleaseName} --namespace ${this.namespace}`,
{},
false,
false,
);
return true;
} catch (error) {
return false;
}
}
async getExistingK8sSecrets(): Promise<string[]> {
const [output] = await execCmd(
`kubectl get secret --selector=app.kubernetes.io/instance=${this.helmReleaseName} -o jsonpath='{.items[*].metadata.name}' -n ${this.namespace}`,
);
// Split on spaces and remove empty strings
return output.split(' ').filter(Boolean);
}
// Returns the names of all pods managed by a Statefulset in the helm release
async getManagedK8sPods() {
// Consider supporting Deployments in the future. For now, we only support StatefulSets because
// jsonpath doesn't support or operators well.
const [output] = await execCmd(
`kubectl get pods --selector=app.kubernetes.io/instance=${this.helmReleaseName} -o jsonpath='{range .items[?(@.metadata.ownerReferences[0].kind=="StatefulSet")]}{.metadata.name}{"\\n"}{end}' -n ${this.namespace}`,
);
// Split on new lines and remove empty strings
return output.split('\n').filter(Boolean);
}
}

@ -0,0 +1,140 @@
import { confirm } from '@inquirer/prompts';
import { pollAsync } from '@hyperlane-xyz/utils';
import { HelmManager } from './helm.js';
import { execCmd } from './utils.js';
export enum K8sResourceType {
SECRET = 'secret',
POD = 'pod',
}
export async function refreshK8sResources(
helmManagers: HelmManager<any>[],
resourceType: K8sResourceType,
namespace: string,
) {
const resourceNames = (
await Promise.all(
helmManagers.map(async (helmManager) => {
if (resourceType === K8sResourceType.SECRET) {
return helmManager.getExistingK8sSecrets();
} else if (resourceType === K8sResourceType.POD) {
return helmManager.getManagedK8sPods();
} else {
throw new Error(`Unknown resource type: ${resourceType}`);
}
}),
)
).flat();
console.log(`Ready to delete ${resourceType}s: ${resourceNames.join(', ')}`);
const cont = await confirm({
message: `Proceed and delete ${resourceNames.length} ${resourceType}s?`,
});
if (!cont) {
throw new Error('Aborting');
}
await execCmd(
`kubectl delete ${resourceType} ${resourceNames.join(' ')} -n ${namespace}`,
);
console.log(
`🏗 Deleted ${resourceNames.length} ${resourceType}s, waiting for them to be recreated...`,
);
await waitForK8sResources(resourceType, resourceNames, namespace);
}
// Polls until all resources are ready.
// For secrets, this means they exist.
// For pods, this means they exist and are running.
async function waitForK8sResources(
resourceType: K8sResourceType,
resourceNames: string[],
namespace: string,
) {
const resourceGetter =
resourceType === K8sResourceType.SECRET
? getExistingK8sSecrets
: getRunningK8sPods;
try {
const pollDelayMs = 2000;
const pollAttempts = 30;
await pollAsync(
async () => {
const { missing } = await resourceGetter(resourceNames, namespace);
if (missing.length > 0) {
console.log(
`${resourceNames.length - missing.length} of ${
resourceNames.length
} ${resourceType}s up, waiting for ${missing.length} more`,
);
throw new Error(
`${resourceType}s not ready, ${missing.length} missing`,
);
}
},
pollDelayMs,
pollAttempts,
);
console.log(`✅ All ${resourceNames.length} ${resourceType}s exist`);
} catch (e) {
console.error(`Error waiting for ${resourceType}s to exist: ${e}`);
}
}
async function getExistingK8sSecrets(
resourceNames: string[],
namespace: string,
): Promise<{
existing: string[];
missing: string[];
}> {
const [output] = await execCmd(
`kubectl get secret ${resourceNames.join(
' ',
)} -n ${namespace} --ignore-not-found -o jsonpath='{.items[*].metadata.name}'`,
);
const existing = output.split(' ').filter(Boolean);
const missing = resourceNames.filter(
(resource) => !existing.includes(resource),
);
return { existing, missing };
}
async function getRunningK8sPods(
resourceNames: string[],
namespace: string,
): Promise<{
existing: string[];
missing: string[];
}> {
// Returns a newline separated list of pod names and their statuses, e.g.:
// pod1:Running
// pod2:Pending
// pod3:Running
// Interestingly, providing names here is incompatible with the jsonpath range syntax. So we get all pods
// and filter.
const [output] = await execCmd(
`kubectl get pods -n ${namespace} --ignore-not-found -o jsonpath='{range .items[*]}{.metadata.name}:{.status.phase}{"\\n"}{end}'`,
);
// Filter out pods that are not in the list of resource names or are not running
const running = output
.split('\n')
.map((line) => {
const [pod, status] = line.split(':');
return resourceNames.includes(pod) && status === 'Running'
? pod
: undefined;
})
// TS isn't smart enough to know that the filter removes undefineds
.filter((pod) => pod !== undefined) as string[];
const missing = resourceNames.filter(
(resource) => !running.includes(resource),
);
return { existing: running, missing };
}

@ -1,46 +1,57 @@
import { confirm } from '@inquirer/prompts'; import input from '@inquirer/input';
import { Separator, checkbox, confirm } from '@inquirer/prompts';
import select from '@inquirer/select';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { timeout } from '@hyperlane-xyz/utils'; import { ChainName } from '@hyperlane-xyz/sdk';
import { ProtocolType, timeout } from '@hyperlane-xyz/utils';
import { Contexts } from '../../config/contexts.js';
import { getChain } from '../../config/registry.js';
import { getEnvironmentConfig } from '../../scripts/core-utils.js';
import { import {
RelayerHelmManager,
ScraperHelmManager,
ValidatorHelmManager,
getSecretRpcEndpoints, getSecretRpcEndpoints,
getSecretRpcEndpointsLatestVersionName, getSecretRpcEndpointsLatestVersionName,
secretRpcEndpointsExist, secretRpcEndpointsExist,
setSecretRpcEndpoints, setSecretRpcEndpoints,
} from '../agents/index.js'; } from '../agents/index.js';
import { DeployEnvironment } from '../config/environment.js';
import { KeyFunderHelmManager } from '../funding/key-funder.js';
import { KathyHelmManager } from '../helloworld/kathy.js';
import { disableGCPSecretVersion } from './gcloud.js'; import { disableGCPSecretVersion } from './gcloud.js';
import { isEthereumProtocolChain } from './utils.js'; import { HelmManager } from './helm.js';
import { K8sResourceType, refreshK8sResources } from './k8s.js';
export async function testProviders(rpcUrlsArray: string[]): Promise<boolean> { /**
let providersSucceeded = true; * Set the RPC URLs for the given chain in the given environment interactively.
for (const url of rpcUrlsArray) { * Includes an interactive experience for selecting new RPC URLs, confirming the change,
const provider = new ethers.providers.StaticJsonRpcProvider(url); * updating the secret, and refreshing dependent k8s resources.
try { * @param environment The environment to set the RPC URLs in
const blockNumber = await timeout(provider.getBlockNumber(), 5000); * @param chain The chain to set the RPC URLs for
console.log(`Valid provider for ${url} with block number ${blockNumber}`); */
} catch (e) { export async function setRpcUrlsInteractive(
console.error(`Provider failed: ${url}`);
providersSucceeded = false;
}
}
return providersSucceeded;
}
export async function setAndVerifyRpcUrls(
environment: string, environment: string,
chain: string, chain: string,
rpcUrlsArray: string[],
): Promise<void> { ): Promise<void> {
const secretPayload = JSON.stringify(rpcUrlsArray);
try { try {
await displayCurrentSecrets(environment, chain); const currentSecrets = await getAndDisplayCurrentSecrets(
await confirmSetSecrets(environment, chain, secretPayload); environment,
await testProvidersIfNeeded(chain, rpcUrlsArray); chain,
);
const newRpcUrls = await inputRpcUrls(chain, currentSecrets);
console.log(`Selected RPC URLs: ${formatRpcUrls(newRpcUrls)}\n`);
const secretPayload = JSON.stringify(newRpcUrls);
await confirmSetSecretsInteractive(environment, chain, secretPayload);
await updateSecretAndDisablePrevious(environment, chain, secretPayload); await updateSecretAndDisablePrevious(environment, chain, secretPayload);
await refreshDependentK8sResourcesInteractive(
environment as DeployEnvironment,
chain,
);
} catch (error: any) { } catch (error: any) {
console.error( console.error(
`Error occurred while setting RPC URLs for ${chain}:`, `Error occurred while setting RPC URLs for ${chain}:`,
@ -50,28 +61,167 @@ export async function setAndVerifyRpcUrls(
} }
} }
async function displayCurrentSecrets( async function getAndDisplayCurrentSecrets(
environment: string, environment: string,
chain: string, chain: string,
): Promise<void> { ): Promise<string[]> {
const secretExists = await secretRpcEndpointsExist(environment, chain); const secretExists = await secretRpcEndpointsExist(environment, chain);
if (!secretExists) { if (!secretExists) {
console.log( console.log(
`No secret rpc urls found for ${chain} in ${environment} environment\n`, `No secret rpc urls found for ${chain} in ${environment} environment\n`,
); );
} else { return [];
const currentSecrets = await getSecretRpcEndpoints(environment, chain); }
console.log(
`Current secrets found for ${chain} in ${environment} environment:\n${JSON.stringify( const currentSecrets = await getSecretRpcEndpoints(environment, chain);
currentSecrets, console.log(
null, `Current secrets found for ${chain} in ${environment} environment:\n${formatRpcUrls(
2, currentSecrets,
)}\n`, )}\n`,
); );
return currentSecrets;
}
// Copied from @inquirer/prompts as it's not exported :(
type Choice<Value> = {
value: Value;
name?: string;
description?: string;
short?: string;
disabled?: boolean | string;
type?: never;
};
/**
* Prompt the user to input RPC URLs for the given chain.
* The user can choose to input new URLs, use all registry URLs, or use existing URLs
* from secrets or the registry.
* @param chain The chain to input RPC URLs for
* @param existingUrls The existing RPC URLs for the chain
* @returns The selected RPC URLs
*/
async function inputRpcUrls(
chain: string,
existingUrls: string[],
): Promise<string[]> {
const selectedUrls: string[] = [];
const registryUrls = getChain(chain).rpcUrls.map((rpc) => rpc.http);
const existingUrlChoices: Array<Choice<string>> = existingUrls.map(
(url, i) => {
return {
value: url,
name: `${url} (existing index ${i})`,
};
},
);
const registryUrlChoices: Array<Choice<string>> = registryUrls.map(
(url, i) => {
return {
value: url,
name: `[PUBLIC] ${url} (registry index ${i})`,
};
},
);
enum SystemChoice {
ADD_NEW = 'Add new RPC URL',
DONE = 'Done',
USE_REGISTRY_URLS = 'Use all registry URLs',
REMOVE_LAST = 'Remove last RPC URL',
} }
const pushSelectedUrl = async (newUrl: string) => {
const providerHealthy = await testProvider(chain, newUrl);
if (!providerHealthy) {
const yes = await confirm({
message: `Provider at ${newUrl} is not healthy. Do you want to continue adding it?\n`,
});
if (!yes) {
console.log('Skipping provider');
return;
}
}
selectedUrls.push(newUrl);
};
const separator = new Separator('-----');
while (true) {
console.log(`Selected RPC URLs: ${formatRpcUrls(selectedUrls)}\n`);
// Sadly @inquirer/prompts doesn't expose the types needed here
const choices: (Separator | Choice<any>)[] = [
...[SystemChoice.DONE, SystemChoice.ADD_NEW].map((choice) => ({
value: choice,
})),
{
value: SystemChoice.USE_REGISTRY_URLS,
name: `Use registry URLs (${JSON.stringify(registryUrls)})`,
},
separator,
...existingUrlChoices,
separator,
...registryUrlChoices,
];
if (selectedUrls.length > 0) {
choices.push(separator);
choices.push({
value: SystemChoice.REMOVE_LAST,
});
}
const selection = await select({
message: 'Select RPC URL',
choices,
pageSize: 30,
});
if (selection === SystemChoice.DONE) {
console.log('Done selecting RPC URLs');
break;
} else if (selection === SystemChoice.ADD_NEW) {
const newUrl = await input({
message: 'Enter new RPC URL',
});
await pushSelectedUrl(newUrl);
} else if (selection === SystemChoice.REMOVE_LAST) {
selectedUrls.pop();
} else if (selection === SystemChoice.USE_REGISTRY_URLS) {
for (const url of registryUrls) {
await pushSelectedUrl(url);
}
console.log('Added all registry URLs');
break;
} else {
// If none of the above, a URL was chosen
let index = existingUrlChoices.findIndex(
(choice) => choice.value === selection,
);
if (index !== -1) {
existingUrlChoices.splice(index, 1);
}
index = registryUrlChoices.findIndex(
(choice) => choice.value === selection,
);
if (index !== -1) {
registryUrlChoices.splice(index, 1);
}
await pushSelectedUrl(selection);
}
console.log('========');
}
return selectedUrls;
} }
async function confirmSetSecrets( /**
* A prompt to confirm setting the given secret payload for the given chain in the given environment.
*/
async function confirmSetSecretsInteractive(
environment: string, environment: string,
chain: string, chain: string,
secretPayload: string, secretPayload: string,
@ -86,33 +236,13 @@ async function confirmSetSecrets(
} }
} }
async function testProvidersIfNeeded( /**
chain: string, * Non-interactively updates the secret for the given chain in the given environment with the given payload.
rpcUrlsArray: string[], * Disables the previous version of the secret if it exists.
): Promise<void> { * @param environment The environment to update the secret in
if (isEthereumProtocolChain(chain)) { * @param chain The chain to update the secret for
console.log('\nTesting providers...'); * @param secretPayload The new secret payload to set
const testPassed = await testProviders(rpcUrlsArray); */
if (!testPassed) {
console.error('At least one provider failed.');
throw new Error('Provider test failed');
}
const confirmedProviders = await confirm({
message: `All providers passed. Do you want to continue setting the secret?\n`,
});
if (!confirmedProviders) {
console.log('Continuing without setting secret.');
throw new Error('User cancelled operation after provider test');
}
} else {
console.log(
'Skipping provider testing as chain is not an Ethereum protocol chain.',
);
}
}
async function updateSecretAndDisablePrevious( async function updateSecretAndDisablePrevious(
environment: string, environment: string,
chain: string, chain: string,
@ -139,3 +269,129 @@ async function updateSecretAndDisablePrevious(
} }
} }
} }
/**
* Interactively refreshes dependent k8s resources for the given chain in the given environment.
* Allows for helm releases to be selected for refreshing. Refreshing involves first deleting
* secrets, expecting them to be recreated by external-secrets, and then deleting pods to restart
* them with the new secrets.
* @param environment The environment to refresh resources in
* @param chain The chain to refresh resources for
*/
async function refreshDependentK8sResourcesInteractive(
environment: DeployEnvironment,
chain: string,
): Promise<void> {
const cont = await confirm({
message: `Do you want to refresh dependent k8s resources for ${chain} in ${environment}?`,
});
if (!cont) {
console.log('Skipping refresh of k8s resources');
return;
}
const envConfig = getEnvironmentConfig(environment);
const contextHelmManagers: [string, HelmManager<any>][] = [];
const pushContextHelmManager = (
context: string,
manager: HelmManager<any>,
) => {
contextHelmManagers.push([context, manager]);
};
for (const [context, agentConfig] of Object.entries(envConfig.agents)) {
if (agentConfig.relayer) {
pushContextHelmManager(context, new RelayerHelmManager(agentConfig));
}
if (agentConfig.validators) {
pushContextHelmManager(
context,
new ValidatorHelmManager(agentConfig, chain),
);
}
if (agentConfig.scraper) {
pushContextHelmManager(context, new ScraperHelmManager(agentConfig));
}
if (context == Contexts.Hyperlane) {
// Key funder
pushContextHelmManager(
context,
KeyFunderHelmManager.forEnvironment(environment),
);
// Kathy - only expected to be running as a long-running service in the
// Hyperlane context
if (envConfig.helloWorld?.hyperlane?.addresses[chain]) {
pushContextHelmManager(
context,
KathyHelmManager.forEnvironment(environment, context),
);
}
}
}
const selection = await checkbox({
message:
'Select deployments to refresh (update secrets & restart any pods)',
choices: contextHelmManagers.map(([context, helmManager], i) => ({
name: `${helmManager.helmReleaseName} (context: ${context})`,
value: i,
// By default, all deployments are selected
checked: true,
})),
});
const selectedHelmManagers = contextHelmManagers
.map(([_, m]) => m)
.filter((_, m) => selection.includes(m));
await refreshK8sResources(
selectedHelmManagers,
K8sResourceType.SECRET,
environment,
);
await refreshK8sResources(
selectedHelmManagers,
K8sResourceType.POD,
environment,
);
}
/**
* Test the provider at the given URL, returning false if the provider is unhealthy
* or related to a different chain. No-op for non-Ethereum chains.
* @param chain The chain to test the provider for
* @param url The URL of the provider
*/
async function testProvider(chain: ChainName, url: string): Promise<boolean> {
const chainMetadata = getChain(chain);
if (chainMetadata.protocol !== ProtocolType.Ethereum) {
console.log(`Skipping provider test for non-Ethereum chain ${chain}`);
return true;
}
const provider = new ethers.providers.StaticJsonRpcProvider(url);
const expectedChainId = chainMetadata.chainId;
try {
const [blockNumber, providerNetwork] = await timeout(
Promise.all([provider.getBlockNumber(), provider.getNetwork()]),
5000,
);
if (providerNetwork.chainId !== expectedChainId) {
throw new Error(
`Expected chainId ${expectedChainId}, got ${providerNetwork.chainId}`,
);
}
console.log(
`✅ Valid provider for ${url} with block number ${blockNumber}`,
);
return true;
} catch (e) {
console.error(`Provider failed: ${url}\nError: ${e}`);
return false;
}
}
function formatRpcUrls(rpcUrls: string[]): string {
return JSON.stringify(rpcUrls, null, 2);
}

@ -0,0 +1,45 @@
import path from 'path';
import { DeployEnvironment } from '../../src/config/environment.js';
import { HelmManager } from '../../src/utils/helm.js';
import { getInfraPath } from '../../src/utils/utils.js';
export class WarpRouteMonitorHelmManager extends HelmManager {
readonly helmChartPath: string = path.join(
getInfraPath(),
'./helm/warp-routes',
);
constructor(
readonly configFilePath: string,
readonly runEnv: DeployEnvironment,
) {
super();
}
async helmValues() {
const pathRelativeToMonorepoRoot = this.configFilePath.includes(
'typescript/infra',
)
? this.configFilePath
: path.join('typescript/infra', this.configFilePath);
return {
image: {
repository: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: '38ff1c4-20240823-093934',
},
configFilePath: pathRelativeToMonorepoRoot,
fullnameOverride: this.helmReleaseName,
};
}
get namespace() {
return this.runEnv;
}
get helmReleaseName(): string {
const match = this.configFilePath.match(/\/([^/]+)-deployments\.yaml$/);
const name = match ? match[1] : this.configFilePath;
return `hyperlane-warp-route-${name.toLowerCase()}`; // helm requires lower case release names
}
}

@ -7460,6 +7460,7 @@ __metadata:
"@hyperlane-xyz/registry": "npm:2.5.0" "@hyperlane-xyz/registry": "npm:2.5.0"
"@hyperlane-xyz/sdk": "npm:5.1.0" "@hyperlane-xyz/sdk": "npm:5.1.0"
"@hyperlane-xyz/utils": "npm:5.1.0" "@hyperlane-xyz/utils": "npm:5.1.0"
"@inquirer/prompts": "npm:^5.3.8"
"@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-ethers": "npm:^2.2.3"
"@nomiclabs/hardhat-etherscan": "npm:^3.0.3" "@nomiclabs/hardhat-etherscan": "npm:^3.0.3"
"@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@nomiclabs/hardhat-waffle": "npm:^2.0.6"
@ -7646,6 +7647,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/checkbox@npm:^2.4.7":
version: 2.4.7
resolution: "@inquirer/checkbox@npm:2.4.7"
dependencies:
"@inquirer/core": "npm:^9.0.10"
"@inquirer/figures": "npm:^1.0.5"
"@inquirer/type": "npm:^1.5.2"
ansi-escapes: "npm:^4.3.2"
yoctocolors-cjs: "npm:^2.1.2"
checksum: 9bc0d6e9d6db90bcda3771d6b96e885e8c4e1f03d96a4fcd04b4eab2fafbecfafbced7a5cc24eca73f677452f9e354505f9cfb79a884dcf06772550845014d6f
languageName: node
linkType: hard
"@inquirer/confirm@npm:^2.0.6": "@inquirer/confirm@npm:^2.0.6":
version: 2.0.6 version: 2.0.6
resolution: "@inquirer/confirm@npm:2.0.6" resolution: "@inquirer/confirm@npm:2.0.6"
@ -7657,6 +7671,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/confirm@npm:^3.1.22":
version: 3.1.22
resolution: "@inquirer/confirm@npm:3.1.22"
dependencies:
"@inquirer/core": "npm:^9.0.10"
"@inquirer/type": "npm:^1.5.2"
checksum: 14e547ae3194c6447d41bb87135c03aa5598fd340fced19e4e8bae1be4ae54a9ad3cf335a9c3c6dc54e2ffb7928319e0f4b428531b8ce720cd23d2444292ca36
languageName: node
linkType: hard
"@inquirer/core@npm:^3.0.0": "@inquirer/core@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "@inquirer/core@npm:3.0.0" resolution: "@inquirer/core@npm:3.0.0"
@ -7679,6 +7703,27 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/core@npm:^9.0.10":
version: 9.0.10
resolution: "@inquirer/core@npm:9.0.10"
dependencies:
"@inquirer/figures": "npm:^1.0.5"
"@inquirer/type": "npm:^1.5.2"
"@types/mute-stream": "npm:^0.0.4"
"@types/node": "npm:^22.1.0"
"@types/wrap-ansi": "npm:^3.0.0"
ansi-escapes: "npm:^4.3.2"
cli-spinners: "npm:^2.9.2"
cli-width: "npm:^4.1.0"
mute-stream: "npm:^1.0.0"
signal-exit: "npm:^4.1.0"
strip-ansi: "npm:^6.0.1"
wrap-ansi: "npm:^6.2.0"
yoctocolors-cjs: "npm:^2.1.2"
checksum: 1bcb1deb7393d78f2dac5b8774d10692ad50b70e3ebc24684d13259d0c6c863dd1bce8ab4d4a806a6e90d5a2517aa8f9981993b1a256c9be68d9ef5e748481c6
languageName: node
linkType: hard
"@inquirer/editor@npm:^1.2.4": "@inquirer/editor@npm:^1.2.4":
version: 1.2.4 version: 1.2.4
resolution: "@inquirer/editor@npm:1.2.4" resolution: "@inquirer/editor@npm:1.2.4"
@ -7691,6 +7736,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/editor@npm:^2.1.22":
version: 2.1.22
resolution: "@inquirer/editor@npm:2.1.22"
dependencies:
"@inquirer/core": "npm:^9.0.10"
"@inquirer/type": "npm:^1.5.2"
external-editor: "npm:^3.1.0"
checksum: d36255567c88ea48bf1071b00c502d6a32bc1402966db4f9ae1be59d41d64d11e02111317d880d0bdc42fbfb1b819240fb229c89b07dfb804a6d5fb176ab8bb0
languageName: node
linkType: hard
"@inquirer/expand@npm:^1.1.5": "@inquirer/expand@npm:^1.1.5":
version: 1.1.5 version: 1.1.5
resolution: "@inquirer/expand@npm:1.1.5" resolution: "@inquirer/expand@npm:1.1.5"
@ -7703,6 +7759,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/expand@npm:^2.1.22":
version: 2.1.22
resolution: "@inquirer/expand@npm:2.1.22"
dependencies:
"@inquirer/core": "npm:^9.0.10"
"@inquirer/type": "npm:^1.5.2"
yoctocolors-cjs: "npm:^2.1.2"
checksum: f997ba916d3ddcc6e2563158805e2ae7a7a6f98e24cf0a08e23d4101b7d78f78e7dce28e648b85ca7f41759eeefdf1c6f6abf2bce0f041fbda54aacf68522454
languageName: node
linkType: hard
"@inquirer/figures@npm:^1.0.5":
version: 1.0.5
resolution: "@inquirer/figures@npm:1.0.5"
checksum: 60a51b2cdef03c89be25071c23d8c4ae427c56d8ac1b00bf054ca7be446674adc4edd66c15465fe6a81ff0726b024bf37f8a2903a8387ef968d33058da3e7a15
languageName: node
linkType: hard
"@inquirer/input@npm:^1.2.5": "@inquirer/input@npm:^1.2.5":
version: 1.2.5 version: 1.2.5
resolution: "@inquirer/input@npm:1.2.5" resolution: "@inquirer/input@npm:1.2.5"
@ -7714,6 +7788,26 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/input@npm:^2.2.9":
version: 2.2.9
resolution: "@inquirer/input@npm:2.2.9"
dependencies:
"@inquirer/core": "npm:^9.0.10"
"@inquirer/type": "npm:^1.5.2"
checksum: 9d0c97da9cc6972d4fb5bcb077e00e581aae90f6891d33c1c5e2f0324023c1772c6d5b03cd30ec7d4f71d22791d38bf45c47bafbe7dd9f74446693e7b120a2b0
languageName: node
linkType: hard
"@inquirer/number@npm:^1.0.10":
version: 1.0.10
resolution: "@inquirer/number@npm:1.0.10"
dependencies:
"@inquirer/core": "npm:^9.0.10"
"@inquirer/type": "npm:^1.5.2"
checksum: 0f9323b581e1c35ee8fbf2acde301c3e354896aeac83f6854e9672575090e0d092d19aadadb3477659079c403e63a3206bf668dd4c87e86834f775744f57c955
languageName: node
linkType: hard
"@inquirer/password@npm:^1.1.5": "@inquirer/password@npm:^1.1.5":
version: 1.1.5 version: 1.1.5
resolution: "@inquirer/password@npm:1.1.5" resolution: "@inquirer/password@npm:1.1.5"
@ -7725,6 +7819,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/password@npm:^2.1.22":
version: 2.1.22
resolution: "@inquirer/password@npm:2.1.22"
dependencies:
"@inquirer/core": "npm:^9.0.10"
"@inquirer/type": "npm:^1.5.2"
ansi-escapes: "npm:^4.3.2"
checksum: ce4e7c268f267c7436cf3a1b2890a9c92fddc2928bbe141d48f2f5a5daedbb3a2c601e44b0fe4e255792676ed241118ba69756b0d0b7d4492e0b7ee8fc548b90
languageName: node
linkType: hard
"@inquirer/prompts@npm:^3.0.0": "@inquirer/prompts@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "@inquirer/prompts@npm:3.0.0" resolution: "@inquirer/prompts@npm:3.0.0"
@ -7742,6 +7847,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/prompts@npm:^5.3.8":
version: 5.3.8
resolution: "@inquirer/prompts@npm:5.3.8"
dependencies:
"@inquirer/checkbox": "npm:^2.4.7"
"@inquirer/confirm": "npm:^3.1.22"
"@inquirer/editor": "npm:^2.1.22"
"@inquirer/expand": "npm:^2.1.22"
"@inquirer/input": "npm:^2.2.9"
"@inquirer/number": "npm:^1.0.10"
"@inquirer/password": "npm:^2.1.22"
"@inquirer/rawlist": "npm:^2.2.4"
"@inquirer/search": "npm:^1.0.7"
"@inquirer/select": "npm:^2.4.7"
checksum: e60eba0d64590c96fed722107962f433fbd8ff13f5d8a3ad6ba56964db69c8bc6b91a439b4e90209184090aacf73d84b0504e8c5a6a0f778ced70deb580ac1cd
languageName: node
linkType: hard
"@inquirer/rawlist@npm:^1.2.5": "@inquirer/rawlist@npm:^1.2.5":
version: 1.2.5 version: 1.2.5
resolution: "@inquirer/rawlist@npm:1.2.5" resolution: "@inquirer/rawlist@npm:1.2.5"
@ -7753,6 +7876,29 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/rawlist@npm:^2.2.4":
version: 2.2.4
resolution: "@inquirer/rawlist@npm:2.2.4"
dependencies:
"@inquirer/core": "npm:^9.0.10"
"@inquirer/type": "npm:^1.5.2"
yoctocolors-cjs: "npm:^2.1.2"
checksum: dd9d34a5cca081d53a9798cdfed2fdb61455dcfa856f54bc036dc5f57aceb95a7484487632c157bdba75e50de24990ebb3bb178ee765b8c0a735ff61b29cebf4
languageName: node
linkType: hard
"@inquirer/search@npm:^1.0.7":
version: 1.0.7
resolution: "@inquirer/search@npm:1.0.7"
dependencies:
"@inquirer/core": "npm:^9.0.10"
"@inquirer/figures": "npm:^1.0.5"
"@inquirer/type": "npm:^1.5.2"
yoctocolors-cjs: "npm:^2.1.2"
checksum: 3cd401cc1a7b01772e0e50ee27a0560cc647900f475d28a4f9b07843d4a85e1555c6adc1d7bc38ad2ef3546c524ca82c60272490d0bb159632c03cbe01f52bb1
languageName: node
linkType: hard
"@inquirer/select@npm:^1.2.5": "@inquirer/select@npm:^1.2.5":
version: 1.2.5 version: 1.2.5
resolution: "@inquirer/select@npm:1.2.5" resolution: "@inquirer/select@npm:1.2.5"
@ -7766,6 +7912,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/select@npm:^2.4.7":
version: 2.4.7
resolution: "@inquirer/select@npm:2.4.7"
dependencies:
"@inquirer/core": "npm:^9.0.10"
"@inquirer/figures": "npm:^1.0.5"
"@inquirer/type": "npm:^1.5.2"
ansi-escapes: "npm:^4.3.2"
yoctocolors-cjs: "npm:^2.1.2"
checksum: 854a3d0393073913f9bd3bf2e4ec7b8d114dfb48308a0a6698cf5c2c627da2700db5bdb69d054eaec89bd4e52a1274e493fa78d4fa26a5893972d91825456047
languageName: node
linkType: hard
"@inquirer/type@npm:^1.1.1": "@inquirer/type@npm:^1.1.1":
version: 1.1.1 version: 1.1.1
resolution: "@inquirer/type@npm:1.1.1" resolution: "@inquirer/type@npm:1.1.1"
@ -7773,6 +7932,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@inquirer/type@npm:^1.5.2":
version: 1.5.2
resolution: "@inquirer/type@npm:1.5.2"
dependencies:
mute-stream: "npm:^1.0.0"
checksum: 90d9203b5d7da8530e210c5421630b577f24554c8b683a4b45ea0f5c6a89c451771170aa34f2b62ca57e4be4de41d6761c941475e25c54c82b527c05644f181f
languageName: node
linkType: hard
"@isaacs/cliui@npm:^8.0.2": "@isaacs/cliui@npm:^8.0.2":
version: 8.0.2 version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2" resolution: "@isaacs/cliui@npm:8.0.2"
@ -12600,6 +12768,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/mute-stream@npm:^0.0.4":
version: 0.0.4
resolution: "@types/mute-stream@npm:0.0.4"
dependencies:
"@types/node": "npm:*"
checksum: af8d83ad7b68ea05d9357985daf81b6c9b73af4feacb2f5c2693c7fd3e13e5135ef1bd083ce8d5bdc8e97acd28563b61bb32dec4e4508a8067fcd31b8a098632
languageName: node
linkType: hard
"@types/node-fetch@npm:^2.6.1": "@types/node-fetch@npm:^2.6.1":
version: 2.6.9 version: 2.6.9
resolution: "@types/node-fetch@npm:2.6.9" resolution: "@types/node-fetch@npm:2.6.9"
@ -12696,6 +12873,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node@npm:^22.1.0":
version: 22.4.0
resolution: "@types/node@npm:22.4.0"
dependencies:
undici-types: "npm:~6.19.2"
checksum: 0b6ccc86856b8473f4d536491edc2ba21386d194219ee84024ef2b2ab054296f0b37a4f52719af797227132853cff065977992e353754a195cd86aea2e128cc7
languageName: node
linkType: hard
"@types/node@npm:^8.0.0": "@types/node@npm:^8.0.0":
version: 8.10.66 version: 8.10.66
resolution: "@types/node@npm:8.10.66" resolution: "@types/node@npm:8.10.66"
@ -15480,7 +15666,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cli-spinners@npm:^2.5.0": "cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.9.2":
version: 2.9.2 version: 2.9.2
resolution: "cli-spinners@npm:2.9.2" resolution: "cli-spinners@npm:2.9.2"
checksum: a0a863f442df35ed7294424f5491fa1756bd8d2e4ff0c8736531d886cec0ece4d85e8663b77a5afaf1d296e3cbbebff92e2e99f52bbea89b667cbe789b994794 checksum: a0a863f442df35ed7294424f5491fa1756bd8d2e4ff0c8736531d886cec0ece4d85e8663b77a5afaf1d296e3cbbebff92e2e99f52bbea89b667cbe789b994794
@ -15561,6 +15747,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cli-width@npm:^4.1.0":
version: 4.1.0
resolution: "cli-width@npm:4.1.0"
checksum: b58876fbf0310a8a35c79b72ecfcf579b354e18ad04e6b20588724ea2b522799a758507a37dfe132fafaf93a9922cafd9514d9e1598e6b2cd46694853aed099f
languageName: node
linkType: hard
"cliui@npm:^5.0.0": "cliui@npm:^5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "cliui@npm:5.0.0" resolution: "cliui@npm:5.0.0"
@ -30684,6 +30877,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"yoctocolors-cjs@npm:^2.1.2":
version: 2.1.2
resolution: "yoctocolors-cjs@npm:2.1.2"
checksum: d731e3ba776a0ee19021d909787942933a6c2eafb2bbe85541f0c59aa5c7d475ce86fcb860d5803105e32244c3dd5ba875b87c4c6bf2d6f297da416aa54e556f
languageName: node
linkType: hard
"zksync-web3@npm:^0.14.3": "zksync-web3@npm:^0.14.3":
version: 0.14.4 version: 0.14.4
resolution: "zksync-web3@npm:0.14.4" resolution: "zksync-web3@npm:0.14.4"

Loading…
Cancel
Save