Skip to content

Commit 04b616e

Browse files
refactor: add k8.ContainerName and implement throughout the codebase (#1292)
Signed-off-by: Jeromy Cannon <[email protected]>
1 parent 620e2f4 commit 04b616e

File tree

9 files changed

+136
-35
lines changed

9 files changed

+136
-35
lines changed

src/commands/mirror_node.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {type Optional, type SoloListrTask} from '../types/index.js';
2222
import * as Base64 from 'js-base64';
2323
import {type NamespaceName} from '../core/kube/namespace_name.js';
2424
import {PodRef} from '../core/kube/pod_ref.js';
25+
import {ContainerName} from '../core/kube/container_name.js';
2526

2627
interface MirrorNodeDeployConfigClass {
2728
chartDirectory: string;
@@ -357,7 +358,7 @@ export class MirrorNodeCommand extends BaseCommand {
357358
throw new SoloError('postgres pod not found');
358359
}
359360
const postgresPodName = PodName.of(pods[0].metadata.name);
360-
const postgresContainerName = 'postgresql';
361+
const postgresContainerName = ContainerName.of('postgresql');
361362
const postgresPodRef = PodRef.of(namespace, postgresPodName);
362363
const mirrorEnvVars = await self.k8.execContainer(
363364
postgresPodRef,

src/core/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {color, type ListrLogger, PRESET_TIMER} from 'listr2';
77
import path, {dirname, normalize} from 'path';
88
import {fileURLToPath} from 'url';
99
import {NamespaceName} from './kube/namespace_name.js';
10+
import {ContainerName} from './kube/container_name.js';
1011

1112
export const ROOT_DIR = path.join(dirname(fileURLToPath(import.meta.url)), '..', '..');
1213

@@ -20,7 +21,7 @@ export const DEFAULT_CERT_MANAGER_NAMESPACE = NamespaceName.of('cert-manager');
2021
export const HELM = 'helm';
2122
export const RESOURCES_DIR = normalize(path.join(ROOT_DIR, 'resources'));
2223

23-
export const ROOT_CONTAINER = 'root-container';
24+
export const ROOT_CONTAINER = ContainerName.of('root-container');
2425
export const SOLO_REMOTE_CONFIGMAP_NAME = 'solo-remote-config';
2526
export const SOLO_REMOTE_CONFIGMAP_LABELS = {'solo.hedera.com/type': 'remote-config'};
2627
export const SOLO_REMOTE_CONFIG_MAX_COMMAND_IN_HISTORY = 50;

src/core/kube/container_name.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* SPDX-License-Identifier: Apache-2.0
3+
*/
4+
import {isDns1123Label} from './kube_validation.js';
5+
import {ContainerNameInvalidError} from './kube_errors.js';
6+
7+
/**
8+
* Represents a Kubernetes container name. A Kubernetes container name must be a valid RFC-1123 DNS label.
9+
*
10+
* @include DNS_1123_LABEL
11+
*/
12+
export class ContainerName {
13+
private constructor(public readonly name: string) {
14+
if (!this.isValid()) {
15+
throw new ContainerNameInvalidError(name);
16+
}
17+
}
18+
19+
/**
20+
* Creates a container. A Kubernetes container name must be a valid RFC-1123 DNS label.
21+
*
22+
* @include DNS_1123_LABEL
23+
*
24+
* @param name The name of the container.
25+
* @returns An instance of ContainerName.
26+
* @throws ContainerNameInvalidError if the container name is invalid.
27+
*/
28+
public static of(name: string): ContainerName {
29+
return new ContainerName(name);
30+
}
31+
32+
/**
33+
* Returns true if the container name is valid. A Kubernetes container name must be a valid RFC-1123 DNS label.
34+
*
35+
* @include DNS_1123_LABEL
36+
*
37+
* @returns true if the container name is valid.
38+
* @throws ContainerNameInvalidError if the container name is invalid.
39+
*/
40+
private isValid(): boolean {
41+
return isDns1123Label(this.name);
42+
}
43+
44+
/**
45+
* Compares this instance with another ContainerName.
46+
* @param other The other ContainerName instance.
47+
* @returns true if both instances have the same name.
48+
*/
49+
public equals(other: ContainerName): boolean {
50+
return other instanceof ContainerName && this.name === other.name;
51+
}
52+
53+
/**
54+
* Allows implicit conversion to a string.
55+
* @returns The container name as a string.
56+
*/
57+
public toString(): string {
58+
return this.name;
59+
}
60+
61+
/**
62+
* Allows `ContainerName` to be used as a primitive string in operations.
63+
*/
64+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
65+
public [Symbol.toPrimitive](hint: string): string {
66+
return this.name;
67+
}
68+
69+
/**
70+
* Returns the primitive value of the object.
71+
*/
72+
public valueOf(): string {
73+
return this.name;
74+
}
75+
}

src/core/kube/k8.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {type V1Lease} from '@kubernetes/client-node';
1010
import {type Namespaces} from './namespaces.js';
1111
import {type NamespaceName} from './namespace_name.js';
1212
import {type Clusters} from './clusters.js';
13+
import {type ContainerName} from './container_name.js';
1314

1415
export interface K8 {
1516
/**
@@ -103,7 +104,7 @@ export interface K8 {
103104
* @param destPath - path inside the container
104105
* @returns a promise that returns array of directory entries, custom object
105106
*/
106-
listDir(podRef: PodRef, containerName: string, destPath: string): Promise<any[] | TDirectoryData[]>;
107+
listDir(podRef: PodRef, containerName: ContainerName, destPath: string): Promise<any[] | TDirectoryData[]>;
107108

108109
/**
109110
* Check if a filepath exists in the container
@@ -112,17 +113,17 @@ export interface K8 {
112113
* @param destPath - path inside the container
113114
* @param [filters] - an object with metadata fields and value
114115
*/
115-
hasFile(podRef: PodRef, containerName: string, destPath: string, filters?: object): Promise<boolean>;
116+
hasFile(podRef: PodRef, containerName: ContainerName, destPath: string, filters?: object): Promise<boolean>;
116117

117118
/**
118119
* Check if a directory path exists in the container
119120
* @param podRef - the pod reference
120121
* @param containerName - the container name
121122
* @param destPath - path inside the container
122123
*/
123-
hasDir(podRef: PodRef, containerName: string, destPath: string): Promise<boolean>;
124+
hasDir(podRef: PodRef, containerName: ContainerName, destPath: string): Promise<boolean>;
124125

125-
mkdir(podRef: PodRef, containerName: string, destPath: string): Promise<string>;
126+
mkdir(podRef: PodRef, containerName: ContainerName, destPath: string): Promise<string>;
126127

127128
/**
128129
* Copy a file into a container
@@ -138,7 +139,7 @@ export interface K8 {
138139
*/
139140
copyTo(
140141
podRef: PodRef,
141-
containerName: string,
142+
containerName: ContainerName,
142143
srcPath: string,
143144
destDir: string,
144145
filter?: TarCreateFilter | undefined,
@@ -154,7 +155,7 @@ export interface K8 {
154155
* @param srcPath - source file path in the container
155156
* @param destDir - destination directory in the local
156157
*/
157-
copyFrom(podRef: PodRef, containerName: string, srcPath: string, destDir: string): Promise<unknown>;
158+
copyFrom(podRef: PodRef, containerName: ContainerName, srcPath: string, destDir: string): Promise<unknown>;
158159

159160
/**
160161
* Invoke sh command within a container and return the console output as string
@@ -163,7 +164,7 @@ export interface K8 {
163164
* @param command - sh commands as an array to be run within the containerName (e.g 'ls -la /opt/hgcapp')
164165
* @returns console output as string
165166
*/
166-
execContainer(podRef: PodRef, containerName: string, command: string | string[]): Promise<string>;
167+
execContainer(podRef: PodRef, containerName: ContainerName, command: string | string[]): Promise<string>;
167168

168169
/**
169170
* Port forward a port from a pod to localhost

src/core/kube/k8_client.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {NamespaceName} from './namespace_name.js';
3333
import K8ClientClusters from './k8_client/k8_client_clusters.js';
3434
import {type Clusters} from './clusters.js';
3535
import {PodRef} from './pod_ref.js';
36+
import {type ContainerName} from './container_name.js';
3637

3738
/**
3839
* A kubernetes API wrapper class providing custom functionalities required by solo
@@ -285,7 +286,7 @@ export class K8Client implements K8 {
285286
return this.cachedContexts;
286287
}
287288

288-
public async listDir(podRef: PodRef, containerName: string, destPath: string) {
289+
public async listDir(podRef: PodRef, containerName: ContainerName, destPath: string) {
289290
// TODO future, return the following
290291
// return this.pods.byName(podName).listDir(containerName, destPath);
291292
// byName(podName) can use an underlying cache to avoid multiple calls to the API
@@ -341,13 +342,13 @@ export class K8Client implements K8 {
341342
return items;
342343
} catch (e) {
343344
throw new SoloError(
344-
`unable to check path in '${podRef.podName.name}':${containerName}' - ${destPath}: ${e.message}`,
345+
`unable to check path in '${podRef.podName.name}':${containerName.name}' - ${destPath}: ${e.message}`,
345346
e,
346347
);
347348
}
348349
}
349350

350-
public async hasFile(podRef: PodRef, containerName: string, destPath: string, filters: object = {}) {
351+
public async hasFile(podRef: PodRef, containerName: ContainerName, destPath: string, filters: object = {}) {
351352
const parentDir = path.dirname(destPath);
352353
const fileName = path.basename(destPath);
353354
const filterMap = new Map(Object.entries(filters));
@@ -363,7 +364,7 @@ export class K8Client implements K8 {
363364
const field = entry[0];
364365
const value = entry[1];
365366
this.logger.debug(
366-
`Checking file ${podRef.podName.name}:${containerName} ${destPath}; ${field} expected ${value}, found ${item[field]}`,
367+
`Checking file ${podRef.podName.name}:${containerName.name} ${destPath}; ${field} expected ${value}, found ${item[field]}`,
367368
{filters},
368369
);
369370
if (`${value}` !== `${item[field]}`) {
@@ -373,14 +374,16 @@ export class K8Client implements K8 {
373374
}
374375

375376
if (found) {
376-
this.logger.debug(`File check succeeded ${podRef.podName.name}:${containerName} ${destPath}`, {filters});
377+
this.logger.debug(`File check succeeded ${podRef.podName.name}:${containerName.name} ${destPath}`, {
378+
filters,
379+
});
377380
return true;
378381
}
379382
}
380383
}
381384
} catch (e) {
382385
const error = new SoloError(
383-
`unable to check file in '${podRef.podName.name}':${containerName}' - ${destPath}: ${e.message}`,
386+
`unable to check file in '${podRef.podName.name}':${containerName.name}' - ${destPath}: ${e.message}`,
384387
e,
385388
);
386389
this.logger.error(error.message, error);
@@ -390,7 +393,7 @@ export class K8Client implements K8 {
390393
return false;
391394
}
392395

393-
public async hasDir(podRef: PodRef, containerName: string, destPath: string) {
396+
public async hasDir(podRef: PodRef, containerName: ContainerName, destPath: string) {
394397
return (
395398
(await this.execContainer(podRef, containerName, [
396399
'bash',
@@ -400,7 +403,7 @@ export class K8Client implements K8 {
400403
);
401404
}
402405

403-
public mkdir(podRef: PodRef, containerName: string, destPath: string) {
406+
public mkdir(podRef: PodRef, containerName: ContainerName, destPath: string) {
404407
return this.execContainer(podRef, containerName, ['bash', '-c', 'mkdir -p "' + destPath + '"']);
405408
}
406409

@@ -481,7 +484,7 @@ export class K8Client implements K8 {
481484

482485
public async copyTo(
483486
podRef: PodRef,
484-
containerName: string,
487+
containerName: ContainerName,
485488
srcPath: string,
486489
destDir: string,
487490
filter: TarCreateFilter | undefined = undefined,
@@ -528,7 +531,7 @@ export class K8Client implements K8 {
528531
.exec(
529532
namespace.name,
530533
podRef.podName.name,
531-
containerName,
534+
containerName.name,
532535
command,
533536
null,
534537
errPassthroughStream,
@@ -569,7 +572,7 @@ export class K8Client implements K8 {
569572
}
570573
}
571574

572-
public async copyFrom(podRef: PodRef, containerName: string, srcPath: string, destDir: string) {
575+
public async copyFrom(podRef: PodRef, containerName: ContainerName, srcPath: string, destDir: string) {
573576
const self = this;
574577
const namespace = podRef.namespaceName;
575578
const guid = uuid4();
@@ -636,7 +639,7 @@ export class K8Client implements K8 {
636639
.exec(
637640
namespace.name,
638641
podRef.podName.name,
639-
containerName,
642+
containerName.name,
640643
command,
641644
outputFileStream,
642645
errPassthroughStream,
@@ -702,7 +705,7 @@ export class K8Client implements K8 {
702705
}
703706
}
704707

705-
public async execContainer(podRef: PodRef, containerName: string, command: string | string[]) {
708+
public async execContainer(podRef: PodRef, containerName: ContainerName, command: string | string[]) {
706709
const self = this;
707710
const namespace = podRef.namespaceName;
708711
const guid = uuid4();
@@ -742,7 +745,7 @@ export class K8Client implements K8 {
742745
.exec(
743746
namespace.name,
744747
podRef.podName.name,
745-
containerName,
748+
containerName.name,
746749
command,
747750
outputFileStream,
748751
errPassthroughStream,

src/core/kube/kube_errors.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,18 @@ export class MissingPodNameError extends SoloError {
6464
super(MissingPodNameError.MISSING_POD_NAME, cause, meta);
6565
}
6666
}
67+
68+
export class ContainerNameInvalidError extends SoloError {
69+
public static CONTAINER_NAME_INVALID = (name: string) => RFC_1123_POSTFIX(`Container name '${name}'`);
70+
71+
/**
72+
* Instantiates a new error with a message and an optional cause.
73+
*
74+
* @param containerName - the invalid container name.
75+
* @param cause - optional underlying cause of the error.
76+
* @param meta - optional metadata to be reported.
77+
*/
78+
public constructor(containerName: string, cause: Error | any = {}, meta: any = {}) {
79+
super(ContainerNameInvalidError.CONTAINER_NAME_INVALID(containerName), cause, meta);
80+
}
81+
}

src/core/kube/pod.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import {type ExtendedNetServer} from '../../types/index.js';
55
import {type TDirectoryData} from './t_directory_data.js';
66
import {type TarCreateFilter} from '../../types/aliases.js';
7+
import {type ContainerName} from './container_name.js';
78

89
export interface Pod {
910
/**
@@ -14,7 +15,7 @@ export interface Pod {
1415
* @param srcPath - the path to the file to copy
1516
* @param destDir - the destination directory
1617
*/
17-
copyFrom(containerName: string, srcPath: string, destDir: string): Promise<unknown>;
18+
copyFrom(containerName: ContainerName, srcPath: string, destDir: string): Promise<unknown>;
1819

1920
/**
2021
* Copy a file into a container
@@ -27,7 +28,7 @@ export interface Pod {
2728
* @returns a Promise that performs the copy operation
2829
*/
2930
copyTo(
30-
containerName: string,
31+
containerName: ContainerName,
3132
srcPath: string,
3233
destDir: string,
3334
filter: TarCreateFilter | undefined,
@@ -39,22 +40,22 @@ export interface Pod {
3940
* @param command - sh commands as an array to be run within the containerName (e.g 'ls -la /opt/hgcapp')
4041
* @returns console output as string
4142
*/
42-
execContainer(containerName: string, command: string | string[]): Promise<string>;
43+
execContainer(containerName: ContainerName, command: string | string[]): Promise<string>;
4344

4445
/**
4546
* Check if a directory exists in the specified container
4647
* @param containerName - the name of the container
4748
* @param destPath - the path to the directory inside the container
4849
*/
49-
hasDir(containerName: string, destPath: string): Promise<boolean>;
50+
hasDir(containerName: ContainerName, destPath: string): Promise<boolean>;
5051

5152
/**
5253
* Check if a file exists in the specified container
5354
* @param containerName - the name of the container
5455
* @param destPath - the remote path to the file
5556
* @param [filters] - optional filters to apply to the tar stream
5657
*/
57-
hasFile(containerName: string, destPath: string, filters: object): Promise<boolean>;
58+
hasFile(containerName: ContainerName, destPath: string, filters: object): Promise<boolean>;
5859

5960
/**
6061
* Get a pod by name and namespace, will check every 1 second until the pod is no longer found.
@@ -79,14 +80,14 @@ export interface Pod {
7980
* @param destPath - the remote path to the directory
8081
* @returns a promise that returns array of directory entries, custom object
8182
*/
82-
listDir(containerName: string, destPath: string): Promise<any[] | TDirectoryData[]>;
83+
listDir(containerName: ContainerName, destPath: string): Promise<any[] | TDirectoryData[]>;
8384

8485
/**
8586
* Make a directory in the specified container
8687
* @param containerName - the name of the container
8788
* @param destPath - the remote path to the directory
8889
*/
89-
mkdir(containerName: string, destPath: string): Promise<string>;
90+
mkdir(containerName: ContainerName, destPath: string): Promise<string>;
9091

9192
/**
9293
* Port forward a port from a pod to localhost

0 commit comments

Comments
 (0)