feat(infra): Add check-warp-deploy cron job (#4370)

### Description

- add helm chart, helper functions and boilerplate to support
check-warp-deploy cron job

### Drive-by changes

- refactor check-deploy to call `getGovernor`

### Testing

Manual
pull/4399/head
Mohammed Hussan 3 months ago committed by GitHub
parent 24c8188e55
commit 70830ffb3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      typescript/infra/config/environments/mainnet3/index.ts
  2. 13
      typescript/infra/config/environments/mainnet3/warp/checkWarpDeploy.ts
  3. 6
      typescript/infra/helm/check-warp-deploy/Chart.yaml
  4. 42
      typescript/infra/helm/check-warp-deploy/templates/_helpers.tpl
  5. 32
      typescript/infra/helm/check-warp-deploy/templates/cron-job.yaml
  6. 45
      typescript/infra/helm/check-warp-deploy/templates/env-var-external-secret.yaml
  7. 12
      typescript/infra/helm/check-warp-deploy/values.yaml
  8. 4
      typescript/infra/helm/key-funder/values.yaml
  9. 7
      typescript/infra/scripts/agent-utils.ts
  10. 60
      typescript/infra/scripts/check/check-deploy.ts
  11. 157
      typescript/infra/scripts/check/check-utils.ts
  12. 92
      typescript/infra/scripts/check/check-warp-deploy.ts
  13. 85
      typescript/infra/scripts/check/deploy-check-warp-deploy.ts
  14. 3
      typescript/infra/src/config/environment.ts
  15. 10
      typescript/infra/src/config/funding.ts

@ -19,6 +19,7 @@ import { infrastructure } from './infrastructure.js';
import { bridgeAdapterConfigs, relayerConfig } from './liquidityLayer.js';
import { ethereumChainOwners } from './owners.js';
import { supportedChainNames } from './supportedChainNames.js';
import { checkWarpDeployConfig } from './warp/checkWarpDeploy.js';
export const environment: EnvironmentConfig = {
environment: environmentName,
@ -51,6 +52,7 @@ export const environment: EnvironmentConfig = {
infra: infrastructure,
helloWorld,
keyFunderConfig,
checkWarpDeployConfig,
liquidityLayerConfig: {
bridgeAdapters: bridgeAdapterConfigs,
relayer: relayerConfig,

@ -0,0 +1,13 @@
import { CheckWarpDeployConfig } from '../../../../src/config/funding.js';
import { environment } from '../chains.js';
export const checkWarpDeployConfig: CheckWarpDeployConfig = {
docker: {
repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: '36f7e14-20240823-160646',
},
namespace: environment,
cronSchedule: '0 15 * * *', // set to 3pm utc every day
prometheusPushGateway:
'http://prometheus-prometheus-pushgateway.monitoring.svc.cluster.local:9091',
};

@ -0,0 +1,6 @@
apiVersion: v2
name: check-warp-deploy
description: Check warp deploy
type: application
version: 0.1.0
appVersion: '1.16.0'

@ -0,0 +1,42 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "hyperlane.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "hyperlane.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "hyperlane.labels" -}}
helm.sh/chart: {{ include "hyperlane.chart" . }}
hyperlane/deployment: {{ .Values.hyperlane.runEnv | quote }}
hyperlane/context: "hyperlane"
{{ include "hyperlane.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "hyperlane.selectorLabels" -}}
app.kubernetes.io/name: {{ include "hyperlane.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
The name of the ClusterSecretStore
*/}}
{{- define "hyperlane.cluster-secret-store.name" -}}
{{- default "external-secrets-gcp-cluster-secret-store" .Values.externalSecrets.clusterSecretStore }}
{{- end }}

@ -0,0 +1,32 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: check-warp-deploy
spec:
schedule: "{{ .Values.cronjob.schedule }}"
successfulJobsHistoryLimit: {{ .Values.cronjob.successfulJobsHistoryLimit }}
failedJobsHistoryLimit: {{ .Values.cronjob.failedJobsHistoryLimit }}
concurrencyPolicy: Forbid
jobTemplate:
spec:
backoffLimit: 0
activeDeadlineSeconds: 14400
template:
spec:
restartPolicy: Never
containers:
- name: check-warp-deploy
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: IfNotPresent
command:
- ./node_modules/.bin/tsx
- ./typescript/infra/scripts/check/check-warp-deploy.ts
- -e
- {{ .Values.hyperlane.runEnv }}
- --pushMetrics
env:
- name: PROMETHEUS_PUSH_GATEWAY
value: {{ .Values.infra.prometheusPushGateway }}
envFrom:
- secretRef:
name: check-warp-deploy-env-var-secret

@ -0,0 +1,45 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: check-warp-deploy-env-var-external-secret
labels:
{{- include "hyperlane.labels" . | nindent 4 }}
spec:
secretStoreRef:
name: {{ include "hyperlane.cluster-secret-store.name" . }}
kind: ClusterSecretStore
refreshInterval: "1h"
# The secret that will be created
target:
name: check-warp-deploy-env-var-secret
template:
type: Opaque
metadata:
labels:
{{- include "hyperlane.labels" . | nindent 10 }}
annotations:
update-on-redeploy: "{{ now }}"
data:
GCP_SECRET_OVERRIDES_ENABLED: "true"
GCP_SECRET_OVERRIDE_HYPERLANE_{{ .Values.hyperlane.runEnv | upper }}_KEY_DEPLOYER: {{ print "'{{ .deployer_key | toString }}'" }}
{{/*
* For each network, create an environment variable with the RPC endpoint.
* The templating of external-secrets will use the data section below to know how
* to replace the correct value in the created secret.
*/}}
{{- range .Values.hyperlane.chains }}
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }}
{{- end }}
data:
- secretKey: deployer_key
remoteRef:
key: {{ printf "hyperlane-%s-key-deployer" .Values.hyperlane.runEnv }}
{{/*
* For each network, load the secret in GCP secret manager with the form: environment-rpc-endpoint-network,
* and associate it with the secret key networkname_rpc.
*/}}
{{- range .Values.hyperlane.chains }}
- secretKey: {{ printf "%s_rpcs" . }}
remoteRef:
key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }}
{{- end }}

