@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
212 lines (189 loc) • 8.27 kB
text/typescript
// SPDX-License-Identifier: Apache-2.0
import {MissingArgumentError} from './errors/missing-argument-error.js';
import {SoloError} from './errors/solo-error.js';
import {Flags as flags} from '../commands/flags.js';
import fs from 'node:fs';
import {Templates} from './templates.js';
import {GrpcProxyTlsEnums} from './enumerations.js';
import {type ConfigManager} from './config-manager.js';
import {type K8Factory} from '../integration/kube/k8-factory.js';
import {type SoloLogger} from './logging/solo-logger.js';
import {AnyListrContext, type NodeAlias} from '../types/aliases.js';
import {inject, injectable} from 'tsyringe-neo';
import {patchInject} from './dependency-injection/container-helper.js';
import {type NamespaceName} from '../types/namespace/namespace-name.js';
import {SecretType} from '../integration/kube/resources/secret/secret-type.js';
import {InjectTokens} from './dependency-injection/inject-tokens.js';
import {SoloListr, type SoloListrTaskWrapper} from '../types/index.js';
/**
* Used to handle interactions with certificates data and inject it into the K8s cluster secrets
*/
()
export class CertificateManager {
constructor(
(InjectTokens.K8Factory) private readonly k8Factory?: K8Factory,
(InjectTokens.SoloLogger) private readonly logger?: SoloLogger,
(InjectTokens.ConfigManager) private readonly configManager?: ConfigManager,
) {
this.k8Factory = patchInject(k8Factory, InjectTokens.K8Factory, this.constructor.name);
this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
this.configManager = patchInject(configManager, InjectTokens.ConfigManager, this.constructor.name);
}
/**
* Reads the certificate and key and build the secret with the appropriate structure
*
* @param cert - file path to the certificate file
* @param key - file path to the key file
* @param type - the certificate type if it's for gRPC or gRPC Web
*
* @returns the secret
*/
private buildSecret(cert: string, key: string, type: GrpcProxyTlsEnums) {
switch (type) {
//? HAProxy
case GrpcProxyTlsEnums.GRPC: {
const certData = fs.readFileSync(cert).toString();
const keyData = fs.readFileSync(key).toString();
const pem = `${certData}\n${keyData}`;
return {
'tls.pem': Buffer.from(pem).toString('base64'),
};
}
//? Envoy
case GrpcProxyTlsEnums.GRPC_WEB: {
return {
'tls.crt': fs.readFileSync(cert).toString('base64'),
'tls.key': fs.readFileSync(key).toString('base64'),
};
}
}
}
/**
* Copies the TLS Certificates into K8s namespaced secret.
*
* @param nodeAlias - the alias of the node to which the TLS certificate should apply
* @param cert - file path to the certificate file
* @param key - file path to the key file
* @param type - the certificate type if it's for gRPC or gRPC Web
*/
private async copyTlsCertificate(nodeAlias: NodeAlias, cert: string, key: string, type: GrpcProxyTlsEnums) {
try {
const data: Record<string, string> = this.buildSecret(cert, key, type);
const name = Templates.renderGrpcTlsCertificatesSecretName(nodeAlias, type);
const namespace = this.getNamespace();
const labels = Templates.renderGrpcTlsCertificatesSecretLabelObject(nodeAlias, type);
const isSecretCreated = await this.k8Factory
.default()
.secrets()
.createOrReplace(namespace, name, SecretType.OPAQUE, data, labels);
if (!isSecretCreated) {
throw new SoloError(`failed to create secret for TLS certificates for node '${nodeAlias}'`);
}
} catch (error: Error | any) {
const errorMessage =
'failed to copy tls certificate to secret ' +
`'${Templates.renderGrpcTlsCertificatesSecretName(nodeAlias, type)}': ${error.message}`;
throw new SoloError(errorMessage, error);
}
}
/**
* Creates sub-tasks for copying the TLS Certificates into K8s secrets for gRPC and gRPC Web
*
* @param task - Listr Task to which to attach the sub-tasks
* @param grpcTlsCertificatePathsUnparsed - the unparsed (alias=path)[] for the gRPC Certificate
* @param grpcWebTlsCertificatePathsUnparsed - the unparsed (alias=path)[] for the gRPC Web Certificate
* @param grpcTlsKeyPathsUnparsed - the unparsed (alias=path)[] for the gRPC Certificate Key
* @param grpcWebTlsKeyPathsUnparsed - the unparsed (alias=path)[] for the gRPC Web Certificate Key
*
* @returns the build sub-tasks for creating the secrets
*/
public buildCopyTlsCertificatesTasks(
task: SoloListrTaskWrapper<any>,
grpcTlsCertificatePathsUnparsed: string,
grpcWebTlsCertificatePathsUnparsed: string,
grpcTlsKeyPathsUnparsed: string,
grpcWebTlsKeyPathsUnparsed: string,
): SoloListr<AnyListrContext> {
const subTasks = [];
const grpcTlsParsedValues = {
title: 'Copy gRPC TLS Certificate data',
certType: GrpcProxyTlsEnums.GRPC,
certs: this.parseAndValidate(grpcTlsCertificatePathsUnparsed, 'gRPC TLS Certificate paths'),
keys: this.parseAndValidate(grpcTlsKeyPathsUnparsed, 'gRPC TLS Certificate Key paths'),
};
const grpcWebTlsParsedValue = {
certType: GrpcProxyTlsEnums.GRPC_WEB,
title: 'Copy gRPC Web TLS data',
certs: this.parseAndValidate(grpcWebTlsCertificatePathsUnparsed, 'gRPC Web TLS Certificate paths'),
keys: this.parseAndValidate(grpcWebTlsKeyPathsUnparsed, 'gRPC Web Certificate TLS Key paths'),
};
if (grpcTlsParsedValues.certs.length !== grpcTlsParsedValues.keys.length) {
throw new SoloError(
"The structure of the gRPC TLS Certificate doesn't match" +
`Certificates: ${grpcTlsCertificatePathsUnparsed}, Keys: ${grpcTlsKeyPathsUnparsed}`,
);
}
if (grpcTlsParsedValues.certs.length !== grpcTlsParsedValues.keys.length) {
throw new SoloError(
"The structure of the gRPC Web TLS Certificate doesn't match" +
`Certificates: ${grpcWebTlsCertificatePathsUnparsed}, Keys: ${grpcWebTlsKeyPathsUnparsed}`,
);
}
for (const {certType, title, certs, keys} of [grpcTlsParsedValues, grpcWebTlsParsedValue]) {
if (certs.length === 0) {
continue;
}
for (const [index, cert_] of certs.entries()) {
const nodeAlias = cert_.nodeAlias;
const cert = cert_.filePath;
const key = keys[index].filePath;
subTasks.push({
title: `${title} for node ${nodeAlias}`,
task: () => this.copyTlsCertificate(nodeAlias, cert, key, certType),
});
}
}
return task.newListr(subTasks, {
concurrent: true,
rendererOptions: {collapseSubtasks: false},
});
}
/**
* Handles parsing the unparsed data validating it follows the structure
*
* @param input - the unparsed data ( ex. node0=/usr/bob/grpc-web.cert )
* @param type - of the data being parsed for the error logging
*
* @returns an array of parsed data with node alias and the path
*
* @throws SoloError - if the data doesn't follow the structure
*/
private parseAndValidate(input: string, type: string): {nodeAlias: NodeAlias; filePath: string}[] {
return input.split(',').map((line, index) => {
if (!line.includes('=')) {
throw new SoloError(`Failed to parse input ${input} of type ${type} on ${line}, index ${index}`);
}
const [nodeAlias, filePath] = line.split('=') as [NodeAlias, string];
if (!nodeAlias?.length || !filePath?.length) {
throw new SoloError(`Failed to parse input ${input} of type ${type} on ${line}, index ${index}`);
}
let fileExists = false;
try {
fileExists = fs.statSync(filePath).isFile();
} catch {
fileExists = false;
}
if (!fileExists) {
throw new SoloError(`File doesn't exist on path ${input} input of type ${type} on ${line}, index ${index}`);
}
return {nodeAlias, filePath};
});
}
private getNamespace() {
const ns = this.configManager.getFlag<NamespaceName>(flags.namespace);
if (!ns) {
throw new MissingArgumentError('namespace is not set');
}
return ns;
}
}