Skip to content

feat: add configmaps fluent interface implementation #1295

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion src/core/config/remote/remote_config_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {ErrorMessages} from '../../error_messages.js';
import {CommonFlagsDataWrapper} from './common_flags_data_wrapper.js';
import {type AnyObject} from '../../../types/aliases.js';
import {NamespaceName} from '../../kube/namespace_name.js';
import {ResourceNotFoundError} from '../../kube/errors/resource_operation_errors.js';

/**
* Uses Kubernetes ConfigMaps to manage the remote configuration data by creating, loading, modifying,
Expand Down Expand Up @@ -261,7 +262,7 @@ export class RemoteConfigManager {
try {
return await this.k8.getNamespacedConfigMap(constants.SOLO_REMOTE_CONFIGMAP_NAME);
} catch (error: any) {
if (error.meta?.statusCode !== StatusCodes.NOT_FOUND) {
if (!(error instanceof ResourceNotFoundError)) {
throw new SoloError('Failed to read remote config from cluster', error);
}

Expand Down
34 changes: 31 additions & 3 deletions src/core/kube/config_maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import {type NamespaceName} from './namespace_name.js';

export interface ConfigMaps {
/**
* Create a new config map
* Create a new config map. If the config map already exists, it will not be replaced.
*
* @param namespace - for the config map
* @param name - for the config name
* @param labels - for the config metadata
* @param data - to contain in the config
* @throws {ResourceCreateError} if the config map could not be created.
* @throws {KubeApiError} if the API call fails for an unexpected reason.
*/
create(
namespace: NamespaceName,
Expand All @@ -20,11 +23,22 @@ export interface ConfigMaps {
): Promise<boolean>; // TODO was createNamespacedConfigMap

/**
* Delete a config map
* Create or replace a config map. If the config map already exists, it will be replaced.
*
* @param namespace - for the config map
* @param name - for the config name
* @param labels - for the config metadata
* @param data - to contain in the config
* @throws {ResourceCreateError} if the config map could not be created.
* @throws {ResourceReplaceError} if the config map could not be replaced.
* @throws {KubeApiError} if the API call fails for an unexpected reason.
*/
delete(namespace: NamespaceName, name: string): Promise<boolean>; // TODO was deleteNamespacedConfigMap
createOrReplace(
namespace: NamespaceName,
name: string,
labels: Record<string, string>,
data: Record<string, string>,
): Promise<boolean>;

/**
* Read a config map
Expand All @@ -46,4 +60,18 @@ export interface ConfigMaps {
labels: Record<string, string>,
data: Record<string, string>,
): Promise<boolean>; // TODO was replaceNamespacedConfigMap

/**
* Delete a config map
* @param namespace - for the config map
* @param name - for the config name
*/
delete(namespace: NamespaceName, name: string): Promise<boolean>; // TODO was deleteNamespacedConfigMap

/**
* Check if a config map exists
* @param namespace - for the config map
* @param name - for the config name
*/
exists(namespace: NamespaceName, name: string): Promise<boolean>;
}
2 changes: 1 addition & 1 deletion src/core/kube/container_name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {isDns1123Label} from './kube_validation.js';
import {ContainerNameInvalidError} from './kube_errors.js';
import {ContainerNameInvalidError} from './errors/namespace_name_invalid_error.js';

/**
* Represents a Kubernetes container name. A Kubernetes container name must be a valid RFC-1123 DNS label.
Expand Down
18 changes: 18 additions & 0 deletions src/core/kube/errors/kube_api_error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {SoloError} from '../../errors.js';

export class KubeApiError extends SoloError {
/**
* Instantiates a new error with a message and an optional cause.
*
* @param message - the error message.
* @param statusCode - the HTTP status code.
* @param cause - optional underlying cause of the error.
* @param meta - optional metadata to be reported.
*/
public constructor(message: string, statusCode: number, cause?: Error, meta?: object) {
super(message + `, statusCode: ${statusCode}`, cause, {...meta, ...{statusCode: statusCode}});
}

Check warning on line 17 in src/core/kube/errors/kube_api_error.ts

View check run for this annotation

Codecov / codecov/patch

src/core/kube/errors/kube_api_error.ts#L16-L17

Added lines #L16 - L17 were not covered by tests
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {SoloError} from '../errors.js';
import {SoloError} from '../../errors.js';

