@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
201 lines (183 loc) • 8.45 kB
text/typescript
// SPDX-License-Identifier: Apache-2.0
import {type NamespaceName} from '../types/namespace/namespace-name.js';
import {type PodReference} from '../integration/kube/resources/pod/pod-reference.js';
import {HEDERA_HAPI_PATH, LOG_CONFIG_ZIP_SUFFIX, ROOT_CONTAINER, SOLO_LOGS_DIR} from './constants.js';
import fs from 'node:fs';
import {ContainerReference} from '../integration/kube/resources/container/container-reference.js';
import * as constants from './constants.js';
import {sleep} from './helpers.js';
import {Duration} from './time/duration.js';
import {inject, injectable} from 'tsyringe-neo';
import {type SoloLogger} from './logging/solo-logger.js';
import {type K8Factory} from '../integration/kube/k8-factory.js';
import {patchInject} from './dependency-injection/container-helper.js';
import {InjectTokens} from './dependency-injection/inject-tokens.js';
import {type Pod} from '../integration/kube/resources/pod/pod.js';
import {PathEx} from '../business/utils/path-ex.js';
import {K8} from '../integration/kube/k8.js';
import {Container} from '../integration/kube/resources/container/container.js';
import chalk from 'chalk';
/**
* Class to manage network nodes
*/
()
export class NetworkNodes {
public constructor(
(InjectTokens.SoloLogger) private readonly logger?: SoloLogger,
(InjectTokens.K8Factory) private readonly k8Factory?: K8Factory,
) {
this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
}
/**
* Download logs files from all network pods and save to local solo log directory
* @param namespace - the namespace of the network
* @param [contexts]
* @param [baseDirectory] - optional base directory to save logs, defaults to SOLO_LOGS_DIR
* @param [excludeSensitiveData] - when true, omit TLS certificates, private keys, and data/keys from the archive
* @returns a promise that resolves when the logs are downloaded
*/
public async getLogs(
namespace: NamespaceName,
contexts?: string[],
baseDirectory?: string,
excludeSensitiveData?: boolean,
): Promise<void[]> {
const podsData: {pod: Pod; context?: string}[] = [];
if (contexts) {
for (const context of contexts) {
const pods: Pod[] = await this.k8Factory
.getK8(context)
.pods()
.list(namespace, ['solo.hedera.com/type=network-node']);
for (const pod of pods) {
podsData.push({pod, context});
}
}
} else {
const pods: Pod[] = await this.k8Factory.default().pods().list(namespace, ['solo.hedera.com/type=network-node']);
for (const pod of pods) {
podsData.push({pod});
}
}
const logBaseDirectory: string = baseDirectory || SOLO_LOGS_DIR;
const promises: Promise<void>[] = [];
for (const podData of podsData) {
promises.push(this.getLog(podData.pod, namespace, logBaseDirectory, podData.context, excludeSensitiveData));
}
this.logger.showUser(`Configurations and logs saved to ${logBaseDirectory}`);
return await Promise.all(promises);
}
private async getLog(
pod: Pod,
namespace: NamespaceName,
baseDirectory: string,
context?: string,
excludeSensitiveData?: boolean,
): Promise<void> {
const podReference: PodReference = pod.podReference;
this.logger.debug(`getNodeLogs(${pod.podReference.name.name}): begin...`);
const targetDirectory: string = PathEx.join(baseDirectory, namespace.name);
try {
if (!fs.existsSync(targetDirectory)) {
fs.mkdirSync(targetDirectory, {recursive: true});
}
const containerReference: ContainerReference = ContainerReference.of(podReference, ROOT_CONTAINER);
const scriptName: string = 'support-zip.sh';
const sourcePath: string = PathEx.joinWithRealPath(constants.RESOURCES_DIR, scriptName); // script source path
const k8: K8 = this.k8Factory.getK8(context);
const container: Container = k8.containers().readByRef(containerReference);
await container.copyTo(sourcePath, `${HEDERA_HAPI_PATH}`);
await sleep(Duration.ofSeconds(3)); // wait for the script to sync to the file system
await container.execContainer([
'bash',
'-c',
`sync ${HEDERA_HAPI_PATH} && chown hedera:hedera ${HEDERA_HAPI_PATH}/${scriptName}`,
]);
await container.execContainer(['bash', '-c', `chmod 0755 ${HEDERA_HAPI_PATH}/${scriptName}`]);
await container.execContainer(
`${HEDERA_HAPI_PATH}/${scriptName} true ${excludeSensitiveData === true ? 'true' : 'false'}`,
);
await container.copyFrom(
`${HEDERA_HAPI_PATH}/data/${podReference.name}${LOG_CONFIG_ZIP_SUFFIX}`,
targetDirectory,
);
this.logger.showUser(
`Log zip file ${podReference.name}${LOG_CONFIG_ZIP_SUFFIX} downloaded to ${targetDirectory}`,
);
} catch (error) {
// not throw error here, so we can continue to finish downloading logs from other pods
// and also delete namespace in the end
this.logger.error(`${constants.NODE_LOG_FAILURE_MSG} ${podReference}`, error);
this.logger.showUser(chalk.red(`${constants.NODE_LOG_FAILURE_MSG} ${podReference}`));
}
this.logger.debug(`getNodeLogs(${pod.podReference.name.name}): ...end`);
}
/**
* Download state files from a pod
* @param namespace - the namespace of the network
* @param nodeAlias - the pod name
* @param [context]
* @param [baseDirectory] - optional base directory to save state files, defaults to SOLO_LOGS_DIR
* @returns a promise that resolves when the state files are downloaded
*/
public async getStatesFromPod(
namespace: NamespaceName,
nodeAlias: string,
context?: string,
baseDirectory?: string,
): Promise<void[]> {
const pods: Pod[] = await this.k8Factory
.getK8(context)
.pods()
.list(namespace, [`solo.hedera.com/node-name=${nodeAlias}`, 'solo.hedera.com/type=network-node']);
// get length of pods
const stateBaseDirectory: string = baseDirectory || SOLO_LOGS_DIR;
const promises: Promise<void>[] = [];
for (const pod of pods) {
promises.push(this.getState(pod, namespace, stateBaseDirectory, context));
}
return await Promise.all(promises);
}
private async getState(pod: Pod, namespace: NamespaceName, baseDirectory: string, context?: string): Promise<void> {
const podReference: PodReference = pod.podReference;
this.logger.debug(`getNodeState(${pod.podReference.name.name}): begin...`);
const targetDirectory: string = PathEx.join(baseDirectory, namespace.name);
try {
if (!fs.existsSync(targetDirectory)) {
fs.mkdirSync(targetDirectory, {recursive: true});
}
// Use zip for compression, similar to tar -czf with -C flag
const containerReference: ContainerReference = ContainerReference.of(podReference, ROOT_CONTAINER);
const k8: K8 = this.k8Factory.getK8(context);
const zipFileName: string = `${HEDERA_HAPI_PATH}/${podReference.name}-state.zip`;
// Zip doesn't have a -C flag like tar, so we use sh -c with subshell to change directory
// Use the -X to archive for cross-platform compatibility
await k8
.containers()
.readByRef(containerReference)
.execContainer([
'sh',
'-c',
`(cd ${HEDERA_HAPI_PATH}/data/saved && zip -rX ${zipFileName} . && sync && test -f ${zipFileName})`,
]);
await sleep(Duration.ofSeconds(1));
await k8.containers().readByRef(containerReference).copyFrom(`${zipFileName}`, targetDirectory);
} catch (error: Error | unknown) {
this.logger.error(`failed to download state from pod ${podReference.name}`, error);
this.logger.showUser(`Failed to download state from pod ${podReference.name}` + error);
}
this.logger.debug(`getNodeState(${pod.podReference.name.name}): ...end`);
}
public async getNetworkNodePodStatus(podReference: PodReference, context?: string): Promise<string> {
return this.k8Factory
.getK8(context)
.containers()
.readByRef(ContainerReference.of(podReference, constants.ROOT_CONTAINER))
.execContainer([
'bash',
'-c',
String.raw`curl -s http://localhost:9999/metrics | grep platform_PlatformStatus | grep -v \#`,
]);
}
}