@starship-ci/generator
Version:
Kubernetes manifest generator for Starship deployments
663 lines (638 loc) • 26.6 kB
JavaScript
"use strict";
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.CosmosGenesisStatefulSetGenerator = void 0;
const defaults_1 = require("../../../defaults");
const helpers = __importStar(require("../../../helpers"));
const version_1 = require("../../../version");
class CosmosGenesisStatefulSetGenerator {
config;
chain;
scriptManager;
defaultsManager;
constructor(chain, config, scriptManager) {
this.config = config;
this.chain = chain;
this.scriptManager = scriptManager;
this.defaultsManager = new defaults_1.DefaultsManager();
}
labels() {
const processedChain = this.defaultsManager.processChain(this.chain);
return {
...helpers.getCommonLabels(this.config),
'app.kubernetes.io/component': 'chain',
'app.kubernetes.io/part-of': helpers.getChainId(processedChain),
'app.kubernetes.io/id': helpers.getChainId(processedChain),
'app.kubernetes.io/name': `${helpers.getHostname(processedChain)}-genesis`,
'app.kubernetes.io/type': `${helpers.getChainId(processedChain)}-statefulset`,
'app.kubernetes.io/role': 'genesis',
'starship.io/chain-name': processedChain.name
};
}
generate() {
const processedChain = this.defaultsManager.processChain(this.chain);
return [
{
apiVersion: 'apps/v1',
kind: 'StatefulSet',
metadata: {
name: `${helpers.getHostname(processedChain)}-genesis`,
labels: this.labels()
},
spec: {
serviceName: `${helpers.getHostname(processedChain)}-genesis`,
replicas: 1,
revisionHistoryLimit: 3,
selector: {
matchLabels: {
'app.kubernetes.io/instance': this.config.name,
'app.kubernetes.io/name': `${helpers.getChainId(processedChain)}-genesis`
}
},
template: {
metadata: {
annotations: {
quality: 'release',
role: 'api-gateway',
sla: 'high',
tier: 'gateway'
},
labels: {
'app.kubernetes.io/instance': this.config.name,
'app.kubernetes.io/type': helpers.getChainId(processedChain),
'app.kubernetes.io/name': `${helpers.getChainId(processedChain)}-genesis`,
'app.kubernetes.io/rawname': helpers.getChainId(processedChain),
'app.kubernetes.io/version': (0, version_1.getGeneratorVersion)(),
'app.kubernetes.io/role': 'genesis'
}
},
spec: {
...(processedChain.imagePullSecrets
? helpers.generateImagePullSecrets(processedChain.imagePullSecrets)
: {}),
initContainers: this.createInitContainers(processedChain),
containers: this.createMainContainers(processedChain),
volumes: helpers.generateChainVolumes(processedChain)
}
}
}
}
];
}
createInitContainers(chain) {
const initContainers = [];
const exposerPort = this.config.exposer?.ports?.rest || 8081;
// Build images init container if needed
if (chain.build?.enabled || chain.upgrade?.enabled) {
initContainers.push(this.createBuildImagesInitContainer(chain));
}
// Genesis init container
initContainers.push(this.createGenesisInitContainer(chain));
// Config init container
initContainers.push(this.createConfigInitContainer(chain));
// Add additional init containers based on chain configuration
if (chain.faucet?.enabled && chain.faucet.type === 'starship') {
initContainers.push(this.createFaucetInitContainer(chain));
}
if (chain.ics?.enabled) {
// Add wait container for provider chain
const providerChainId = chain.ics.provider || 'cosmoshub';
initContainers.push(this.createIcsWaitInitContainer([providerChainId], exposerPort));
initContainers.push(this.createIcsInitContainer(chain, exposerPort));
}
return initContainers;
}
createMainContainers(chain) {
const containers = [];
// Main validator container
containers.push(this.createValidatorContainer(chain));
// Exposer container
containers.push(this.createExposerContainer(chain));
// Faucet container if enabled
if (chain.faucet?.enabled) {
containers.push(this.createFaucetContainer(chain));
}
return containers;
}
createBuildImagesInitContainer(chain) {
const buildCommands = [
'# Install cosmovisor',
'go install github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@v1.0.0',
'',
'# Build genesis'
];
if (chain.upgrade?.enabled) {
// Build genesis version
buildCommands.push(`UPGRADE_NAME=genesis CODE_TAG=${chain.upgrade.genesis} bash -e /scripts/build-chain.sh`);
// Build upgrade versions
if (chain.upgrade.upgrades) {
chain.upgrade.upgrades.forEach((upgrade) => {
buildCommands.push(`UPGRADE_NAME=${upgrade.name} CODE_TAG=${upgrade.version} bash -e /scripts/build-chain.sh`);
});
}
}
else if (chain.build?.enabled) {
buildCommands.push(`UPGRADE_NAME=genesis CODE_TAG=${chain.build.source} bash -e /scripts/build-chain.sh`);
}
return {
name: 'init-build-images',
image: 'ghcr.io/cosmology-tech/starship/builder:latest',
imagePullPolicy: 'IfNotPresent',
command: ['bash', '-c', buildCommands.join('\n')],
env: [
{ name: 'CODE_REF', value: chain.repo },
{ name: 'UPGRADE_DIR', value: `${chain.home}/cosmovisor` },
{ name: 'GOBIN', value: '/go/bin' },
{ name: 'CHAIN_NAME', value: helpers.getChainId(chain) },
...helpers.getDefaultEnvVars(chain)
],
resources: helpers.getNodeResources(chain, this.config),
volumeMounts: helpers.generateChainVolumeMounts(chain)
};
}
createGenesisInitContainer(chain) {
return {
name: 'init-genesis',
image: chain.image,
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
env: [
...helpers.getDefaultEnvVars(chain),
...helpers.getChainEnvVars(chain),
...helpers.getTimeoutEnvVars(this.config.timeouts || {}),
{ name: 'KEYS_CONFIG', value: '/configs/keys.json' },
{
name: 'FAUCET_ENABLED',
value: String(chain.faucet?.enabled || false)
},
{
name: 'NUM_VALIDATORS',
value: String(chain.numValidators || 1)
},
{
name: 'NUM_RELAYERS',
value: String(this.config.relayers?.length || 0)
}
],
command: ['bash', '-c', this.getGenesisInitScript(chain)],
resources: helpers.getNodeResources(chain, this.config),
volumeMounts: helpers.generateChainVolumeMounts(chain)
};
}
createConfigInitContainer(chain) {
return {
name: 'init-config',
image: chain.image,
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
env: [
...helpers.getDefaultEnvVars(chain),
...helpers.getChainEnvVars(chain),
...helpers.getTimeoutEnvVars(this.config.timeouts || {}),
{ name: 'KEYS_CONFIG', value: '/configs/keys.json' },
{ name: 'METRICS', value: String(chain.metrics || false) }
],
command: ['bash', '-c', this.getConfigInitScript(chain)],
resources: helpers.getNodeResources(chain, this.config),
volumeMounts: [
...helpers.generateChainVolumeMounts(chain),
...(chain.genesis
? [
{
mountPath: '/patch',
name: 'patch'
}
]
: [])
]
};
}
createFaucetInitContainer(chain) {
return {
name: 'init-faucet',
image: chain.faucet.image,
imagePullPolicy: 'IfNotPresent',
command: [
'bash',
'-c',
'cp /bin/faucet /faucet/faucet && chmod +x /faucet/faucet'
],
resources: helpers.getNodeResources(chain, this.config),
volumeMounts: [{ mountPath: '/faucet', name: 'faucet' }]
};
}
createIcsInitContainer(chain, exposerPort) {
// Need to get provider chain info - for now using a placeholder
// In real implementation, this would need access to provider chain config
const providerChainId = chain.ics?.provider || 'cosmoshub';
const providerChain = this.config.chains.find((c) => c.id === providerChainId);
return {
name: 'init-ics',
image: providerChain?.image,
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
env: [
...helpers.getDefaultEnvVars(chain),
...helpers.getChainEnvVars(chain),
{
name: 'NAMESPACE',
valueFrom: {
fieldRef: {
fieldPath: 'metadata.namespace'
}
}
},
{ name: 'KEYS_CONFIG', value: '/configs/keys.json' }
],
command: [
'bash',
'-c',
this.getIcsInitScript(chain, providerChain, exposerPort)
],
resources: helpers.getNodeResources(chain, this.config),
volumeMounts: [
{ mountPath: '/proposal', name: 'proposal' },
{ mountPath: chain.home, name: 'node' },
{ mountPath: '/configs', name: 'addresses' },
{ mountPath: '/scripts', name: 'scripts' }
]
};
}
createIcsWaitInitContainer(chainIDs, port) {
return helpers.generateWaitInitContainer(chainIDs, port, this.config);
}
createValidatorContainer(chain) {
const toBuild = chain.build?.enabled || chain.upgrade?.enabled;
return {
name: 'validator',
image: chain.image,
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
env: [
...helpers.getDefaultEnvVars(chain),
...helpers.getChainEnvVars(chain),
{
name: 'FAUCET_ENABLED',
value: String(chain.faucet?.enabled || false)
},
{ name: 'SLOGFILE', value: 'slog.slog' },
...(toBuild
? [
{
name: 'DAEMON_NAME',
value: chain.binary || helpers.getChainId(chain)
},
{
name: 'DAEMON_HOME',
value: chain.home || `/home/validator/.${helpers.getChainId(chain)}`
}
]
: []),
...(chain.env || []).map((env) => ({
name: env.name,
value: String(env.value)
}))
],
command: ['bash', '-c', this.getValidatorStartScript(chain)],
resources: helpers.getNodeResources(chain, this.config),
volumeMounts: helpers.generateChainVolumeMounts(chain),
...(chain.cometmock?.enabled
? {}
: {
readinessProbe: chain.readinessProbe || {
exec: {
command: [
'bash',
'-e',
'/scripts/chain-rpc-ready.sh',
'http://localhost:26657'
]
},
initialDelaySeconds: 10,
periodSeconds: 10,
timeoutSeconds: 15
}
})
};
}
createExposerContainer(chain) {
return {
name: 'exposer',
image: this.config.exposer?.image ||
'ghcr.io/cosmology-tech/starship/exposer:latest',
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
env: [
...helpers.getGenesisEnvVars(chain, this.config.exposer?.ports?.rest || 8081),
{ name: 'EXPOSER_HTTP_PORT', value: '8081' },
{ name: 'EXPOSER_GRPC_PORT', value: '9099' },
{
name: 'EXPOSER_GENESIS_FILE',
value: `${chain.home}/config/genesis.json`
},
{ name: 'EXPOSER_MNEMONIC_FILE', value: '/configs/keys.json' },
{
name: 'EXPOSER_PRIV_VAL_FILE',
value: `${chain.home}/config/priv_validator_key.json`
},
{
name: 'EXPOSER_NODE_KEY_FILE',
value: `${chain.home}/config/node_key.json`
},
{
name: 'EXPOSER_NODE_ID_FILE',
value: `${chain.home}/config/node_id.json`
},
{
name: 'EXPOSER_PRIV_VAL_STATE_FILE',
value: `${chain.home}/data/priv_validator_state.json`
}
],
command: ['exposer'],
resources: helpers.getResourceObject(this.config.exposer?.resources || { cpu: '0.1', memory: '128M' }),
volumeMounts: [
{ mountPath: chain.home, name: 'node' },
{ mountPath: '/configs', name: 'addresses' }
]
};
}
createFaucetContainer(chain) {
if (chain.faucet?.type === 'cosmjs') {
return this.createCosmjsFaucetContainer(chain);
}
return this.createStarshipFaucetContainer(chain);
}
createCosmjsFaucetContainer(chain) {
const faucet = chain.faucet;
return {
name: 'faucet',
image: faucet.image ||
this.config.faucet?.image ||
'ghcr.io/cosmology-tech/starship/faucet:latest',
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
env: [
{
name: 'FAUCET_CONCURRENCY',
value: String(faucet.concurrency || 1)
},
{
name: 'FAUCET_PORT',
value: String(faucet.ports?.rest || 8000)
},
{ name: 'FAUCET_MEMO', value: 'faucet txn' },
{ name: 'FAUCET_GAS_PRICE', value: `1.25${chain.denom}` },
{ name: 'FAUCET_GAS_LIMIT', value: '2000000' },
{ name: 'FAUCET_ADDRESS_PREFIX', value: chain.prefix },
{ name: 'FAUCET_REFILL_FACTOR', value: '8' },
{ name: 'FAUCET_REFILL_THRESHOLD', value: '20' },
{ name: 'FAUCET_COOLDOWN_TIME', value: '0' },
{
name: 'COINS',
value: chain.coins || `1000000000000000000${chain.denom}`
},
{ name: 'HD_PATH', value: chain.hdPath || "m/44'/118'/0'/0/0" }
],
command: ['bash', '-c', this.getCosmjsFaucetScript()],
resources: helpers.getResourceObject(faucet.resources || { cpu: '0.2', memory: '200M' }),
volumeMounts: [
{ mountPath: '/configs', name: 'addresses' },
{ mountPath: '/scripts', name: 'scripts' }
],
readinessProbe: {
httpGet: {
path: '/status',
port: String(faucet.ports?.rest || 8000)
},
initialDelaySeconds: 30,
periodSeconds: 10
}
};
}
createStarshipFaucetContainer(chain) {
const faucet = chain.faucet;
return {
name: 'faucet',
image: chain.image,
imagePullPolicy: this.config.images?.imagePullPolicy || 'IfNotPresent',
env: [
{
name: 'FAUCET_CONCURRENCY',
value: String(faucet.concurrency || 1)
},
{
name: 'FAUCET_HTTP_PORT',
value: String(faucet.ports?.rest || 8000)
},
{
name: 'FAUCET_CHAIN_BINARY',
value: chain.binary || helpers.getChainId(chain)
},
{ name: 'FAUCET_CHAIN_ID', value: helpers.getChainId(chain) },
{
name: 'COINS',
value: chain.coins || `1000000000000000000${chain.denom}`
}
],
command: ['bash', '-c', this.getStarshipFaucetScript()],
resources: helpers.getResourceObject(faucet.resources || { cpu: '0.1', memory: '128M' }),
volumeMounts: [
{ mountPath: '/configs', name: 'addresses' },
{ mountPath: '/faucet', name: 'faucet' },
{ mountPath: '/scripts', name: 'scripts' }
],
readinessProbe: {
httpGet: {
path: '/status',
port: String(faucet.ports?.rest || 8000)
},
initialDelaySeconds: 30,
periodSeconds: 10
}
};
}
getGenesisInitScript(chain) {
const toBuild = chain.build?.enabled || chain.upgrade?.enabled;
let script = `
VAL_INDEX=\${HOSTNAME##*-}
echo "Validator Index: $VAL_INDEX"
`;
// Add build binary copying logic if needed
if (toBuild) {
script += `
cp $CHAIN_DIR/cosmovisor/genesis/bin/$CHAIN_BIN /usr/bin
`;
}
script += `
if [ -f $CHAIN_DIR/config/genesis.json ]; then
echo "Genesis file exists, exiting init container"
exit 0
fi
echo "Running setup genesis script..."
bash -e /scripts/create-genesis.sh
bash -e /scripts/update-genesis.sh
echo "Create node id json file"
NODE_ID=$($CHAIN_BIN tendermint show-node-id)
echo '{"node_id":"'$NODE_ID'"}' > $CHAIN_DIR/config/node_id.json
echo "Create consensus key json file"
$CHAIN_BIN tendermint show-validator > $CHAIN_DIR/config/consensus_key.json
cat $CHAIN_DIR/config/consensus_key.json
echo "Add custom accounts and balances"
CHAIN_GENESIS_CMD=$($CHAIN_BIN 2>&1 | grep -q "genesis-related subcommands" && echo "genesis" || echo "")
`;
// Add balances if configured
if (chain.balances && chain.balances.length > 0) {
chain.balances.forEach((balance) => {
script += `
echo "Adding balance to ${balance.address}"
$CHAIN_BIN $CHAIN_GENESIS_CMD add-genesis-account ${balance.address} ${balance.amount} --keyring-backend="test"
`;
});
}
return script.trim();
}
getConfigInitScript(chain) {
const toBuild = chain.build?.enabled || chain.upgrade?.enabled;
let script = `
VAL_INDEX=\${HOSTNAME##*-}
echo "Validator Index: $VAL_INDEX"
`;
// Add build binary copying logic if needed
if (toBuild) {
script += `
cp $CHAIN_DIR/cosmovisor/genesis/bin/$CHAIN_BIN /usr/bin
`;
}
script += `
echo "Running setup config script..."
`;
// Add genesis patching logic BEFORE config script (order matters!)
if (chain.genesis && Object.keys(chain.genesis).length > 0) {
script += `
jq -s '.[0] * .[1]' $CHAIN_DIR/config/genesis.json /patch/genesis.json > $CHAIN_DIR/config/genesis.json.tmp && mv $CHAIN_DIR/config/genesis.json.tmp $CHAIN_DIR/config/genesis.json
`;
}
script += `
bash -e /scripts/update-config.sh
`;
return script.trim();
}
getGenesisScript(chain) {
return this.scriptManager.getScriptContent(chain.scripts?.createGenesis || {
name: 'create-genesis.sh',
data: '/scripts/create-genesis.sh'
});
}
getValidatorStartScript(chain) {
const toBuild = chain.build?.enabled || chain.upgrade?.enabled;
return `#!/bin/bash
set -euo pipefail
START_ARGS=""
${chain.cometmock?.enabled ? `START_ARGS="--grpc-web.enable=false --transport=grpc --with-tendermint=false --address tcp://0.0.0.0:26658"` : ''}
${toBuild ? `/usr/bin/cosmovisor start $START_ARGS` : `$CHAIN_BIN start $START_ARGS`}`;
}
getCosmjsFaucetScript() {
return `
export FAUCET_TOKENS=$(printf '%s\\n' \${COINS//[[:digit:]]/})
for coin in \${COINS//,/ }
do
var="FAUCET_CREDIT_AMOUNT_$(printf '%s\\n' \${coin//[[:digit:]]/} | tr '[:lower:]' '[:upper:]')"
amt="\${coin//[!0-9]/}"
if [ \${#amt} -gt 18 ]; then
creditAmt=$(echo $amt | sed -e "s/000000$//")
feesAmt=$(echo $amt | sed -e "s/0000000000000$//")
else
creditAmt=$(echo $amt | sed -e "s/0000$//")
feesAmt=$(echo $amt | sed -e "s/00000000$//")
fi
export $var="$creditAmt"
done
export FAUCET_PATH_PATTERN="\${HD_PATH:0:$((\${#HD_PATH}-1))}a"
export FAUCET_MNEMONIC=$(jq -r ".faucet[0].mnemonic" /configs/keys.json)
echo "FAUCET_MNEMONIC: $FAUCET_MNEMONIC"
echo "FAUCET_PATH_PATTERN: $FAUCET_PATH_PATTERN"
export | grep "FAUCET"
until bash -e /scripts/chain-rpc-ready.sh http://localhost:26657; do
sleep 10;
done
/app/packages/faucet/bin/cosmos-faucet-dist start "http://localhost:26657"
`.trim();
}
getStarshipFaucetScript() {
return `
CREDIT_COINS=""
FEES=""
for coin in \${COINS//,/ }
do
amt="\${coin//[!0-9]/}"
denom="\${coin//[0-9]/}"
# Calculate the order of magnitude
if [ \${#amt} -gt 18 ]; then
creditAmt=$(echo $amt | sed -e "s/000000$//")
feesAmt=$(echo $amt | sed -e "s/0000000000000$//")
else
creditAmt=$(echo $amt | sed -e "s/0000$//")
feesAmt=$(echo $amt | sed -e "s/00000000$//")
fi
if [[ $CREDIT_COINS == "" ]]
then
CREDIT_COINS="$creditAmt$denom"
FEES="$feesAmt$denom"
else
CREDIT_COINS="\${CREDIT_COINS},$creditAmt$denom"
fi
done
export FAUCET_MNEMONIC=$(jq -r ".faucet[0].mnemonic" /configs/keys.json)
export | grep "FAUCET"
until bash -e /scripts/chain-rpc-ready.sh http://localhost:26657; do
sleep 10
done
/faucet/faucet --credit-coins="$CREDIT_COINS" --chain-fees="$FEES"
`.trim();
}
getIcsInitScript(chain, providerChain, exposerPort) {
const providerHostname = helpers.getChainName(providerChain.id);
return `
export
echo "Fetching priv keys from provider exposer"
curl -s http://${providerHostname}-genesis.$NAMESPACE.svc.cluster.local:${exposerPort}/priv_keys | jq > $CHAIN_DIR/config/provider_priv_validator_key.json
cat $CHAIN_DIR/config/provider_priv_validator_key.json
echo "Replace provider priv validator key with provider keys"
mv $CHAIN_DIR/config/priv_validator_key.json $CHAIN_DIR/config/previous_priv_validator_key.json
mv $CHAIN_DIR/config/provider_priv_validator_key.json $CHAIN_DIR/config/priv_validator_key.json
echo "Create consumer addition proposal"
DENOM=${providerChain?.denom} \\
CHAIN_ID=${providerChain?.id} \\
CHAIN_BIN=${providerChain?.binary || '$CHAIN_BIN'} \\
NODE_URL=http://${providerHostname}-genesis.$NAMESPACE.svc.cluster.local:26657 \\
PROPOSAL_FILE=/proposal/proposal.json \\
bash -e /scripts/create-ics.sh
echo "create ccv state file"
${providerChain?.binary || '$CHAIN_BIN'} query provider consumer-genesis ${chain.id} \\
--node http://${providerHostname}-genesis.$NAMESPACE.svc.cluster.local:26657 \\
-o json > $CHAIN_DIR/config/ccv-state.json
cat $CHAIN_DIR/config/ccv-state.json | jq
echo "Update genesis file with ccv state"
jq -s '.[0].app_state.ccvconsumer = .[1] | .[0]' $CHAIN_DIR/config/genesis.json $CHAIN_DIR/config/ccv-state.json > $CHAIN_DIR/config/genesis-ccv.json
mv $CHAIN_DIR/config/genesis.json $CHAIN_DIR/config/genesis-no-ccv.json
mv $CHAIN_DIR/config/genesis-ccv.json $CHAIN_DIR/config/genesis.json
`.trim();
}
}
exports.CosmosGenesisStatefulSetGenerator = CosmosGenesisStatefulSetGenerator;