Skip to content

Commit 938a956

Browse files
authored
feat: add contexts fluent interface implementation (#1297)
Signed-off-by: Nathan Klick <[email protected]>
1 parent 87e660d commit 938a956

File tree

8 files changed

+103
-48
lines changed

8 files changed

+103
-48
lines changed

src/core/kube/contexts.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* SPDX-License-Identifier: Apache-2.0
3+
*/
4+
import {type NamespaceName} from './namespace_name.js';
5+
16
/**
27
* SPDX-License-Identifier: Apache-2.0
38
*/
@@ -6,25 +11,25 @@ export interface Contexts {
611
* List all contexts in the kubeconfig
712
* @returns a list of context names
813
*/
9-
list(): Promise<string[]>; // TODO was getContextNames
14+
list(): string[]; // TODO was getContextNames
1015

1116
/**
1217
* Read the current context in the kubeconfig
1318
* @returns the current context name
1419
*/
15-
readCurrent(): Promise<string>; // TODO was getCurrentContext
20+
readCurrent(): string; // TODO was getCurrentContext
1621

1722
/**
1823
* Read the current namespace in the kubeconfig
1924
* @returns the current namespace name
2025
*/
21-
readCurrentNamespace(): Promise<string>; // TODO was getCurrentContextNamespace
26+
readCurrentNamespace(): NamespaceName; // TODO was getCurrentContextNamespace
2227

2328
/**
2429
* Set the current context in the kubeconfig
2530
* @param context - the context name to set
2631
*/
27-
updateCurrent(context: string): Promise<void>; // TODO delete this once we are instantiating multiple K8 instances, was setCurrentContext
32+
updateCurrent(context: string): void; // TODO delete this once we are instantiating multiple K8 instances, was setCurrentContext
2833

2934
/**
3035
* Test the connection to a context

src/core/kube/k8.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {type Containers} from './containers.js';
1313
import {type Clusters} from './clusters.js';
1414
import {type ConfigMaps} from './config_maps.js';
1515
import {type ContainerRef} from './container_ref.js';
16+
import {type Contexts} from './contexts.js';
1617

1718
export interface K8 {
1819
/**
@@ -39,6 +40,12 @@ export interface K8 {
3940
*/
4041
configMaps(): ConfigMaps;
4142

43+
/**
44+
* Fluent accessor for reading and manipulating contexts in the kubeconfig file.
45+
* @returns an object instance providing context operations
46+
*/
47+
contexts(): Contexts;
48+
4249
/**
4350
* Create a new namespace
4451
* @param namespace - the namespace to create

src/core/kube/k8_client.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import net from 'net';
88
import path from 'path';
99
import {Flags as flags} from '../../commands/flags.js';
1010
import {MissingArgumentError, SoloError} from './../errors.js';
11-
import type * as http from 'node:http';
1211
import {getReasonPhrase, StatusCodes} from 'http-status-codes';
1312
import {sleep} from './../helpers.js';
1413
import * as constants from './../constants.js';
@@ -32,6 +31,9 @@ import {PodRef} from './pod_ref.js';
3231
import {ContainerRef} from './container_ref.js';
3332
import {K8ClientContainers} from './k8_client/k8_client_containers.js';
3433
import {type Containers} from './containers.js';
34+
import {type Contexts} from './contexts.js';
35+
import type http from 'node:http';
36+
import K8ClientContexts from './k8_client/k8_client_contexts.js';
3537

3638
/**
3739
* A kubernetes API wrapper class providing custom functionalities required by solo
@@ -42,8 +44,6 @@ import {type Containers} from './containers.js';
4244
// TODO rename to K8Client and move to kube folder
4345
@injectable()
4446
export class K8Client implements K8 {
45-
private cachedContexts: Context[];
46-
4747
static PodReadyCondition = new Map<string, string>().set(
4848
constants.POD_CONDITION_READY,
4949
constants.POD_CONDITION_STATUS_TRUE,
@@ -56,8 +56,8 @@ export class K8Client implements K8 {
5656

5757
private k8Clusters: K8ClientClusters;
5858
private k8ConfigMaps: K8ClientConfigMaps;
59-
6059
private k8Containers: K8ClientContainers;
60+
private k8Contexts: K8ClientContexts;
6161

6262
constructor(
6363
@inject(ConfigManager) private readonly configManager?: ConfigManager,
@@ -89,6 +89,7 @@ export class K8Client implements K8 {
8989
this.k8Clusters = new K8ClientClusters(this.kubeConfig);
9090
this.k8ConfigMaps = new K8ClientConfigMaps(this.kubeClient);
9191
this.k8Containers = new K8ClientContainers(this.kubeConfig);
92+
this.k8Contexts = new K8ClientContexts(this.kubeConfig);
9293

9394
return this; // to enable chaining
9495
}
@@ -122,6 +123,14 @@ export class K8Client implements K8 {
122123
return this.k8Containers;
123124
}
124125

126+
/**
127+
* Fluent accessor for reading and manipulating contexts in the kubeconfig file.
128+
* @returns an object instance providing context operations
129+
*/
130+
public contexts(): Contexts {
131+
return this.k8Contexts;
132+
}
133+
125134
/**
126135
* Apply filters to metadata
127136
* @param items - list of items
@@ -288,21 +297,7 @@ export class K8Client implements K8 {
288297
}
289298

290299
public getContextNames(): string[] {
291-
const contexts: string[] = [];
292-
293-
for (const context of this.getContexts()) {
294-
contexts.push(context.name);
295-
}
296-
297-
return contexts;
298-
}
299-
300-
private getContexts(): Context[] {
301-
if (!this.cachedContexts) {
302-
this.cachedContexts = this.kubeConfig.getContexts();
303-
}
304-
305-
return this.cachedContexts;
300+
return this.contexts().list();
306301
}
307302

308303
public async listDir(containerRef: ContainerRef, destPath: string) {
@@ -1096,11 +1091,11 @@ export class K8Client implements K8 {
10961091
}
10971092

10981093
public getCurrentContext(): string {
1099-
return this.kubeConfig.getCurrentContext();
1094+
return this.contexts().readCurrent();
11001095
}
11011096

11021097
public getCurrentContextNamespace(): NamespaceName {
1103-
return NamespaceName.of(this.kubeConfig.getContextObject(this.getCurrentContext())?.namespace);
1098+
return this.contexts().readCurrentNamespace();
11041099
}
11051100

11061101
public getCurrentClusterName(): string {

src/core/kube/k8_client/k8_client_clusters.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default class K8ClientClusters implements Clusters {
1212
}
1313
}
1414

15-
list(): string[] {
15+
public list(): string[] {
1616
const clusters: string[] = [];
1717
for (const cluster of this.kubeConfig.getClusters()) {
1818
clusters.push(cluster.name);
@@ -21,7 +21,7 @@ export default class K8ClientClusters implements Clusters {
2121
return clusters;
2222
}
2323

24-
readCurrent(): string {
24+
public readCurrent(): string {
2525
const currentCluster = this.kubeConfig.getCurrentCluster();
2626
return !currentCluster ? '' : currentCluster.name;
2727
}

src/core/kube/k8_client/k8_client_config_maps.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
/**
22
* SPDX-License-Identifier: Apache-2.0
33
*/
4-
import * as k8s from '@kubernetes/client-node';
5-
import {type V1ConfigMap} from '@kubernetes/client-node';
4+
import {type CoreV1Api, V1ConfigMap, V1ObjectMeta} from '@kubernetes/client-node';
65
import {type ConfigMaps} from '../config_maps.js';
76
import {type NamespaceName} from '../namespace_name.js';
87
import {
@@ -16,9 +15,9 @@ import {ResourceOperation} from '../resource_operation.js';
1615
import {KubeApiResponse} from '../kube_api_response.js';
1716

1817
export default class K8ClientConfigMaps implements ConfigMaps {
19-
constructor(private readonly kubeClient: k8s.CoreV1Api) {}
18+
public constructor(private readonly kubeClient: CoreV1Api) {}
2019

21-
async create(
20+
public async create(
2221
namespace: NamespaceName,
2322
name: string,
2423
labels: Record<string, string>,
@@ -27,7 +26,7 @@ export default class K8ClientConfigMaps implements ConfigMaps {
2726
return this.createOrReplaceWithForce(namespace, name, labels, data, false, true);
2827
}
2928

30-
async createOrReplace(
29+
public async createOrReplace(
3130
namespace: NamespaceName,
3231
name: string,
3332
labels: Record<string, string>,
@@ -36,7 +35,7 @@ export default class K8ClientConfigMaps implements ConfigMaps {
3635
return this.createOrReplaceWithForce(namespace, name, labels, data, false, false);
3736
}
3837

39-
async delete(namespace: NamespaceName, name: string): Promise<boolean> {
38+
public async delete(namespace: NamespaceName, name: string): Promise<boolean> {
4039
try {
4140
const resp = await this.kubeClient.deleteNamespacedConfigMap(name, namespace.name);
4241
return KubeApiResponse.isFailingStatus(resp.response);
@@ -45,13 +44,13 @@ export default class K8ClientConfigMaps implements ConfigMaps {
4544
}
4645
}
4746

48-
async read(namespace: NamespaceName, name: string): Promise<V1ConfigMap> {
47+
public async read(namespace: NamespaceName, name: string): Promise<V1ConfigMap> {
4948
const {response, body} = await this.kubeClient.readNamespacedConfigMap(name, namespace.name).catch(e => e);
5049
KubeApiResponse.check(response, ResourceOperation.READ, ResourceType.CONFIG_MAP, namespace, name);
51-
return body as k8s.V1ConfigMap;
50+
return body as V1ConfigMap;
5251
}
5352

54-
async replace(
53+
public async replace(
5554
namespace: NamespaceName,
5655
name: string,
5756
labels: Record<string, string>,
@@ -60,7 +59,7 @@ export default class K8ClientConfigMaps implements ConfigMaps {
6059
return this.createOrReplaceWithForce(namespace, name, labels, data, true, false);
6160
}
6261

63-
async exists(namespace: NamespaceName, name: string): Promise<boolean> {
62+
public async exists(namespace: NamespaceName, name: string): Promise<boolean> {
6463
try {
6564
const cm = await this.read(namespace, name);
6665
return !!cm;
@@ -82,10 +81,10 @@ export default class K8ClientConfigMaps implements ConfigMaps {
8281
forceCreate?: boolean,
8382
): Promise<boolean> {
8483
const replace = await this.shouldReplace(namespace, name, forceReplace, forceCreate);
85-
const configMap = new k8s.V1ConfigMap();
84+
const configMap = new V1ConfigMap();
8685
configMap.data = data;
8786

88-
const metadata = new k8s.V1ObjectMeta();
87+
const metadata = new V1ObjectMeta();
8988
metadata.name = name;
9089
metadata.namespace = namespace.name;
9190
metadata.labels = labels;

src/core/kube/k8_client/k8_client_container.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class K8ClientContainer implements Container {
3131
this.k8 = container.resolve('K8') as K8;
3232
}
3333

34-
async copyFrom(srcPath: string, destDir: string): Promise<unknown> {
34+
public async copyFrom(srcPath: string, destDir: string): Promise<unknown> {
3535
const self = this;
3636
const namespace = this.containerRef.podRef.namespaceName;
3737
const guid = uuid4();
@@ -165,7 +165,7 @@ export class K8ClientContainer implements Container {
165165
}
166166
}
167167

168-
async copyTo(srcPath: string, destDir: string, filter: TarCreateFilter | undefined): Promise<boolean> {
168+
public async copyTo(srcPath: string, destDir: string, filter: TarCreateFilter | undefined): Promise<boolean> {
169169
const self = this;
170170
const namespace = this.containerRef.podRef.namespaceName;
171171
const guid = uuid4();
@@ -250,7 +250,7 @@ export class K8ClientContainer implements Container {
250250
}
251251
}
252252

253-
async execContainer(command: string | string[]): Promise<string> {
253+
public async execContainer(command: string | string[]): Promise<string> {
254254
const self = this;
255255
const namespace = this.containerRef.podRef.namespaceName;
256256
const guid = uuid4();
@@ -330,14 +330,14 @@ export class K8ClientContainer implements Container {
330330
});
331331
}
332332

333-
async hasDir(destPath: string): Promise<boolean> {
333+
public async hasDir(destPath: string): Promise<boolean> {
334334
return (
335335
(await this.execContainer(['bash', '-c', '[[ -d "' + destPath + '" ]] && echo -n "true" || echo -n "false"'])) ===
336336
'true'
337337
);
338338
}
339339

340-
async hasFile(destPath: string, filters: object): Promise<boolean> {
340+
public async hasFile(destPath: string, filters: object): Promise<boolean> {
341341
const parentDir = path.dirname(destPath);
342342
const fileName = path.basename(destPath);
343343
const filterMap = new Map(Object.entries(filters));
@@ -385,7 +385,7 @@ export class K8ClientContainer implements Container {
385385
return false;
386386
}
387387

388-
async listDir(destPath: string): Promise<any[] | TDirectoryData[]> {
388+
public async listDir(destPath: string): Promise<any[] | TDirectoryData[]> {
389389
// TODO future, return the following
390390
// return this.pods.byName(podName).listDir(containerName, destPath);
391391
// byName(podName) can use an underlying cache to avoid multiple calls to the API
@@ -447,7 +447,7 @@ export class K8ClientContainer implements Container {
447447
}
448448
}
449449

450-
async mkdir(destPath: string): Promise<string> {
450+
public async mkdir(destPath: string): Promise<string> {
451451
return this.execContainer(['bash', '-c', 'mkdir -p "' + destPath + '"']);
452452
}
453453

src/core/kube/k8_client/k8_client_containers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import {type KubeConfig} from '@kubernetes/client-node';
1111
* SPDX-License-Identifier: Apache-2.0
1212
*/
1313
export class K8ClientContainers implements Containers {
14-
constructor(private readonly kubeConfig: KubeConfig) {}
14+
public constructor(private readonly kubeConfig: KubeConfig) {}
1515

16-
readByRef(containerRef: ContainerRef): Container {
16+
public readByRef(containerRef: ContainerRef): Container {
1717
return new K8ClientContainer(this.kubeConfig, containerRef);
1818
}
1919
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* SPDX-License-Identifier: Apache-2.0
3+
*/
4+
import {type Contexts} from '../contexts.js';
5+
import {type KubeConfig, CoreV1Api} from '@kubernetes/client-node';
6+
import {NamespaceName} from '../namespace_name.js';
7+
8+
export default class K8ClientContexts implements Contexts {
9+
public constructor(private readonly kubeConfig: KubeConfig) {}
10+
11+
public list(): string[] {
12+
const contexts: string[] = [];
13+
14+
for (const context of this.kubeConfig.getContexts()) {
15+
contexts.push(context.name);
16+
}
17+
18+
return contexts;
19+
}
20+
21+
public readCurrent(): string {
22+
return this.kubeConfig.getCurrentContext();
23+
}
24+
25+
public readCurrentNamespace(): NamespaceName {
26+
return NamespaceName.of(this.kubeConfig.getContextObject(this.readCurrent())?.namespace);
27+
}
28+
29+
public updateCurrent(context: string): void {
30+
this.kubeConfig.setCurrentContext(context);
31+
}
32+
33+
public async testContextConnection(context: string): Promise<boolean> {
34+
const originalCtxName = this.readCurrent();
35+
this.kubeConfig.setCurrentContext(context);
36+
37+
const tempKubeClient = this.kubeConfig.makeApiClient(CoreV1Api);
38+
return await tempKubeClient
39+
.listNamespace()
40+
.then(() => {
41+
this.kubeConfig.setCurrentContext(originalCtxName);
42+
return true;
43+
})
44+
.catch(() => {
45+
this.kubeConfig.setCurrentContext(originalCtxName);
46+
return false;
47+
});
48+
}
49+
}

0 commit comments

Comments
 (0)