Skip to content

Commit

Permalink
feat: add service and services fluent interface implementation (#1303)
Browse files Browse the repository at this point in the history
Signed-off-by: Nathan Klick <[email protected]>
Co-authored-by: Jeromy Cannon <[email protected]>
  • Loading branch information
nathanklick and jeromy-cannon authored Feb 6, 2025
1 parent 013caa5 commit 8ef6998
Show file tree
Hide file tree
Showing 18 changed files with 216 additions and 57 deletions.
3 changes: 0 additions & 3 deletions src/core/kube/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
*/
import {type NamespaceName} from './namespace_name.js';

/**
* SPDX-License-Identifier: Apache-2.0
*/
export interface Contexts {
/**
* List all contexts in the kubeconfig
Expand Down
15 changes: 13 additions & 2 deletions src/core/kube/k8.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {type Clusters} from './clusters.js';
import {type ConfigMaps} from './config_maps.js';
import {type ContainerRef} from './container_ref.js';
import {type Contexts} from './contexts.js';
import {type Services} from './services.js';
import {type Service} from './service.js';
import {type Pods} from './pods.js';

export interface K8 {
/**
Expand Down Expand Up @@ -46,6 +49,14 @@ export interface K8 {
*/
contexts(): Contexts;

/**
* Fluent accessor for reading and manipulating services.
* @returns an object instance providing service operations
*/
services(): Services;

pods(): Pods;

/**
* Create a new namespace
* @param namespace - the namespace to create
Expand Down Expand Up @@ -90,9 +101,9 @@ export interface K8 {
* Get a svc by name
* @param name - svc name
*/
getSvcByName(name: string): Promise<k8s.V1Service>;
getSvcByName(name: string): Promise<Service>;

listSvcs(namespace: NamespaceName, labels: string[]): Promise<k8s.V1Service[]>;
listSvcs(namespace: NamespaceName, labels: string[]): Promise<Service[]>;

/**
* Get a list of clusters
Expand Down
64 changes: 21 additions & 43 deletions src/core/kube/k8_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ import type http from 'node:http';
import {K8ClientContexts} from './k8_client/k8_client_contexts.js';
import {K8ClientPods} from './k8_client/k8_client_pods.js';
import {type Pods} from './pods.js';
import {K8ClientFilter} from './k8_client/k8_client_filter.js';
import {K8ClientBase} from './k8_client/k8_client_base.js';
import {type Services} from './services.js';
import {K8ClientServices} from './k8_client/k8_client_services.js';
import {type Service} from './service.js';

/**
* A kubernetes API wrapper class providing custom functionalities required by solo
Expand All @@ -45,7 +48,7 @@ import {K8ClientFilter} from './k8_client/k8_client_filter.js';
*/
// TODO move to kube folder
@injectable()
export class K8Client extends K8ClientFilter implements K8 {
export class K8Client extends K8ClientBase implements K8 {
// TODO - remove extends K8ClientFilter after services refactor, it is using filterItem()

private kubeConfig!: k8s.KubeConfig;
Expand All @@ -58,6 +61,7 @@ export class K8Client extends K8ClientFilter implements K8 {
private k8Containers: Containers;
private k8Pods: Pods;
private k8Contexts: Contexts;
private k8Services: Services;

constructor(
@inject(ConfigManager) private readonly configManager?: ConfigManager,
Expand Down Expand Up @@ -91,6 +95,7 @@ export class K8Client extends K8ClientFilter implements K8 {
this.k8ConfigMaps = new K8ClientConfigMaps(this.kubeClient);
this.k8Containers = new K8ClientContainers(this.kubeConfig);
this.k8Contexts = new K8ClientContexts(this.kubeConfig);
this.k8Services = new K8ClientServices(this.kubeClient);
this.k8Pods = new K8ClientPods(this.kubeClient, this.kubeConfig);

return this; // to enable chaining
Expand Down Expand Up @@ -136,6 +141,14 @@ export class K8Client extends K8ClientFilter implements K8 {
return this.k8Contexts;
}

/**
* Fluent accessor for reading and manipulating services in the kubernetes cluster.
* @returns an object instance providing service operations
*/
public services(): Services {
return this.k8Services;
}

/**
* Fluent accessor for reading and manipulating pods in the kubernetes cluster.
* @returns an object instance providing pod operations
Expand Down Expand Up @@ -207,24 +220,8 @@ export class K8Client extends K8ClientFilter implements K8 {
return result.body.items;
}

public async getSvcByName(name: string): Promise<k8s.V1Service> {
const ns = this.getNamespace();
const fieldSelector = `metadata.name=${name}`;
const resp = await this.kubeClient.listNamespacedService(
ns.name,
undefined,
undefined,
undefined,
fieldSelector,
undefined,
undefined,
undefined,
undefined,
undefined,
Duration.ofMinutes(5).toMillis(),
);

return this.filterItem(resp.body.items, {name});
public async getSvcByName(name: string): Promise<Service> {
return this.services().read(this.getNamespace(), name);
}

public getClusters(): string[] {
Expand Down Expand Up @@ -352,13 +349,7 @@ export class K8Client extends K8ClientFilter implements K8 {

// TODO this can be removed once K8 is context/cluster specific when instantiating
public async testContextConnection(context: string): Promise<boolean> {
this.kubeConfig.setCurrentContext(context);

const tempKubeClient = this.kubeConfig.makeApiClient(k8s.CoreV1Api);
return await tempKubeClient
.listNamespace()
.then(() => true)
.catch(() => false);
return this.contexts().testContextConnection(context);
}

// --------------------------------------- Secret --------------------------------------- //
Expand Down Expand Up @@ -789,11 +780,7 @@ export class K8Client extends K8ClientFilter implements K8 {

// TODO make private once we are instantiating multiple K8 instances
public setCurrentContext(context: string) {
this.kubeConfig.setCurrentContext(context);

// Reinitialize clients
this.kubeClient = this.kubeConfig.makeApiClient(k8s.CoreV1Api);
this.coordinationApiClient = this.kubeConfig.makeApiClient(k8s.CoordinationV1Api);
return this.contexts().updateCurrent(context);
}

public getCurrentContext(): string {
Expand All @@ -808,16 +795,7 @@ export class K8Client extends K8ClientFilter implements K8 {
return this.clusters().readCurrent();
}

public async listSvcs(namespace: NamespaceName, labels: string[]): Promise<k8s.V1Service[]> {
const labelSelector = labels.join(',');
const serviceList = await this.kubeClient.listNamespacedService(
namespace.name,
undefined,
undefined,
undefined,
undefined,
labelSelector,
);
return serviceList.body.items;
public async listSvcs(namespace: NamespaceName, labels: string[]): Promise<Service[]> {
return this.services().list(namespace, labels);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {MissingArgumentError, SoloError} from '../../errors.js';
import {IllegalArgumentError, MissingArgumentError, SoloError} from '../../errors.js';
import {type V1ObjectMeta} from '@kubernetes/client-node';
import {type ObjectMeta} from '../object_meta.js';
import {K8ClientObjectMeta} from './k8_client_object_meta.js';
import {NamespaceName} from '../namespace_name.js';

/**
* The abstract K8 Client Filter adds the `filterItem` method to the class that extends it.
*/
export abstract class K8ClientFilter {
export abstract class K8ClientBase {
/**
* Apply filters to metadata
* @param items - list of items
Expand Down Expand Up @@ -52,4 +56,24 @@ export abstract class K8ClientFilter {
if (filtered.length > 1) throw new SoloError('multiple items found with filters', {filters});
return filtered[0];
}

/**
* Wraps the V1ObjectMeta object instance into a ObjectMeta instance.
*
* @param v1meta - the V1ObjectMeta object from the K8S API client.
* @protected
*/
protected wrapObjectMeta(v1meta: V1ObjectMeta): ObjectMeta {
if (!v1meta) {
throw new IllegalArgumentError('metadata is required');
}

return new K8ClientObjectMeta(
NamespaceName.of(v1meta!.name),
v1meta?.name,
v1meta?.labels,
v1meta?.annotations,
v1meta?.uid,
);
}
}
3 changes: 0 additions & 3 deletions src/core/kube/k8_client/k8_client_containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import {type Container} from '../container.js';
import {K8ClientContainer} from './k8_client_container.js';
import {type KubeConfig} from '@kubernetes/client-node';

/**
* SPDX-License-Identifier: Apache-2.0
*/
export class K8ClientContainers implements Containers {
public constructor(private readonly kubeConfig: KubeConfig) {}

Expand Down
15 changes: 15 additions & 0 deletions src/core/kube/k8_client/k8_client_object_meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {type ObjectMeta} from '../object_meta.js';
import {type NamespaceName} from '../namespace_name.js';

export class K8ClientObjectMeta implements ObjectMeta {
constructor(
public readonly namespace: NamespaceName,
public readonly name: string,
public readonly labels?: {[key: string]: string},
public readonly annotations?: {[key: string]: string},
public readonly uid?: string,
) {}
}
4 changes: 2 additions & 2 deletions src/core/kube/k8_client/k8_client_pods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {type PodRef} from '../pod_ref.js';
import {type Pod} from '../pod.js';
import {K8ClientPod} from './k8_client_pod.js';
import {Duration} from '../../time/duration.js';
import {K8ClientFilter} from './k8_client_filter.js';
import {K8ClientBase} from './k8_client_base.js';
import {MissingArgumentError, SoloError} from '../../errors.js';
import * as constants from '../../constants.js';
import {SoloLogger} from '../../logging.js';
import {container} from 'tsyringe-neo';

export class K8ClientPods extends K8ClientFilter implements Pods {
export class K8ClientPods extends K8ClientBase implements Pods {
private readonly logger: SoloLogger;

constructor(
Expand Down
15 changes: 15 additions & 0 deletions src/core/kube/k8_client/k8_client_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {type Service} from '../service.js';
import {type ObjectMeta} from '../object_meta.js';
import {type ServiceSpec} from '../service_spec.js';
import {type ServiceStatus} from '../service_status.js';

export class K8ClientService implements Service {
public constructor(
public readonly metadata: ObjectMeta,
public readonly spec: ServiceSpec,
public readonly status?: ServiceStatus,
) {}
}
56 changes: 56 additions & 0 deletions src/core/kube/k8_client/k8_client_services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {type Services} from '../services.js';
import {type NamespaceName} from '../namespace_name.js';
import {type CoreV1Api, type V1Service} from '@kubernetes/client-node';
import {K8ClientBase} from './k8_client_base.js';
import {type Service} from '../service.js';
import {KubeApiResponse} from '../kube_api_response.js';
import {ResourceOperation} from '../resource_operation.js';
import {ResourceType} from '../resource_type.js';
import {K8ClientService} from './k8_client_service.js';
import {type ServiceSpec} from '../service_spec.js';
import {type ServiceStatus} from '../service_status.js';

export class K8ClientServices extends K8ClientBase implements Services {
public constructor(private readonly kubeClient: CoreV1Api) {
super();
}

public async list(namespace: NamespaceName, labels?: string[]): Promise<Service[]> {
const labelSelector = labels ? labels.join(',') : undefined;
const serviceList = await this.kubeClient.listNamespacedService(
namespace.name,
undefined,
undefined,
undefined,
undefined,
labelSelector,
);
KubeApiResponse.check(serviceList.response, ResourceOperation.LIST, ResourceType.SERVICE, namespace, '');
return serviceList.body.items.map((svc: V1Service) => {
return this.wrapService(namespace, svc);
});
}

public async read(namespace: NamespaceName, name: string): Promise<Service> {
const svc = await this.readV1Service(namespace, name);

if (!svc) {
return null;
}

return this.wrapService(namespace, svc);
}

private async readV1Service(namespace: NamespaceName, name: string): Promise<V1Service> {
const {response, body} = await this.kubeClient.readNamespacedService(name, namespace.name);
KubeApiResponse.check(response, ResourceOperation.READ, ResourceType.SERVICE, namespace, name);
return body as V1Service;
}

private wrapService(namespace: NamespaceName, svc: V1Service): Service {
return new K8ClientService(this.wrapObjectMeta(svc.metadata), svc.spec as ServiceSpec, svc.status as ServiceStatus);
}
}
7 changes: 7 additions & 0 deletions src/core/kube/load_balancer_ingress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
export interface LoadBalancerIngress {
readonly hostname?: string;
readonly ip?: string;
}
8 changes: 8 additions & 0 deletions src/core/kube/load_balancer_status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {type LoadBalancerIngress} from './load_balancer_ingress.js';

export interface LoadBalancerStatus {
readonly ingress?: LoadBalancerIngress[];
}
12 changes: 12 additions & 0 deletions src/core/kube/object_meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {type NamespaceName} from './namespace_name.js';

export interface ObjectMeta {
readonly namespace?: NamespaceName;
readonly name: string;
readonly labels?: {[key: string]: string};
readonly annotations?: {[key: string]: string};
readonly uid?: string;
}
1 change: 1 addition & 0 deletions src/core/kube/resource_operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export enum ResourceOperation {
UPDATE = 'update',
DELETE = 'delete',
REPLACE = 'replace',
LIST = 'list',
}
12 changes: 12 additions & 0 deletions src/core/kube/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {type ObjectMeta} from './object_meta.js';
import {type ServiceSpec} from './service_spec.js';
import {type ServiceStatus} from './service_status.js';

export interface Service {
readonly metadata?: ObjectMeta;
readonly spec?: ServiceSpec;
readonly status?: ServiceStatus;
}
7 changes: 7 additions & 0 deletions src/core/kube/service_port.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
export interface ServicePort {
readonly name?: string;
readonly port: number;
}
10 changes: 10 additions & 0 deletions src/core/kube/service_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {type ServicePort} from './service_port.js';

export interface ServiceSpec {
readonly clusterIP?: string;
readonly ports?: ServicePort[];
readonly selector?: {[key: string]: string};
}
Loading

0 comments on commit 8ef6998

Please sign in to comment.