UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

265 lines 16.3 kB
// 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