@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
265 lines • 16.3 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var PostgresSharedResource_1;
import { InjectTokens } from '../dependency-injection/inject-tokens.js';
import { patchInject } from '../dependency-injection/container-helper.js';
import { inject, injectable } from 'tsyringe-neo';
import { PathEx } from '../../business/utils/path-ex.js';
import { ContainerReference } from '../../integration/kube/resources/container/container-reference.js';
import { Templates } from '../templates.js';
import { ContainerName } from '../../integration/kube/resources/container/container-name.js';
import * as constants from '../../core/constants.js';
import fs, { createWriteStream } from 'node:fs';
import { SOLO_CACHE_DIR } from '../constants.js';
import { MIRROR_NODE_VERSION } from '../../../version.js';
import { PassThrough, pipeline } from 'node:stream';
import { promisify } from 'node:util';
import { SoloError } from '../errors/solo-error.js';
import * as Base64 from 'js-base64';
import { sleep } from '../helpers.js';
import { Duration } from '../time/duration.js';
import { SemanticVersion } from '../../business/utils/semantic-version.js';
let PostgresSharedResource = class PostgresSharedResource {
static { PostgresSharedResource_1 = this; }
logger;
k8Factory;
helm;
chartManager;
static POSTGRES_LABEL_SELECTOR = [
'app.kubernetes.io/name=postgres',
'app.kubernetes.io/instance=solo-shared-resources',
];
constructor(logger, k8Factory, helm, chartManager) {
this.logger = logger;
this.k8Factory = k8Factory;
this.helm = helm;
this.chartManager = chartManager;
this.helm = patchInject(helm, InjectTokens.Helm, this.constructor.name);
this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
this.chartManager = patchInject(chartManager, InjectTokens.ChartManager, this.constructor.name);
}
async waitForPodReady(namespace, context) {
await this.k8Factory
.getK8(context)
.pods()
.waitForRunningPhase(namespace, PostgresSharedResource_1.POSTGRES_LABEL_SELECTOR, constants.PODS_RUNNING_MAX_ATTEMPTS, constants.PODS_RUNNING_DELAY);
}
async resolveContainerReference(namespace, context) {
const pods = this.k8Factory.getK8(context).pods();
const matchingPods = await pods.list(namespace, PostgresSharedResource_1.POSTGRES_LABEL_SELECTOR);
const postgresPod = matchingPods.find((pod) => Boolean(pod.podReference)) ?? matchingPods[0];
if (postgresPod?.podReference) {
return ContainerReference.of(postgresPod.podReference, ContainerName.of('postgresql'));
}
throw new SoloError(`Postgres pod not found in namespace ${namespace.name} with selector: ${PostgresSharedResource_1.POSTGRES_LABEL_SELECTOR.join(',')}`);
}
static tryToDecode(value) {
return Base64.decode(value) || value;
}
async initializeMirrorNode(namespace, context, prefix = 'HIERO') {
const containerReference = await this.resolveContainerReference(namespace, context);
const k8Container = this.k8Factory.getK8(context).containers().readByRef(containerReference);
const tag = getMirrorNodeReleaseTag(MIRROR_NODE_VERSION);
// check if path exists recursive PathEx.join(constants.SOLO_CACHE_DIR, 'mirror-node', mirrorRelease, 'init-script.sh')
if (!fs.existsSync(PathEx.join(SOLO_CACHE_DIR, 'mirror-node', tag))) {
fs.mkdirSync(PathEx.join(SOLO_CACHE_DIR, 'mirror-node', tag), { recursive: true });
}
const initScriptLocalPath = PathEx.join(SOLO_CACHE_DIR, 'mirror-node', tag, 'init-postgres.sh');
// Download and cache init script
if (!fs.existsSync(initScriptLocalPath)) {
const initScriptDownloadUrl = Templates.renderMirrorNodeDatabaseInitScriptUrl(tag);
this.logger.info(`Downloading Mirror Node Postgres init script from ${initScriptDownloadUrl}...`);
const response = await fetch(initScriptDownloadUrl);
if (!response.ok) {
throw new Error(`Failed to download Mirror Node Postgres init script from ${initScriptDownloadUrl}: ${response.status} ${response.statusText}`);
}
const fileStream = createWriteStream(initScriptLocalPath);
const streamPipeline = promisify(pipeline);
if (response.body && typeof response.body.getReader === 'function') {
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
// value is a Uint8Array chunk
await new Promise((resolve, reject) => {
fileStream.write(Buffer.from(value), (error) => (error ? reject(error) : resolve()));
});
}
fileStream.end();
await new Promise((resolve, reject) => {
fileStream.on('finish', resolve);
fileStream.on('error', reject);
});
}
finally {
// optional: release the lock if supported
reader.releaseLock?.();
}
}
else if (response.body && typeof response.body.pipe === 'function') {
await streamPipeline(response.body, fileStream);
}
else {
// Fallback: load into memory and write
const buffer = Buffer.from(await response.arrayBuffer());
fs.writeFileSync(initScriptLocalPath, buffer);
}
}
try {
await k8Container.copyTo(initScriptLocalPath, '/tmp');
await k8Container.execContainer('chmod +x /tmp/init-postgres.sh');
}
catch (error) {
throw new SoloError(`Failed to copy Mirror Node Postgres initialization script to container: ${error.message}`, error);
}
const sharedResourcesSecrets = await this.k8Factory
.getK8(context)
.secrets()
.list(namespace, ['app.kubernetes.io/instance=solo-shared-resources']);
const postgresPasswordsSecret = sharedResourcesSecrets.find((secret) => secret.name === 'solo-shared-resources-passwords');
const mirrorPasswordsSecret = await this.k8Factory
.getK8(context)
.secrets()
.read(namespace, 'mirror-passwords');
const superUserPassword = Base64.decode(postgresPasswordsSecret.data['password']);
const databaseName = Base64.decode(mirrorPasswordsSecret.data[`${prefix}_MIRROR_IMPORTER_DB_NAME`]);
const ownerUsername = Base64.decode(mirrorPasswordsSecret.data[`${prefix}_MIRROR_IMPORTER_DB_OWNER`]);
const ownerPassword = Base64.decode(mirrorPasswordsSecret.data[`${prefix}_MIRROR_IMPORTER_DB_OWNERPASSWORD`]);
const maxAttempts = 3;
const backoff = 2;
let attempt = 1;
while (attempt < maxAttempts) {
try {
const wrapperScriptName = 'run-init.sh';
const wrapperLines = [
'#!/usr/bin/env bash',
'set -e',
'',
'# connection and DB vars',
'export POSTGRES_USER=postgres',
'export PGUSER=postgres',
'export PGDATABASE=postgres',
'export PGHOST=127.0.0.1',
'export PGPORT=5432',
`export DB_NAME=${databaseName}`,
`export OWNER_USERNAME=${ownerUsername}`,
`export OWNER_PASSWORD=${ownerPassword}`,
'',
'# superuser password (from your secrets list)',
`export SUPERUSER_PASSWORD=${superUserPassword}`,
'',
'# build .pgpass with both postgres (superuser) and owner credentials',
'cat > /tmp/.pgpass <<EOF',
`127.0.0.1:5432:*:postgres:${superUserPassword}`,
`127.0.0.1:5432:${databaseName}:${ownerUsername}:${ownerPassword}`,
'EOF',
'chmod 600 /tmp/.pgpass',
'export PGPASSFILE=/tmp/.pgpass',
'',
'',
'# export the other API user passwords used by init script',
`export GRAPHQL_PASSWORD=${PostgresSharedResource_1.tryToDecode(mirrorPasswordsSecret.data[`${prefix}_MIRROR_GRAPHQL_DB_PASSWORD`])}`,
`export GRPC_PASSWORD=${PostgresSharedResource_1.tryToDecode(mirrorPasswordsSecret.data[`${prefix}_MIRROR_GRPC_DB_PASSWORD`])}`,
`export IMPORTER_PASSWORD=${PostgresSharedResource_1.tryToDecode(mirrorPasswordsSecret.data[`${prefix}_MIRROR_IMPORTER_DB_PASSWORD`])}`,
`export REST_PASSWORD=${PostgresSharedResource_1.tryToDecode(mirrorPasswordsSecret.data[`${prefix}_MIRROR_REST_DB_PASSWORD`])}`,
`export REST_JAVA_PASSWORD=${PostgresSharedResource_1.tryToDecode(mirrorPasswordsSecret.data[`${prefix}_MIRROR_RESTJAVA_DB_PASSWORD`])}`,
`export ROSETTA_PASSWORD=${PostgresSharedResource_1.tryToDecode(mirrorPasswordsSecret.data[`${prefix}_MIRROR_ROSETTA_DB_PASSWORD`])}`,
`export WEB3_PASSWORD=${PostgresSharedResource_1.tryToDecode(mirrorPasswordsSecret.data[`${prefix}_MIRROR_WEB3_DB_PASSWORD`])}`,
'',
'# Check for the sentinel comment that marks a fully completed initialization.',
'# Using a DB comment means the sentinel survives pod restarts and is only written',
'# after init-postgres.sh completes successfully (see end of this script).',
`SENTINEL=$(psql -tc "SELECT obj_description(oid, 'pg_database') FROM pg_database WHERE datname = '${databaseName}'" 2>/dev/null | tr -d '[:space:]')`,
'if [[ "${SENTINEL}" == "solo-initialized" ]]; then',
` echo "Initialization sentinel found on database '${databaseName}' — already complete, skipping."`,
' exit 0',
'fi',
'',
'# Handle partial initialization: database exists but no sentinel means a prior run',
'# was interrupted mid-script. Drop the database and all mirror node roles/users so',
'# init-postgres.sh can run cleanly from scratch.',
`DB_EXISTS=$(psql -tc "SELECT 1 FROM pg_database WHERE datname = '${databaseName}'" 2>/dev/null | tr -d '[:space:]')`,
'if [[ "${DB_EXISTS}" == "1" ]]; then',
` echo "Partial initialization detected: database '${databaseName}' exists but no sentinel. Cleaning up for fresh initialization."`,
` psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${databaseName}' AND pid <> pg_backend_pid();" 2>/dev/null || true`,
` psql -c "DROP DATABASE IF EXISTS ${databaseName};"`,
` for role in mirror_graphql mirror_grpc mirror_importer mirror_api mirror_rest_java mirror_rosetta mirror_web3 ${ownerUsername}; do`,
' psql -c "DROP USER IF EXISTS ${role};" 2>/dev/null || true',
' done',
' psql -c "DROP ROLE IF EXISTS temporary_admin, readwrite, readonly;" 2>/dev/null || true',
'else',
' # Database does not exist — check whether only the owner user was created (crash',
' # between CREATE USER and CREATE DATABASE). Drop the orphaned user so the init',
' # script can create it again (safe: no database means no owned objects).',
` OWNER_EXISTS=$(psql -tc "SELECT 1 FROM pg_roles WHERE rolname = '${ownerUsername}'" 2>/dev/null | tr -d '[:space:]')`,
' if [[ "${OWNER_EXISTS}" == "1" ]]; then',
` echo "Partial initialization detected: owner '${ownerUsername}' exists but database '${databaseName}' does not. Dropping owner for clean retry."`,
` psql -c "DROP USER ${ownerUsername};"`,
' fi',
'fi',
'',
'# Run the upstream init script. Not using exec so we can write the sentinel below.',
'/bin/bash /tmp/init-postgres.sh',
'',
'# Write sentinel to mark that initialization completed successfully.',
`psql -c "COMMENT ON DATABASE ${databaseName} IS 'solo-initialized';"`,
'echo "Initialization complete — sentinel written."',
];
const wrapper = wrapperLines.join('\n');
const temporaryLocal = PathEx.join(constants.SOLO_CACHE_DIR, wrapperScriptName);
fs.writeFileSync(temporaryLocal, wrapper);
await k8Container.copyTo(temporaryLocal, '/tmp');
await k8Container.execContainer(`chmod +x /tmp/${wrapperScriptName}`);
const outputStream = new PassThrough();
outputStream.on('data', (chunk) => {
this.logger.info(`${wrapperScriptName}: ${chunk.toString()}`);
});
const errorStream = new PassThrough();
errorStream.on('data', (chunk) => {
this.logger.info(`${wrapperScriptName}: ${chunk.toString()}`);
});
await k8Container.execContainer(`/bin/bash /tmp/${wrapperScriptName}`, outputStream, errorStream);
await k8Container.execContainer('rm /tmp/.pgpass');
await k8Container.execContainer(`rm /tmp/${wrapperScriptName}`);
fs.rmSync(temporaryLocal);
break;
}
catch (error) {
this.logger.error(`Failed to run Mirror Node Postgres initialization script in container. Attempt ${attempt} out of ${maxAttempts}: ${error}`);
attempt++;
if (attempt >= maxAttempts) {
throw new SoloError(`Failed to run Mirror Node Postgres initialization script in container after ${attempt} attempts: ${error}`, error);
}
await sleep(Duration.ofSeconds(backoff * attempt)); // wait before retrying
}
}
}
};
PostgresSharedResource = PostgresSharedResource_1 = __decorate([
injectable(),
__param(0, inject(InjectTokens.SoloLogger)),
__param(1, inject(InjectTokens.K8Factory)),
__param(2, inject(InjectTokens.Helm)),
__param(3, inject(InjectTokens.ChartManager)),
__metadata("design:paramtypes", [Object, Object, Object, Function])
], PostgresSharedResource);
export { PostgresSharedResource };
export function getMirrorNodeReleaseTag(version) {
return new SemanticVersion(version).toPrefixedString();
}
//# sourceMappingURL=postgres.js.map