@ -0,0 +1,12 @@
image:
repository: gcr.io/hyperlane-labs-dev/hyperlane-monorepo
tag:
hyperlane:
runEnv: mainnet3
chains: []
cronjob:
schedule: '0 15 * * *'
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 1
externalSecrets:
clusterSecretStore:

@ -9,9 +9,9 @@ hyperlane:
# key = context, value = array of roles to fund
contextsAndRolesToFund:
hyperlane:
- relayer
- relayer
cronjob:
schedule: "*/10 * * * *" # Every 10 minutes
schedule: '*/10 * * * *' # Every 10 minutes
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 1
externalSecrets:

@ -114,6 +114,13 @@ export function withContext<T>(args: Argv<T>) {
.demandOption('context');
}
export function withPushMetrics<T>(args: Argv<T>) {
return args
.describe('pushMetrics', 'Push metrics to prometheus')
.boolean('pushMetrics')
.default('pushMetrics', false);
}
export function withAsDeployer<T>(args: Argv<T>) {
return args
.describe('asDeployer', 'Set signer to the deployer key')

@ -1,7 +1,63 @@
import { check } from './check-utils.js';
import {
getCheckDeployArgs,
getGovernor,
logViolations,
} from './check-utils.js';
async function main() {
await check();
const {
module,
context,
environment,
asDeployer,
chain,
fork,
govern,
warpRouteId,
} = await getCheckDeployArgs().argv;
const governor = await getGovernor(
module,
context,
environment,
asDeployer,
warpRouteId,
chain,
fork,
govern,
);
if (fork) {
await governor.checker.checkChain(fork);
if (govern) {
await governor.govern(false, fork);
}
} else if (chain) {
await governor.checker.checkChain(chain);
if (govern) {
await governor.govern(true, chain);
}
} else {
await governor.checker.check();
if (govern) {
await governor.govern();
}
}
if (!govern) {
const violations = governor.checker.violations;
if (violations.length > 0) {
logViolations(violations);
if (!fork) {
throw new Error(
`Checking ${module} deploy yielded ${violations.length} violations`,
);
}
} else {
console.info(`${module} checker found no violations`);
}
}
}
main()

@ -1,10 +1,10 @@
import { Registry } from 'prom-client';
import { HelloWorldChecker } from '@hyperlane-xyz/helloworld';
import { ChainAddresses } from '@hyperlane-xyz/registry';
import {
ChainMap,
CheckerViolation,
HypERC20App,
HypERC20Checker,
HyperlaneCore,
HyperlaneCoreChecker,
HyperlaneIgp,
HyperlaneIgpChecker,
@ -14,7 +14,6 @@ import {
InterchainAccountConfig,
InterchainQuery,
InterchainQueryChecker,
MultiProvider,
attachContractsMapAndGetForeignDeployments,
hypERC20factories,
proxiedFactories,
@ -24,7 +23,7 @@ import { eqAddress, objFilter } from '@hyperlane-xyz/utils';
import { Contexts } from '../../config/contexts.js';
import { DEPLOYER } from '../../config/environments/mainnet3/owners.js';
import { getWarpConfig } from '../../config/warp.js';
import { EnvironmentConfig } from '../../src/config/environment.js';
import { DeployEnvironment } from '../../src/config/environment.js';
import { HyperlaneAppGovernor } from '../../src/govern/HyperlaneAppGovernor.js';
import { HyperlaneCoreGovernor } from '../../src/govern/HyperlaneCoreGovernor.js';
import { HyperlaneIgpGovernor } from '../../src/govern/HyperlaneIgpGovernor.js';
@ -42,33 +41,36 @@ import {
withFork,
withGovern,
withModule,
withPushMetrics,
withWarpRouteId,
} from '../agent-utils.js';
import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js';
import { getHelloWorldApp } from '../helloworld/utils.js';
export function getCheckArgs() {
export function getCheckBaseArgs() {
return withAsDeployer(
withGovern(withChain(withFork(withContext(getRootArgs())))),
);
}
export function getCheckDeployArgs() {
return withWarpRouteId(withModule(getCheckArgs()));
export function getCheckWarpDeployArgs() {
return withPushMetrics(getCheckBaseArgs());
}
export async function check(argv?: Record<string, any>) {
const {
fork,
govern,
module,
environment,
context,
chain,
asDeployer,
warpRouteId,
} = argv ?? (await getCheckDeployArgs().argv);
export function getCheckDeployArgs() {
return withWarpRouteId(withModule(getCheckBaseArgs()));
}
export async function getGovernor(
module: Modules,
context: Contexts,
environment: DeployEnvironment,
asDeployer: boolean,
warpRouteId?: string,
chain?: string,
fork?: string,
govern?: boolean,
) {
const envConfig = getEnvironmentConfig(environment);
let multiProvider = await envConfig.getMultiProvider();
@ -93,77 +95,6 @@ export async function check(argv?: Record<string, any>) {
multiProvider,
);
const governor = await getGovernor(
module,
multiProvider,
core,
envConfig,
chainAddresses,
context,
chain,
fork,
warpRouteId,
);
// TODO: getGovernor should throw if module not implemented and this should be removed
if (!governor) {
return;
}
if (fork) {
await governor.checker.checkChain(fork);
if (govern) {
await governor.govern(false, fork);
}
} else if (chain) {
await governor.checker.checkChain(chain);
if (govern) {
await governor.govern(true, chain);
}
} else {
await governor.checker.check();
if (govern) {
await governor.govern();
}
}
if (!govern) {
const violations = governor.checker.violations;
if (violations.length > 0) {
console.table(violations, [
'chain',
'remote',
'name',
'type',
'subType',
'actual',
'expected',
]);
logViolationDetails(violations);
if (!fork) {
throw new Error(
`Checking ${module} deploy yielded ${violations.length} violations`,
);
}
} else {
console.info(`${module} checker found no violations`);
}
}
}
const getGovernor = async (
module: Modules,
multiProvider: MultiProvider,
core: HyperlaneCore,
envConfig: EnvironmentConfig,
chainAddresses: ChainMap<ChainAddresses>,
context: Contexts,
chain?: string,
fork?: string,
warpRouteId?: string,
) => {
let governor: HyperlaneAppGovernor<any, any>;
const ismFactory = HyperlaneIsmFactory.fromAddressesMap(
@ -278,7 +209,11 @@ const getGovernor = async (
chain ?? fork
} is non evm and it not compatible with warp checker tooling`,
);
return;
throw Error(
`${
chain ?? fork
} is non evm and it not compatible with warp checker tooling`,
);
}
const app = new HypERC20App(
@ -296,10 +231,42 @@ const getGovernor = async (
);
governor = new ProxiedRouterGovernor(checker, ica);
} else {
// TODO: should we throw here instead?
console.log(`Skipping ${module}, checker or governor not implemented`);
return;
throw Error(
`Checker or governor not implemented not implemented for ${module}`,
);
}
return governor;
};
}
export function logViolations(violations: CheckerViolation[]) {
console.table(violations, [
'chain',
'remote',
'name',
'type',
'subType',
'actual',
'expected',
]);
logViolationDetails(violations);
}
export function getCheckerViolationsGaugeObj(metricsRegister: Registry) {
return {
name: 'hyperlane_check_violations',
help: 'Checker violation',
registers: [metricsRegister],
labelNames: [
'module',
'warp_route_id',
'chain',
'remote',
'contract_name',
'type',
'sub_type',
'actual',
'expected',
],
};
}

@ -1,29 +1,103 @@
import chalk from 'chalk';
import { Gauge, Registry } from 'prom-client';
import { WarpRouteIds } from '../../config/warp.js';
import { submitMetrics } from '../../src/utils/metrics.js';
import { Modules } from '../agent-utils.js';
import { check, getCheckArgs } from './check-utils.js';
import {
getCheckWarpDeployArgs,
getCheckerViolationsGaugeObj,
getGovernor,
logViolations,
} from './check-utils.js';
async function checkWarp() {
const argv = await getCheckArgs().argv;
async function main() {
const { environment, asDeployer, chain, fork, context, pushMetrics } =
await getCheckWarpDeployArgs().argv;
const metricsRegister = new Registry();
const checkerViolationsGauge = new Gauge(
getCheckerViolationsGaugeObj(metricsRegister),
);
metricsRegister.registerMetric(checkerViolationsGauge);
const failedWarpRoutesChecks: string[] = [];
// TODO: consider retrying this if check throws an error
for (const warpRouteId of Object.values(WarpRouteIds)) {
console.log(`\nChecking warp route ${warpRouteId}...`);
const warpModule = Modules.WARP;
try {
await check({
...argv,
const governor = await getGovernor(
warpModule,
context,
environment,
asDeployer,
warpRouteId,
module: Modules.WARP,
});
chain,
fork,
);
await governor.checker.check();
const violations: any = governor.checker.violations;
if (violations.length > 0) {
logViolations(violations);
if (pushMetrics) {
for (const violation of violations) {
checkerViolationsGauge
.labels({
module: warpModule,
warp_route_id: warpRouteId,
chain: violation.chain,
contract_name: violation.name,
type: violation.type,
actual: violation.actual,
expected: violation.expected,
})
.set(1);
console.log(
`Violation: ${violation.name} on ${violation.chain} with ${violation.actual} ${violation.type} ${violation.expected} pushed to metrics`,
);
}
}
} else {
console.info(chalk.green(`${warpModule} checker found no violations`));
}
if (pushMetrics) {
await submitMetrics(
metricsRegister,
`check-warp-deploy-${environment}`,
{
overwriteAllMetrics: true,
},
);
}
} catch (e) {
console.log(chalk.red(`Error checking warp route ${warpRouteId}: ${e}`));
console.error(
chalk.red(`Error checking warp route ${warpRouteId}: ${e}`),
);
failedWarpRoutesChecks.push(warpRouteId);
}
}
if (failedWarpRoutesChecks.length > 0) {
console.error(
chalk.red(
`Failed to check warp routes: ${failedWarpRoutesChecks.join(', ')}`,
),
);
process.exit(1);
}
process.exit(0);
}
checkWarp()
main()
.then()
.catch((e) => {
console.error(e);

@ -0,0 +1,85 @@
import { Contexts } from '../../config/contexts.js';
import { AgentContextConfig } from '../../src/config/agent/agent.js';
import { CheckWarpDeployConfig } from '../../src/config/funding.js';
import { HelmCommand, helmifyValues } from '../../src/utils/helm.js';
import { execCmd } from '../../src/utils/utils.js';
import { assertCorrectKubeContext } from '../agent-utils.js';
import { getConfigsBasedOnArgs } from '../core-utils.js';
async function main() {
const { agentConfig, envConfig } = await getConfigsBasedOnArgs();
if (agentConfig.context != Contexts.Hyperlane)
throw new Error(
`Invalid context ${agentConfig.context}, must be ${Contexts.Hyperlane}`,
);
await assertCorrectKubeContext(envConfig);
if (!envConfig.checkWarpDeployConfig) {
throw new Error('No checkWarpDeployConfig found');
}
await runCheckWarpDeployHelmCommand(
HelmCommand.InstallOrUpgrade,
agentConfig,
envConfig.checkWarpDeployConfig,
);
}
main()
.then(() => console.log('Deploy successful!'))
.catch(console.error);
async function runCheckWarpDeployHelmCommand(
helmCommand: HelmCommand,
agentConfig: AgentContextConfig,
config: CheckWarpDeployConfig,
) {
const values = getCheckWarpDeployHelmValues(agentConfig, config);
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=check-warp-deploy`,
{},
false,
false,
);
} catch (e) {
console.error(e);
}
}
return execCmd(
`helm ${helmCommand} check-warp-deploy ./helm/check-warp-deploy --namespace ${
config.namespace
} ${values.join(' ')}`,
{},
false,
true,
);
}
function getCheckWarpDeployHelmValues(
agentConfig: AgentContextConfig,
config: CheckWarpDeployConfig,
) {
const values = {
cronjob: {
schedule: config.cronSchedule,
},
hyperlane: {
runEnv: agentConfig.runEnv,
chains: agentConfig.environmentChainNames,
},
infra: {
prometheusPushGateway: config.prometheusPushGateway,
},
image: {
repository: config.docker.repo,
tag: config.docker.tag,
},
};
return helmifyValues(values);
}

@ -17,7 +17,7 @@ import { CloudAgentKey } from '../agents/keys.js';
import { Role } from '../roles.js';
import { RootAgentConfig } from './agent/agent.js';
import { KeyFunderConfig } from './funding.js';
import { CheckWarpDeployConfig, KeyFunderConfig } from './funding.js';
import { HelloWorldConfig } from './helloworld/types.js';
import { InfrastructureConfig } from './infrastructure.js';
import { LiquidityLayerRelayerConfig } from './middleware.js';
@ -61,6 +61,7 @@ export type EnvironmentConfig = {
) => Promise<ChainMap<CloudAgentKey>>;
helloWorld?: Partial<Record<Contexts, HelloWorldConfig>>;
keyFunderConfig?: KeyFunderConfig<string[]>;
checkWarpDeployConfig?: CheckWarpDeployConfig;
liquidityLayerConfig?: {
bridgeAdapters: ChainMap<BridgeAdapterConfig>;
relayer: LiquidityLayerRelayerConfig;

@ -12,15 +12,21 @@ export interface ContextAndRoles {
export type ContextAndRolesMap = Partial<Record<Contexts, FundableRole[]>>;
export interface KeyFunderConfig<SupportedChains extends readonly ChainName[]> {
export interface CronJobConfig {
docker: DockerConfig;
cronSchedule: string;
namespace: string;
prometheusPushGateway: string;
}
export interface KeyFunderConfig<SupportedChains extends readonly ChainName[]>
extends CronJobConfig {
contextFundingFrom: Contexts;
contextsAndRolesToFund: ContextAndRolesMap;
cyclesBetweenEthereumMessages?: number;
prometheusPushGateway: string;
desiredBalancePerChain: Record<SupportedChains[number], string>;
desiredKathyBalancePerChain: ChainMap<string>;
igpClaimThresholdPerChain: ChainMap<string>;
}
export interface CheckWarpDeployConfig extends CronJobConfig {}

Loading…
Cancel
Save