const RFC_1123_POSTFIX = (prefix: string) => `${prefix} is invalid, must be a valid RFC-1123 DNS label. \` +
"A DNS 1123 label must consist of lower case alphanumeric characters, '-' " +
Expand Down
117 changes: 117 additions & 0 deletions src/core/kube/errors/resource_operation_errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {SoloError} from '../../errors.js';
import {ResourceOperation} from '../resource_operation.js';
import {type ResourceType} from '../resource_type.js';
import {type NamespaceName} from '../namespace_name.js';

export class ResourceOperationError extends SoloError {
/**
* Instantiates a new error with a message and an optional cause.
* @param operation - the operation that failed.
* @param resourceType - the type of resource that failed to read.
* @param namespace - the namespace of the resource.
* @param name - the name of the resource.
* @param cause - optional underlying cause of the error.
*/
public constructor(
operation: ResourceOperation,
resourceType: ResourceType,
namespace: NamespaceName,
name: string,
cause?: Error,
) {
super(`failed to ${operation} ${resourceType} '${name}' in namespace '${namespace}'`, cause, {
operation: operation,
resourceType: resourceType,
namespace: namespace?.name,
name: name,
});
}
}

export class ResourceReadError extends ResourceOperationError {
/**
* Instantiates a new error with a message and an optional cause.
* @param resourceType - the type of resource that failed to read.
* @param namespace - the namespace of the resource.
* @param name - the name of the resource.
* @param cause - optional underlying cause of the error.
*/
public constructor(resourceType: ResourceType, namespace: NamespaceName, name: string, cause?: Error) {
super(ResourceOperation.READ, resourceType, namespace, name, cause);
}
}

export class ResourceCreateError extends ResourceOperationError {
/**
* Instantiates a new error with a message and an optional cause.
* @param resourceType - the type of resource that failed to create.
* @param namespace - the namespace of the resource.
* @param name - the name of the resource.
* @param cause - optional underlying cause of the error.
*/
public constructor(resourceType: ResourceType, namespace: NamespaceName, name: string, cause?: Error) {
super(ResourceOperation.CREATE, resourceType, namespace, name, cause);
}
}

export class ResourceUpdateError extends ResourceOperationError {
/**
* Instantiates a new error with a message and an optional cause.
* @param resourceType - the type of resource that failed to update.
* @param namespace - the namespace of the resource.
* @param name - the name of the resource.
* @param cause - optional underlying cause of the error.
*/
public constructor(resourceType: ResourceType, namespace: NamespaceName, name: string, cause?: Error) {
super(ResourceOperation.UPDATE, resourceType, namespace, name, cause);
}
}

export class ResourceDeleteError extends ResourceOperationError {
/**
* Instantiates a new error with a message and an optional cause.
* @param resourceType - the type of resource that failed to delete.
* @param namespace - the namespace of the resource.
* @param name - the name of the resource.
* @param cause - optional underlying cause of the error.
*/
public constructor(resourceType: ResourceType, namespace: NamespaceName, name: string, cause?: Error) {
super(ResourceOperation.DELETE, resourceType, namespace, name, cause);
}
}

export class ResourceReplaceError extends ResourceOperationError {
/**
* Instantiates a new error with a message and an optional cause.
* @param resourceType - the type of resource that failed to replace.
* @param namespace - the namespace of the resource.
* @param name - the name of the resource.
* @param cause - optional underlying cause of the error.
*/
public constructor(resourceType: ResourceType, namespace: NamespaceName, name: string, cause?: Error) {
super(ResourceOperation.REPLACE, resourceType, namespace, name, cause);
}
}

export class ResourceNotFoundError extends ResourceOperationError {
/**
* Instantiates a new error with a message and an optional cause.
* @param operation - the operation that failed.
* @param resourceType - the type of resource that failed to read.
* @param namespace - the namespace of the resource.
* @param name - the name of the resource.
* @param cause - optional underlying cause of the error.
*/
public constructor(
operation: ResourceOperation,
resourceType: ResourceType,
namespace: NamespaceName,
name: string,
cause?: Error,
) {
super(operation, resourceType, namespace, name, cause);
}
}
9 changes: 8 additions & 1 deletion src/core/kube/k8.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {type V1Lease} from '@kubernetes/client-node';
import {type Namespaces} from './namespaces.js';
import {type NamespaceName} from './namespace_name.js';
import {type Clusters} from './clusters.js';
import {type ConfigMaps} from './config_maps.js';
import {type ContainerName} from './container_name.js';

export interface K8 {
Expand All @@ -21,10 +22,16 @@ export interface K8 {

/**
* Fluent accessor for reading and manipulating cluster information from the kubeconfig file.
* returns an object instance providing cluster operations
* @returns an object instance providing cluster operations
*/
clusters(): Clusters;

/**
* Fluent accessor for reading and manipulating config maps.
* @returns an object instance providing config map operations
*/
configMaps(): ConfigMaps;

/**
* Create a new namespace
* @param namespace - the namespace to create
Expand Down
75 changes: 17 additions & 58 deletions src/core/kube/k8_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import {NamespaceName} from './namespace_name.js';
import K8ClientClusters from './k8_client/k8_client_clusters.js';
import {type Clusters} from './clusters.js';
import {type ConfigMaps} from './config_maps.js';
import K8ClientConfigMaps from './k8_client/k8_client_config_maps.js';
import {PodRef} from './pod_ref.js';
import {type ContainerName} from './container_name.js';

Expand All @@ -57,6 +59,7 @@
private networkingApi: k8s.NetworkingV1Api;

private k8Clusters: K8ClientClusters;
private k8ConfigMaps: K8ClientConfigMaps;

constructor(
@inject(ConfigManager) private readonly configManager?: ConfigManager,
Expand Down Expand Up @@ -86,6 +89,7 @@
this.coordinationApiClient = this.kubeConfig.makeApiClient(k8s.CoordinationV1Api);

this.k8Clusters = new K8ClientClusters(this.kubeConfig);
this.k8ConfigMaps = new K8ClientConfigMaps(this.kubeClient);

return this; // to enable chaining
}
Expand All @@ -97,12 +101,20 @@

/**
* Fluent accessor for reading and manipulating cluster information from the kubeconfig file.
* returns an object instance providing cluster operations
* @returns an object instance providing cluster operations
*/
public clusters(): Clusters {
return this.k8Clusters;
}

/**
* Fluent accessor for reading and manipulating config maps in the kubernetes cluster.
* @returns an object instance providing config map operations
*/
public configMaps(): ConfigMaps {
return this.k8ConfigMaps;
}

/**
* Apply filters to metadata
* @param items - list of items
Expand Down Expand Up @@ -1148,80 +1160,27 @@
/* ------------- ConfigMap ------------- */

public async getNamespacedConfigMap(name: string): Promise<k8s.V1ConfigMap> {
const {response, body} = await this.kubeClient
.readNamespacedConfigMap(name, this.getNamespace().name)
.catch(e => e);

this.handleKubernetesClientError(response, body, 'Failed to get namespaced configmap');

return body as k8s.V1ConfigMap;
return this.configMaps().read(this.getNamespace(), name);
}

public async createNamespacedConfigMap(
name: string,
labels: Record<string, string>,
data: Record<string, string>,
): Promise<boolean> {
const namespace = this.getNamespace();

const configMap = new k8s.V1ConfigMap();
configMap.data = data;

const metadata = new k8s.V1ObjectMeta();
metadata.name = name;
metadata.namespace = namespace.name;
metadata.labels = labels;
configMap.metadata = metadata;
try {
const resp = await this.kubeClient.createNamespacedConfigMap(namespace.name, configMap);

return resp.response.statusCode === StatusCodes.CREATED;
} catch (e) {
throw new SoloError(
`failed to create configmap ${name} in namespace ${namespace}: ${e.message}, ${e?.body?.message}`,
e,
);
}
return this.configMaps().create(this.getNamespace(), name, labels, data);
}

public async replaceNamespacedConfigMap(
name: string,
labels: Record<string, string>,
data: Record<string, string>,
): Promise<boolean> {
const namespace = this.getNamespace();

const configMap = new k8s.V1ConfigMap();
configMap.data = data;

const metadata = new k8s.V1ObjectMeta();
metadata.name = name;
metadata.namespace = namespace.name;
metadata.labels = labels;
configMap.metadata = metadata;
try {
const resp = await this.kubeClient.replaceNamespacedConfigMap(name, namespace.name, configMap);

return resp.response.statusCode === StatusCodes.CREATED;
} catch (e) {
throw new SoloError(
`failed to replace configmap ${name} in namespace ${namespace}: ${e.message}, ${e?.body?.message}`,
e,
);
}
return this.configMaps().replace(this.getNamespace(), name, labels, data);
}

public async deleteNamespacedConfigMap(name: string, namespace: NamespaceName): Promise<boolean> {
try {
const resp = await this.kubeClient.deleteNamespacedConfigMap(name, namespace.name);

return resp.response.statusCode === StatusCodes.CREATED;
} catch (e) {
throw new SoloError(
`failed to delete configmap ${name} in namespace ${namespace}: ${e.message}, ${e?.body?.message}`,
e,
);
}
return this.configMaps().delete(namespace, name);

Check warning on line 1183 in src/core/kube/k8_client.ts

View check run for this annotation

Codecov / codecov/patch

src/core/kube/k8_client.ts#L1183

Added line #L1183 was not covered by tests
}

// --------------------------------------- LEASES --------------------------------------- //
Expand Down
Loading
Loading