@starship-ci/generator
Version:
Kubernetes manifest generator for Starship deployments
469 lines (455 loc) • 18.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HermesRelayerBuilder = exports.HermesStatefulSetGenerator = exports.HermesServiceGenerator = exports.HermesConfigMapGenerator = void 0;
const helpers = __importStar(require("../../helpers"));
const version_1 = require("../../version");
const base_1 = require("./base");
const utils_1 = require("./utils");
/**
* ConfigMap generator for Hermes relayer
*/
class HermesConfigMapGenerator {
config;
relayer;
constructor(relayer, config) {
this.config = config;
this.relayer = relayer;
}
generate() {
const metadata = {
name: `${this.relayer.type}-${this.relayer.name}`,
labels: {
...helpers.getCommonLabels(this.config),
'app.kubernetes.io/component': 'relayer',
'app.kubernetes.io/part-of': 'starship',
'app.kubernetes.io/role': this.relayer.type,
'app.kubernetes.io/name': `${this.relayer.type}-${this.relayer.name}`
}
};
const configToml = this.generateHermesConfig();
return [
{
apiVersion: 'v1',
kind: 'ConfigMap',
metadata,
data: {
'config.toml': configToml,
'config-cli.toml': configToml.replace(/key_name = "([^"]+)"/g, 'key_name = "$1-cli"')
}
}
];
}
generateHermesConfig() {
const relayerConfig = this.relayer.config || {};
const globalConfig = relayerConfig.global || {};
const modeConfig = relayerConfig.mode || {};
const restConfig = relayerConfig.rest || {};
const telemetryConfig = relayerConfig.telemetry || {};
const eventSourceConfig = relayerConfig.event_source || {};
let configToml = `# The global section has parameters that apply globally to the relayer operation.
[global]
log_level = "${globalConfig.log_level || 'info'}"
[mode]
[mode.clients]
enabled = ${modeConfig.clients?.enabled ?? true}
refresh = ${modeConfig.clients?.refresh ?? true}
misbehaviour = ${modeConfig.clients?.misbehaviour ?? true}
[mode.connections]
enabled = ${modeConfig.connections?.enabled ?? true}
[mode.channels]
enabled = ${modeConfig.channels?.enabled ?? true}
[mode.packets]
enabled = ${modeConfig.packets?.enabled ?? true}
clear_interval = ${modeConfig.packets?.clear_interval ?? 100}
clear_on_start = ${modeConfig.packets?.clear_on_start ?? true}
tx_confirmation = ${modeConfig.packets?.tx_confirmation ?? true}
[rest]
enabled = ${restConfig.enabled ?? true}
host = "${restConfig.host || '0.0.0.0'}"
port = ${restConfig.port || 3000}
[telemetry]
enabled = ${telemetryConfig.enabled ?? true}
host = "${telemetryConfig.host || '0.0.0.0'}"
port = ${telemetryConfig.port || 3001}
`;
// Add chain configurations
this.relayer.chains.forEach((chainId) => {
const chain = this.config.chains.find((c) => String(c.id) === chainId);
if (!chain) {
throw new Error(`Chain ${chainId} not found in configuration`);
}
const chainConfig = relayerConfig.chains?.find((c) => c.id === chainId) || {};
const chainName = helpers.getChainName(String(chain.id));
const addressType = (0, utils_1.getAddressType)(chain.name);
const gasPrice = (0, utils_1.getGasPrice)(chain.name, chain.denom);
configToml += `
[[chains]]
id = "${chainId}"
type = "CosmosSdk"
key_name = "${chainId}"
${chain.ics?.enabled ? 'ccv_consumer_chain = true' : ''}
rpc_addr = "http://${chainName}-genesis.$(NAMESPACE).svc.cluster.local:26657"
grpc_addr = "http://${chainName}-genesis.$(NAMESPACE).svc.cluster.local:9090"
${eventSourceConfig.mode === 'pull'
? `event_source = { mode = 'pull', interval = '${eventSourceConfig.interval || '500ms'}' }`
: `event_source = { mode = 'push', url = "ws://${chainName}-genesis.$(NAMESPACE).svc.cluster.local:26657/websocket", batch_delay = '${eventSourceConfig.batch_delay || '500ms'}' }`}
trusted_node = false
account_prefix = "${chainConfig.account_prefix || chain.prefix}"
default_gas = ${chainConfig.default_gas || 500000000}
max_gas = ${chainConfig.max_gas || 1000000000}
rpc_timeout = "${chainConfig.rpc_timeout || '10s'}"
store_prefix = "${chainConfig.store_prefix || 'ibc'}"
gas_multiplier = ${chainConfig.gas_multiplier || 2}
max_msg_num = ${chainConfig.max_msg_num || 30}
max_tx_size = ${chainConfig.max_tx_size || 2097152}
clock_drift = "${chainConfig.clock_drift || '5s'}"
max_block_time = "${chainConfig.max_block_time || '30s'}"
trusting_period = "${chainConfig.trusting_period || '75s'}"
trust_threshold = { numerator = "${(chainConfig.trust_threshold || {}).numerator || '2'}", denominator = "${(chainConfig.trust_threshold || {}).denominator || '3'}" }
${addressType}
${gasPrice}
`;
});
return configToml;
}
}
exports.HermesConfigMapGenerator = HermesConfigMapGenerator;
/**
* Service generator for Hermes relayer
*/
class HermesServiceGenerator {
config;
relayer;
constructor(relayer, config) {
this.config = config;
this.relayer = relayer;
}
generate() {
const metadata = {
name: `${this.relayer.type}-${this.relayer.name}`,
labels: {
...helpers.getCommonLabels(this.config),
'app.kubernetes.io/component': 'relayer',
'app.kubernetes.io/part-of': 'starship',
'app.kubernetes.io/role': this.relayer.type,
'app.kubernetes.io/name': `${this.relayer.type}-${this.relayer.name}`
}
};
const ports = [
{
name: 'rest',
port: 3000,
protocol: 'TCP',
targetPort: this.relayer.config?.rest?.port || 3000
},
{
name: 'exposer',
port: this.config.exposer?.ports?.rest || 8081,
protocol: 'TCP',
targetPort: this.config.exposer?.ports?.rest || 8081
}
];
return [
{
apiVersion: 'v1',
kind: 'Service',
metadata,
spec: {
clusterIP: 'None',
ports,
selector: {
'app.kubernetes.io/name': `${this.relayer.type}-${this.relayer.name}`
}
}
}
];
}
}
exports.HermesServiceGenerator = HermesServiceGenerator;
/**
* StatefulSet generator for Hermes relayer
*/
class HermesStatefulSetGenerator {
config;
relayer;
constructor(relayer, config) {
this.config = config;
this.relayer = relayer;
}
generate() {
const fullname = `${this.relayer.type}-${this.relayer.name}`;
return [
{
apiVersion: 'apps/v1',
kind: 'StatefulSet',
metadata: {
name: fullname,
labels: {
...helpers.getCommonLabels(this.config),
'app.kubernetes.io/component': 'relayer',
'app.kubernetes.io/part-of': 'starship',
'app.kubernetes.io/role': this.relayer.type,
'app.kubernetes.io/name': fullname
}
},
spec: {
serviceName: fullname,
replicas: this.relayer.replicas || 1,
podManagementPolicy: 'Parallel',
revisionHistoryLimit: 3,
selector: {
matchLabels: {
'app.kubernetes.io/instance': 'relayer',
'app.kubernetes.io/type': this.relayer.type,
'app.kubernetes.io/name': fullname
}
},
template: {
metadata: {
annotations: {
quality: 'release',
role: 'api-gateway',
sla: 'high',
tier: 'gateway'
},
labels: {
'app.kubernetes.io/instance': 'relayer',
'app.kubernetes.io/type': this.relayer.type,
'app.kubernetes.io/name': fullname,
'app.kubernetes.io/rawname': this.relayer.name,
'app.kubernetes.io/version': (0, version_1.getGeneratorVersion)()
}
},
spec: {
initContainers: this.generateInitContainers(),
containers: this.generateContainers(),
volumes: this.generateVolumes()
}
}
}
}
];
}
generateInitContainers() {
const initContainers = [];
// Add exposer init container
initContainers.push({
name: 'init-exposer',
image: this.config.exposer?.image ||
'ghcr.io/cosmology-tech/starship/exposer:v0.2.0',
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
command: ['bash', '-c'],
args: [
'# Install exposer binary from the image\ncp /bin/exposer /exposer/exposer\nchmod +x /exposer/exposer'
],
resources: helpers.getResourceObject(this.relayer.resources || { cpu: '0.1', memory: '100M' }),
volumeMounts: [{ mountPath: '/exposer', name: 'exposer' }]
});
// Add wait init containers for all chains
this.relayer.chains.forEach((chainId) => {
const chain = this.config.chains.find((c) => String(c.id) === chainId);
if (!chain)
return;
const chainName = helpers.getChainName(String(chain.id));
initContainers.push({
name: `init-${chainName}`,
image: 'ghcr.io/cosmology-tech/starship/wait-for-service:v0.1.0',
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
command: ['bash', '-c'],
args: [
`echo "Waiting for ${chainName} service..."\nwait-for-service ${chainName}-genesis.$(NAMESPACE).svc.cluster.local:26657`
],
env: [
{
name: 'NAMESPACE',
valueFrom: { fieldRef: { fieldPath: 'metadata.namespace' } }
}
]
});
});
// Add hermes init container
initContainers.push(this.generateHermesInitContainer());
return initContainers;
}
generateHermesInitContainer() {
const image = this.relayer.image || 'ghcr.io/cosmology-tech/starship/hermes:1.10.0';
const env = [
{ name: 'KEYS_CONFIG', value: '/keys/keys.json' },
{ name: 'RELAYER_DIR', value: '/root/.hermes' },
{ name: 'RELAYER_INDEX', value: '${HOSTNAME##*-}' },
{
name: 'NAMESPACE',
valueFrom: { fieldRef: { fieldPath: 'metadata.namespace' } }
}
];
const command = this.generateHermesInitCommand();
return {
name: 'init-relayer',
image,
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
env,
command: ['bash', '-c'],
args: [command],
resources: helpers.getResourceObject(this.relayer.resources || { cpu: '0.2', memory: '200M' }),
volumeMounts: [
{ mountPath: '/root', name: 'relayer' },
{ mountPath: '/configs', name: 'relayer-config' },
{ mountPath: '/keys', name: 'keys' },
{ mountPath: '/scripts', name: 'scripts' }
]
};
}
generateContainers() {
const containers = [];
// Main hermes container
containers.push({
name: 'relayer',
image: this.relayer.image || 'ghcr.io/cosmology-tech/starship/hermes:1.10.0',
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
env: [{ name: 'RELAYER_DIR', value: '/root/.hermes' }],
command: ['bash', '-c'],
args: [
'RLY_INDEX=${HOSTNAME##*-}\necho "Relayer Index: $RLY_INDEX"\nhermes start'
],
resources: helpers.getResourceObject(this.relayer.resources || { cpu: '0.2', memory: '200M' }),
securityContext: {
allowPrivilegeEscalation: false,
runAsUser: 0
},
volumeMounts: [
{ mountPath: '/root', name: 'relayer' },
{ mountPath: '/configs', name: 'relayer-config' }
]
});
// Exposer container
containers.push({
name: 'exposer',
image: this.relayer.image || 'ghcr.io/cosmology-tech/starship/hermes:1.10.0',
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
env: [
{ name: 'EXPOSER_HTTP_PORT', value: '8081' },
{ name: 'EXPOSER_GRPC_PORT', value: '9099' }
],
command: ['bash', '-c'],
args: ['/exposer/exposer'],
resources: helpers.getResourceObject(this.config.exposer?.resources || { cpu: '0.1', memory: '100M' }),
securityContext: {
allowPrivilegeEscalation: false,
runAsUser: 0
},
volumeMounts: [
{ mountPath: '/root', name: 'relayer' },
{ mountPath: '/configs', name: 'relayer-config' },
{ mountPath: '/exposer', name: 'exposer' }
]
});
return containers;
}
generateVolumes() {
return [
{ name: 'relayer', emptyDir: {} },
{
name: 'relayer-config',
configMap: { name: `${this.relayer.type}-${this.relayer.name}` }
},
{ name: 'keys', configMap: { name: 'keys' } },
{ name: 'scripts', configMap: { name: 'setup-scripts' } },
{ name: 'exposer', emptyDir: {} }
];
}
generateHermesInitCommand() {
let command = `set -ux
RLY_INDEX=\${HOSTNAME##*-}
echo "Relayer Index: $RLY_INDEX"
mkdir -p $RELAYER_DIR
cp /configs/config.toml $RELAYER_DIR/config.toml
cp /configs/config-cli.toml $RELAYER_DIR/config-cli.toml
MNEMONIC=$(jq -r ".relayers[$RLY_INDEX].mnemonic" $KEYS_CONFIG)
echo $MNEMONIC > $RELAYER_DIR/mnemonic.txt
MNEMONIC_CLI=$(jq -r ".relayers_cli[$RLY_INDEX].mnemonic" $KEYS_CONFIG)
echo $MNEMONIC_CLI > $RELAYER_DIR/mnemonic-cli.txt
`;
// Add key creation and funding for each chain
this.relayer.chains.forEach((chainId) => {
const chain = this.config.chains.find((c) => String(c.id) === chainId);
if (!chain)
return;
const chainName = helpers.getChainName(String(chain.id));
command += `
echo "Creating key for ${chainId}..."
hermes keys add \\
--chain ${chainId} \\
--mnemonic-file $RELAYER_DIR/mnemonic.txt \\
--key-name ${chainId} \\
--hd-path "${chain.hdPath || "m/44'/118'/0'/0/0"}"
DENOM="${chain.denom}"
RLY_ADDR=$(hermes --json keys list --chain ${chainId} | tail -1 | jq -r '.result."${chainId}".account')
echo "Transfer tokens to address $RLY_ADDR"
bash -e /scripts/transfer-tokens.sh \\
$RLY_ADDR \\
$DENOM \\
http://${chainName}-genesis.$NAMESPACE.svc.cluster.local:8000/credit \\
"${chain.faucet?.enabled || false}" || true
`;
});
// Add channel creation if specified
if (this.relayer.channels && this.relayer.channels.length > 0) {
this.relayer.channels.forEach((channel) => {
command += `
hermes create channel \\
${channel['new-connection'] ? '--new-client-connection --yes \\' : ''}
${channel['b-chain'] ? `--b-chain ${channel['b-chain']} \\` : ''}
${channel['a-connection']
? `--a-connection ${channel['a-connection']} \\`
: ''}
${channel['channel-version']
? `--channel-version ${channel['channel-version']} \\`
: ''}
${channel.order ? `--order ${channel.order} \\` : ''}
--a-chain ${channel['a-chain']} \\
--a-port ${channel['a-port']} \\
--b-port ${channel['b-port']}
`;
});
}
return command;
}
}
exports.HermesStatefulSetGenerator = HermesStatefulSetGenerator;
/**
* Main Hermes relayer builder
*/
class HermesRelayerBuilder extends base_1.BaseRelayerBuilder {
constructor(relayer, config) {
super(relayer, config);
this.generators = [
new HermesConfigMapGenerator(relayer, config),
new HermesServiceGenerator(relayer, config),
new HermesStatefulSetGenerator(relayer, config)
];
}
}
exports.HermesRelayerBuilder = HermesRelayerBuilder;