Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/commands/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {createHash} from 'node:crypto';
import {DeploymentPhase} from '../data/schema/model/remote/deployment-phase.js';
import {optionFromFlag} from './command-helpers.js';
import {HelmChartValues} from '../integration/helm/model/values.js';
import {buildSchedulingChartValues} from '../core/util/helm-scheduling-values.js';

interface ExplorerDeployConfigClass {
cacheDir: string;
Expand Down Expand Up @@ -428,7 +429,12 @@ export class ExplorerCommand extends BaseCommand {
title: 'Install explorer ingress controller',
skip: ({config}: ExplorerDeployContext | ExplorerUpgradeContext): boolean => !config.enableIngress,
task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => {
const explorerIngressControllerChartValues: HelmChartValues = new HelmChartValues();
const explorerChartValues: HelmChartValues = new HelmChartValues().filesFromCommaSeparatedInput(
config.valuesFile,
);
const explorerIngressControllerChartValues: HelmChartValues = new HelmChartValues().add(
buildSchedulingChartValues(explorerChartValues, 'controller'),
);

if (config.explorerStaticIp !== '') {
explorerIngressControllerChartValues.setLiteral('controller.service.loadBalancerIP', config.explorerStaticIp);
Expand All @@ -439,9 +445,7 @@ export class ExplorerCommand extends BaseCommand {
'controller.extraArgs.controller-class',
config.ingressReleaseName,
);
if (config.tlsClusterIssuerType === 'self-signed') {
explorerIngressControllerChartValues.filesFromCommaSeparatedInput(config.ingressControllerValueFile);
}
explorerIngressControllerChartValues.filesFromCommaSeparatedInput(config.ingressControllerValueFile);

await this.chartManager.upgrade(
config.namespace,
Expand Down
25 changes: 12 additions & 13 deletions src/commands/mirror-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {optionFromFlag} from './command-helpers.js';
import {ImageReference, type ParsedImageReference} from '../business/utils/image-reference.js';
import {HelmChartValues} from '../integration/helm/model/values.js';
import {K8} from '../integration/kube/k8.js';
import {buildSchedulingChartValues} from '../core/util/helm-scheduling-values.js';
// Port forwarding is now a method on the components object

interface MirrorNodeDeployConfigClass {
Expand Down Expand Up @@ -437,6 +438,7 @@ export class MirrorNodeCommand extends BaseCommand {
const chartValues: HelmChartValues = new HelmChartValues();

chartValues.filesFromCommaSeparatedInput(config.valuesFile);
chartValues.add(buildSchedulingChartValues(chartValues, 'pinger', 'pinger'));

config.mirrorNodeVersion = SemanticVersion.getValidSemanticVersion(
config.mirrorNodeVersion,
Expand Down Expand Up @@ -865,20 +867,10 @@ export class MirrorNodeCommand extends BaseCommand {
MirrorNodeCommand.MIRROR_ENVIRONMENT_VARIABLE_PREFIX,
);
},
skip: ({config}: MirrorNodeDeployContext): boolean =>
config.useExternalDatabase || !config.installSharedResources,
skip: ({config}: MirrorNodeDeployContext): boolean => config.useExternalDatabase,
};
}

/**
* Installs the mirror chart with all application components disabled in order to create the
* `mirror-passwords` secret. The init script (run by {@link initializeSharedPostgresDatabaseTask})
* reads that secret to obtain the DB user passwords, so the secret must exist before init runs.
* The importer must not be running during init (it would hold a session that blocks DROP DATABASE),
* so we use this lightweight prime install instead of a full chart install.
*
* Skipped when the secret already exists (upgrade path) or when using an external database.
*/
/**
* Deletes the `<release>-redis` secret so that the subsequent mirror chart install/upgrade
* re-creates it cleanly. This is necessary because Kubernetes strategic-merge-patch does not
Expand All @@ -899,6 +891,13 @@ export class MirrorNodeCommand extends BaseCommand {
};
}

/**
* Installs the mirror chart with all application components disabled in order to create the
* `mirror-passwords` secret. The init script (run by {@link initializeSharedPostgresDatabaseTask})
* reads that secret to obtain the DB user passwords, so the secret must exist before init runs.
* The importer must not be running during init (it would hold a session that blocks DROP DATABASE),
* so we use this lightweight prime install instead of a full chart install.
*/
private primePostgresSecretTask(): SoloListrTask<AnyListrContext> {
return {
title: 'Prime mirror-node postgres secret',
Expand Down Expand Up @@ -948,8 +947,7 @@ export class MirrorNodeCommand extends BaseCommand {
true,
);
},
skip: ({config}: MirrorNodeDeployContext): boolean =>
config.useExternalDatabase || !config.installSharedResources,
skip: ({config}: MirrorNodeDeployContext): boolean => config.useExternalDatabase,
};
}

Expand Down Expand Up @@ -992,6 +990,7 @@ export class MirrorNodeCommand extends BaseCommand {
const mirrorIngressControllerChartValues: HelmChartValues = new HelmChartValues().file(
constants.INGRESS_CONTROLLER_VALUES_FILE,
);
mirrorIngressControllerChartValues.add(buildSchedulingChartValues(config.chartValues, 'controller'));
if (config.mirrorStaticIp !== '') {
mirrorIngressControllerChartValues.setLiteral(
'controller.service.loadBalancerIP',
Expand Down
4 changes: 2 additions & 2 deletions src/core/shared-resources/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export class PostgresSharedResource {
'# 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:]')`,
`SENTINEL=$(psql -tc "SELECT shobj_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',
Expand All @@ -221,7 +221,7 @@ export class PostgresSharedResource {
` 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`,
` for role in mirror_graphql mirror_grpc mirror_importer mirror_api mirror_rest 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',
Expand Down
202 changes: 56 additions & 146 deletions src/core/shared-resources/shared-resource-manager.ts
Comment thread
jan-milenkov marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup rogue function exports, look for opportunity to make the code more readable, avoid unnecessary complexion

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@ import {type ChartManager} from '../chart-manager.js';
import {type NamespaceName} from '../../types/namespace/namespace-name.js';
import * as constants from '../../core/constants.js';
import {HelmChartValues, type HelmChartValue} from '../../integration/helm/model/values.js';
import * as fs from 'node:fs';
import yaml from 'yaml';

type HelmValuesObject = Record<string, unknown>;
type HelmMapValue = Record<string, HelmChartValue>;
type HelmToleration = HelmMapValue;

interface SharedResourceSchedulingValues {
postgresNodeSelector: HelmMapValue;
postgresTolerations: HelmToleration[];
redisNodeSelector: HelmMapValue;
redisTolerations: HelmToleration[];
}
import {
addSchedulingValues,
collectSchedulingValues,
type HelmSchedulingValues,
} from '../util/helm-scheduling-values.js';

const ROLE_SCHEDULING_KEY: string = 'solo.hashgraph.io/role';
const POSTGRES_SCHEDULING_SOURCE_PATHS: string[] = ['postgresql.postgresql', 'postgresql.primary'];
const REDIS_SCHEDULING_SOURCE_PATHS: string[] = ['redis', 'redis.master', 'redis.replica'];
const REDIS_SCHEDULING_TARGET_PATHS: string[] = ['redis.master', 'redis.replica'];
const REDIS_ROLE_FALLBACK_PATHS: string[] = [
'postgresql.postgresql',
'postgresql.primary',
'importer',
'grpc',
'rest',
'restjava',
'web3',
'monitor',
];

@injectable()
export class SharedResourceManager {
Expand Down Expand Up @@ -126,152 +133,55 @@ export class SharedResourceManager {
}

function buildSchedulingChartValues(sourceChartValues: HelmChartValues): HelmChartValues {
const schedulingValues: SharedResourceSchedulingValues = {
postgresNodeSelector: {},
postgresTolerations: [],
redisNodeSelector: {},
redisTolerations: [],
};

for (const valuesFilePath of getValuesFilePaths(sourceChartValues)) {
const values: unknown = yaml.parse(fs.readFileSync(valuesFilePath, 'utf8'));
if (!isHelmValuesObject(values)) {
continue;
}

mergeSchedulingValues(schedulingValues, values);
}

return toChartValues(schedulingValues);
}

function getValuesFilePaths(chartValues: HelmChartValues): string[] {
const arguments_: string[] = chartValues.toArguments();
const valuesFilePaths: string[] = [];

for (let index: number = 0; index < arguments_.length - 1; index++) {
if (arguments_[index] === '--values') {
valuesFilePaths.push(arguments_[index + 1]);
}
}

return valuesFilePaths;
}

function mergeSchedulingValues(target: SharedResourceSchedulingValues, values: HelmValuesObject): void {
const topLevelNodeSelector: HelmMapValue = getMapValue(values, 'nodeSelector');
const topLevelTolerations: HelmToleration[] = getTolerations(values, 'tolerations');

Object.assign(target.postgresNodeSelector, topLevelNodeSelector);
addTolerations(target.postgresTolerations, topLevelTolerations);
Object.assign(target.redisNodeSelector, topLevelNodeSelector);
addTolerations(target.redisTolerations, topLevelTolerations);

for (const path of ['postgresql.postgresql', 'postgresql.primary']) {
Object.assign(target.postgresNodeSelector, getMapValue(values, `${path}.nodeSelector`));
addTolerations(target.postgresTolerations, getTolerations(values, `${path}.tolerations`));
}

for (const path of ['redis', 'redis.master', 'redis.replica']) {
Object.assign(target.redisNodeSelector, getMapValue(values, `${path}.nodeSelector`));
addTolerations(target.redisTolerations, getTolerations(values, `${path}.tolerations`));
}
}

function getMapValue(values: HelmValuesObject, path: string): HelmMapValue {
const value: unknown = getValueAtPath(values, path);
if (!isHelmValuesObject(value)) {
return {};
}

return Object.fromEntries(
Object.entries(value).filter((entry: [string, unknown]): entry is [string, HelmChartValue] =>
isHelmChartValue(entry[1]),
),
const chartValues: HelmChartValues = new HelmChartValues();
const postgresSchedulingValues: HelmSchedulingValues = collectSchedulingValues(
sourceChartValues,
POSTGRES_SCHEDULING_SOURCE_PATHS,
);
const redisSchedulingValues: HelmSchedulingValues = collectSchedulingValues(
sourceChartValues,
REDIS_SCHEDULING_SOURCE_PATHS,
);
}

function getTolerations(values: HelmValuesObject, path: string): HelmToleration[] {
const value: unknown = getValueAtPath(values, path);
if (!Array.isArray(value)) {
return [];
}

return value
.filter(isHelmValuesObject)
.map(
(toleration: HelmValuesObject): HelmToleration =>
Object.fromEntries(
Object.entries(toleration).filter((entry: [string, unknown]): entry is [string, HelmChartValue] =>
isHelmChartValue(entry[1]),
),
),
)
.filter((toleration: HelmToleration): boolean => Object.keys(toleration).length > 0);
}

function getValueAtPath(values: HelmValuesObject, path: string): unknown {
let currentValue: unknown = values;

for (const segment of path.split('.')) {
if (!isHelmValuesObject(currentValue)) {
return undefined;
}
addMissingRedisRoleScheduling(redisSchedulingValues, sourceChartValues);
addSchedulingValues(chartValues, 'postgresql.primary', postgresSchedulingValues);

currentValue = currentValue[segment];
for (const redisPath of REDIS_SCHEDULING_TARGET_PATHS) {
addSchedulingValues(chartValues, redisPath, redisSchedulingValues);
}

return currentValue;
return chartValues;
}

function addTolerations(target: HelmToleration[], tolerations: HelmToleration[]): void {
const existing: Set<string> = new Set(target.map((toleration: HelmToleration): string => JSON.stringify(toleration)));
function addMissingRedisRoleScheduling(target: HelmSchedulingValues, sourceChartValues: HelmChartValues): void {
for (const path of REDIS_ROLE_FALLBACK_PATHS) {
const fallbackSchedulingValues: HelmSchedulingValues = collectSchedulingValues(sourceChartValues, [path], false);

for (const toleration of tolerations) {
const serialized: string = JSON.stringify(toleration);
if (!existing.has(serialized)) {
target.push(toleration);
existing.add(serialized);
if (target.nodeSelector[ROLE_SCHEDULING_KEY] === undefined) {
const role: HelmChartValue | undefined = fallbackSchedulingValues.nodeSelector[ROLE_SCHEDULING_KEY];
if (role !== undefined) {
target.nodeSelector[ROLE_SCHEDULING_KEY] = role;
}
}
}
}

function toChartValues(schedulingValues: SharedResourceSchedulingValues): HelmChartValues {
const chartValues: HelmChartValues = new HelmChartValues();

addNodeSelectorChartValues(chartValues, 'postgresql.primary.nodeSelector', schedulingValues.postgresNodeSelector);
addTolerationChartValues(chartValues, 'postgresql.primary.tolerations', schedulingValues.postgresTolerations);

for (const redisPath of ['redis.master', 'redis.replica']) {
addNodeSelectorChartValues(chartValues, `${redisPath}.nodeSelector`, schedulingValues.redisNodeSelector);
addTolerationChartValues(chartValues, `${redisPath}.tolerations`, schedulingValues.redisTolerations);
}

return chartValues;
}

function addNodeSelectorChartValues(chartValues: HelmChartValues, path: string, nodeSelector: HelmMapValue): void {
for (const [key, value] of Object.entries(nodeSelector)) {
chartValues.setLiteral(`${path}.${escapeHelmPathSegment(key)}`, value);
}
}
if (!hasTolerationForKey(target.tolerations, ROLE_SCHEDULING_KEY)) {
const toleration: Record<string, HelmChartValue> | undefined = fallbackSchedulingValues.tolerations.find(
(candidate: Record<string, HelmChartValue>): boolean => candidate.key === ROLE_SCHEDULING_KEY,
);
if (toleration) {
target.tolerations.push(toleration);
}
}

function addTolerationChartValues(chartValues: HelmChartValues, path: string, tolerations: HelmToleration[]): void {
for (const [index, toleration] of tolerations.entries()) {
for (const [key, value] of Object.entries(toleration)) {
chartValues.setLiteral(`${path}[${index}].${key}`, value);
if (
target.nodeSelector[ROLE_SCHEDULING_KEY] !== undefined &&
hasTolerationForKey(target.tolerations, ROLE_SCHEDULING_KEY)
) {
return;
}
}
}

function escapeHelmPathSegment(segment: string): string {
return segment.replaceAll('.', String.raw`\.`);
}

function isHelmValuesObject(value: unknown): value is HelmValuesObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function isHelmChartValue(value: unknown): value is HelmChartValue {
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
function hasTolerationForKey(tolerations: Record<string, HelmChartValue>[], key: string): boolean {
return tolerations.some((toleration: Record<string, HelmChartValue>): boolean => toleration.key === key);
}
Loading
Loading