@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
334 lines • 17 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 CacheCommand_1;
import { SoloError } from '../core/errors/solo-error.js';
import * as constants from '../core/constants.js';
import { BaseCommand } from './base.js';
import { Flags as flags } from './flags.js';
import { inject, injectable } from 'tsyringe-neo';
import { ImageCacheHandlerBuilder } from '../integration/cache/impl/image-cache-handler-builder.js';
import { InjectTokens } from '../core/dependency-injection/inject-tokens.js';
import { patchInject } from '../core/dependency-injection/container-helper.js';
import fs from 'node:fs/promises';
import { CacheImageTargetTemplateRenderer } from '../integration/cache/impl/cache-image-target-template-renderer.js';
import { PathEx } from '../business/utils/path-ex.js';
import { CacheImageTemplateValues } from '../integration/cache/models/impl/cache-image-template-values.js';
import * as version from '../../version.js';
import { DefaultCacheImageTemplateResolver } from '../integration/cache/impl/default-cache-image-template-resolver.js';
let CacheCommand = class CacheCommand extends BaseCommand {
static { CacheCommand_1 = this; }
containerEngineClient;
static CACHE_NOT_MATERIALIZED_ERROR_MESSAGE = 'Cache image targets have not been materialized yet. Run `solo cache image pull` first.';
constructor(containerEngineClient) {
super();
this.containerEngineClient = containerEngineClient;
this.containerEngineClient = patchInject(containerEngineClient, InjectTokens.ContainerEngineClient, this.constructor.name);
}
async close() { }
// ------ Flags ------ //
static PULL_FLAGS_LIST = {
required: [],
optional: [flags.quiet, flags.cacheDir, flags.devMode, flags.edgeEnabled],
};
static LOAD_FLAGS_LIST = {
required: [],
optional: [flags.quiet, flags.cacheDir, flags.devMode, flags.clusterRef],
};
static LIST_FLAGS_LIST = {
required: [],
optional: [flags.quiet, flags.cacheDir, flags.devMode],
};
static CLEAR_FLAGS_LIST = {
required: [],
optional: [flags.quiet, flags.cacheDir, flags.devMode],
};
static STATUS_FLAGS_LIST = {
required: [],
optional: [flags.quiet, flags.cacheDir, flags.devMode, flags.clusterRef],
};
// ----- Handlers ------- //
async pull(argv) {
const tasks = this.taskList.newTaskList([
{
title: 'Initialize',
task: async (context_, task) => {
this.configManager.update(argv);
flags.disablePrompts(CacheCommand_1.PULL_FLAGS_LIST.optional);
const allFlags = [
...CacheCommand_1.PULL_FLAGS_LIST.required,
...CacheCommand_1.PULL_FLAGS_LIST.optional,
];
await this.configManager.executePrompt(task, allFlags);
const edgeEnabled = this.configManager.getFlag(flags.edgeEnabled);
const renderedYamlPath = await this.renderImageTargetsFile(edgeEnabled);
context_.config = {
imageCacheHandler: await this.buildImageCacheHandlerFromYaml(renderedYamlPath),
results: [],
edgeEnabled,
};
},
},
this.pullAndCacheContainerImages(),
this.showUserMessages(),
], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, undefined, 'cache image pull');
if (tasks.isRoot()) {
try {
await tasks.run();
}
catch (error) {
throw new SoloError(`Error pulling cache: ${error.message}`, error);
}
}
else {
this.taskList.registerCloseFunction(async () => { });
}
return true;
}
async load(argv) {
const tasks = this.taskList.newTaskList([
{
title: 'Initialize',
task: async (context_, task) => {
await this.localConfig.load();
this.configManager.update(argv);
flags.disablePrompts(CacheCommand_1.LOAD_FLAGS_LIST.optional);
const allFlags = [
...CacheCommand_1.LOAD_FLAGS_LIST.required,
...CacheCommand_1.LOAD_FLAGS_LIST.optional,
];
await this.configManager.executePrompt(task, allFlags);
const clusterReference = this.getClusterReference();
const context = this.getClusterContext(clusterReference);
const cacheDirectory = this.configManager.getFlag(flags.cacheDir);
context_.config = {
imageCacheHandler: await this.buildImageCacheHandlerFromRenderedFile(cacheDirectory),
clusterReference,
context,
};
},
},
this.loadImagesIntoCluster(),
this.showUserMessages(),
], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, undefined, 'cache image load');
if (tasks.isRoot()) {
try {
await tasks.run();
}
catch (error) {
throw new SoloError(`Error loading from cache: ${error.message}`, error);
}
}
else {
this.taskList.registerCloseFunction(async () => { });
}
return true;
}
async list() {
const tasks = this.taskList.newTaskList([
{
title: 'List cached images',
task: async (context_) => {
const cacheDirectory = this.configManager.getFlag(flags.cacheDir);
const config = {
imageCacheHandler: await this.buildImageCacheHandlerFromRenderedFile(cacheDirectory),
};
context_.config = config;
const cachedItems = await config.imageCacheHandler.list();
try {
this.logger.showList(`Cached images: [${cachedItems.length}]`, cachedItems.map((item) => `${item.target.name}:${item.target.version}`));
}
catch {
this.logger.warn('No cache manifest found');
}
},
},
], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, undefined, 'cache image list');
await tasks.run();
return true;
}
async clear() {
const tasks = this.taskList.newTaskList([
{
title: 'Clear image cache',
task: async (context_) => {
const cacheDirectory = this.configManager.getFlag(flags.cacheDir);
const renderedYamlPath = this.getRenderedImageTargetsFilePath(cacheDirectory);
try {
const config = {
imageCacheHandler: await this.buildImageCacheHandlerFromRenderedFile(cacheDirectory),
};
context_.config = config;
await config.imageCacheHandler.clear();
}
catch (error) {
if (!(error instanceof SoloError) ||
error.message !== CacheCommand_1.CACHE_NOT_MATERIALIZED_ERROR_MESSAGE) {
throw error;
}
}
await fs.rm(renderedYamlPath, { force: true });
},
},
], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, undefined, 'cache image clear');
await tasks.run();
return true;
}
async status(argv) {
const tasks = this.taskList.newTaskList([
{
title: 'Check cache status',
task: async (context_, task) => {
this.configManager.update(argv);
flags.disablePrompts(CacheCommand_1.STATUS_FLAGS_LIST.optional);
const allFlags = [
...CacheCommand_1.STATUS_FLAGS_LIST.required,
...CacheCommand_1.STATUS_FLAGS_LIST.optional,
];
await this.configManager.executePrompt(task, allFlags);
const cacheDirectory = this.configManager.getFlag(flags.cacheDir);
const config = {
imageCacheHandler: await this.buildImageCacheHandlerFromRenderedFile(cacheDirectory),
};
const clusterReference = this.configManager.getFlag(flags.clusterRef);
if (clusterReference) {
await this.localConfig.load();
const context = this.getClusterContext(clusterReference);
config.clusterReference = clusterReference;
config.context = context;
config.clusterName = this.prepareClusterName(this.k8Factory.getK8(context).clusters().readCurrent());
}
else {
try {
config.clusterName = this.prepareClusterName(this.k8Factory.default().clusters().readCurrent());
}
catch {
// Best effort only. Local cache status should still work.
}
}
context_.config = config;
const items = await config.imageCacheHandler.healthcheck();
const cachedItems = await config.imageCacheHandler.list();
const expectedTargets = await config.imageCacheHandler.resolveRequiredArtifacts();
const expectedImages = expectedTargets.map((target) => `${target.name}:${target.version}`);
const missingImages = items
.filter((item) => !item.healthy)
.map((item) => `${item.target.name}:${item.target.version}`);
let totalBytes = 0;
for (const item of cachedItems) {
try {
const stat = await fs.stat(item.localPath);
totalBytes += stat.size;
}
catch {
// missing files are already reflected by healthcheck
}
}
const totalSizeMb = (totalBytes / (1024 * 1024)).toFixed(2);
this.logger.showUser(`Cached images: ${items.length}`);
this.logger.showUser(`Total size: ${totalSizeMb} MB`);
this.logger.showUser(`Healthy: ${missingImages.length === 0}`);
if (missingImages.length > 0) {
this.logger.showList('Missing cache archives', missingImages);
}
if (!config.clusterName) {
this.logger.showUser('Cluster images: unavailable');
return;
}
try {
const clusterImages = await this.containerEngineClient.listLoadedImagesInCluster(config.clusterName);
const clusterImageSet = new Set(clusterImages);
const expectedImageSet = new Set(expectedImages);
const loadedExpectedImages = expectedImages.filter((image) => clusterImageSet.has(image));
const missingInCluster = expectedImages.filter((image) => !clusterImageSet.has(image));
const additionalClusterImages = clusterImages.filter((image) => !expectedImageSet.has(image));
this.logger.showUser(`Cluster loaded expected images: ${loadedExpectedImages.length}/${expectedImages.length}`);
if (missingInCluster.length > 0) {
this.logger.showList('Expected but not loaded in cluster', missingInCluster);
}
if (additionalClusterImages.length > 0) {
this.logger.showList('Additional images loaded in cluster', additionalClusterImages);
}
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.showUser(`Cluster images: failed to inspect (${message})`);
}
},
},
], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, undefined, 'cache image status');
await tasks.run();
return true;
}
// ------ Tasks ------ //
pullAndCacheContainerImages() {
return {
title: 'Pull and cache container images',
task: async ({ config: { imageCacheHandler } }, task) => {
return task.newListr(await imageCacheHandler.pull(), constants.LISTR_DEFAULT_OPTIONS.WITH_CONCURRENCY);
},
};
}
loadImagesIntoCluster() {
return {
title: 'Load images into cluster',
task: async ({ config: { imageCacheHandler, context } }, task) => {
const subTasks = [];
const newTasks = await imageCacheHandler.load(this.prepareClusterName(this.k8Factory.getK8(context).clusters().readCurrent()));
subTasks.push(...newTasks);
return task.newListr(subTasks, constants.LISTR_DEFAULT_OPTIONS.WITH_CONCURRENCY);
},
};
}
showUserMessages() {
return {
title: 'Show user messages',
skip: () => this.oneShotState.isActive(),
task: () => {
this.logger.showAllMessageGroups();
},
};
}
// ------ Helpers ------ //
prepareClusterName(clusterReference) {
return clusterReference.startsWith('kind-') ? clusterReference.replace('kind-', '') : clusterReference;
}
getRenderedImageTargetsFilePath(cacheDirectory) {
return PathEx.join(cacheDirectory, 'config', CacheImageTargetTemplateRenderer.RENDERED_FILE_NAME);
}
async renderImageTargetsFile(edgeEnabled) {
const cacheDirectory = this.configManager.getFlag(flags.cacheDir);
const renderedConfigDirectory = PathEx.join(cacheDirectory, 'config');
return new CacheImageTargetTemplateRenderer(new DefaultCacheImageTemplateResolver(new CacheImageTemplateValues(edgeEnabled ? version.MIRROR_NODE_EDGE_VERSION : version.MIRROR_NODE_VERSION, edgeEnabled ? version.BLOCK_NODE_EDGE_VERSION : version.BLOCK_NODE_VERSION, edgeEnabled ? version.HEDERA_JSON_RPC_RELAY_EDGE_VERSION : version.HEDERA_JSON_RPC_RELAY_VERSION, edgeEnabled ? version.EXPLORER_EDGE_VERSION : version.EXPLORER_VERSION, version.MINIO_OPERATOR_VERSION, edgeEnabled ? version.HEDERA_PLATFORM_EDGE_VERSION : version.HEDERA_PLATFORM_VERSION))).renderToFile(constants.SOLO_CACHE_IMAGES_TARGET_FILE, renderedConfigDirectory);
}
async buildImageCacheHandlerFromYaml(filePath) {
return ImageCacheHandlerBuilder.fromYaml(filePath).engine(this.containerEngineClient).build();
}
async buildImageCacheHandlerFromRenderedFile(cacheDirectory) {
const renderedYamlPath = this.getRenderedImageTargetsFilePath(cacheDirectory);
try {
await fs.access(renderedYamlPath);
}
catch {
throw new SoloError(CacheCommand_1.CACHE_NOT_MATERIALIZED_ERROR_MESSAGE);
}
return this.buildImageCacheHandlerFromYaml(renderedYamlPath);
}
};
CacheCommand = CacheCommand_1 = __decorate([
injectable(),
__param(0, inject(InjectTokens.ContainerEngineClient)),
__metadata("design:paramtypes", [Object])
], CacheCommand);
export { CacheCommand };
//# sourceMappingURL=cache.js.map