Skip to content
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

feat: introduce ClusterApiObject type #2869

Open
wants to merge 1 commit into
base: 2.x
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion src/api-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export class ApiObject extends Construct {
}
}

function parseApiGroup(apiVersion: string) {
export function parseApiGroup(apiVersion: string) {
const v = apiVersion.split('/');

// no group means "core"
Expand Down
155 changes: 155 additions & 0 deletions src/cluster-api-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Construct, IConstruct } from 'constructs';
import { sanitizeValue } from './_util';
import { Chart } from './chart';
import { JsonPatch } from './json-patch';
import { ClusterApiObjectMetadata, ClusterApiObjectMetadataDefinition } from './cluster-metadata';
import { resolve } from './resolve';
import { ApiObject, parseApiGroup } from './api-object';

/**
* Options for defining API objects.
*/
export interface ClusterApiObjectProps {
/**
* Object metadata.
*
* If `name` is not specified, an app-unique name will be allocated by the
* framework based on the path of the construct within thes construct tree.
*/
readonly metadata?: ClusterApiObjectMetadata;

/**
* API version.
*/
readonly apiVersion: string;

/**
* Resource kind.
*/
readonly kind: string;

/**
* Additional attributes for this API object.
* @jsii ignore
* @see https://github.com/cdk8s-team/cdk8s-core/issues/1297
*/
readonly [key: string]: any;
}

const CLUSTER_API_OBJECT_SYMBOL = Symbol.for('cdk8s.ClusterApiObject');

export class ClusterApiObject extends ApiObject {

/**
* Return whether the given object is an `ClusterApiObject`.
*
* We do attribute detection since we can't reliably use 'instanceof'.

* @param o The object to check
*/
static isClusterApiObject(o: any): o is ClusterApiObject {
return o !== null && typeof o === 'object' && CLUSTER_API_OBJECT_SYMBOL in o;
}

/**
* Implements `instanceof ClusterApiObject` using the more reliable `ClusterApiObject.isClusterApiObject` static method
*
* @param o The object to check
* @internal
*/
static [Symbol.hasInstance](o: unknown) {
return ClusterApiObject.isClusterApiObject(o);
}
/**
* Returns the `ApiObject` named `Resource` which is a child of the given
* construct. If `c` is an `ApiObject`, it is returned directly. Throws an
* exception if the construct does not have a child named `Default` _or_ if
* this child is not an `ApiObject`.
*
* @param c The higher-level construct
*/
public static of(c: IConstruct): ClusterApiObject {
if (c instanceof ClusterApiObject) {
return c;
}

const child = c.node.defaultChild;
if (!child) {
throw new Error(`cannot find a (direct or indirect) child of type ClusterApiObject for construct ${c.node.path}`);
}

return ClusterApiObject.of(child);
}

/**
* Metadata associated with this API object.
*/
public readonly metadata: ClusterApiObjectMetadataDefinition;

/**
* Defines an API object.
*
* @param scope the construct scope
* @param id namespace
* @param props options
*/
constructor(scope: Construct, id: string, private readonly props: ClusterApiObjectProps) {
super(scope, id, props);
this.patches = new Array<JsonPatch>();
this.chart = Chart.of(this);
this.kind = props.kind;
this.apiVersion = props.apiVersion;
this.apiGroup = parseApiGroup(this.apiVersion);

this.name = props.metadata?.name ?? this.chart.generateObjectName(this);

this.metadata = new ClusterApiObjectMetadataDefinition({
name: this.name,

// user defined values
...props.metadata,

labels: {
...this.chart.labels,
...props.metadata?.labels,
},
clusterApiObject: this,
});

Object.defineProperty(this, CLUSTER_API_OBJECT_SYMBOL, { value: true });
}

/**
* Renders the object to Kubernetes JSON.
*
* To disable sorting of dictionary keys in output object set the
* `CDK8S_DISABLE_SORT` environment variable to any non-empty value.
*/
public toJson(): any {

try {
const data: any = {
...this.props,
metadata: this.metadata.toJson(),
};

const sortKeys = process.env.CDK8S_DISABLE_SORT ? false : true;
const json = sanitizeValue(resolve([], data, this), { sortKeys });
const patched = JsonPatch.apply(json, ...this.patches);

// reorder top-level keys so that we first have "apiVersion", "kind" and
// "metadata" and then all the rest
const result: any = {};
const orderedKeys = ['apiVersion', 'kind', 'metadata', ...Object.keys(patched)];
for (const k of orderedKeys) {
if (k in patched) {
result[k] = patched[k];
}
}

return result;
} catch (e) {
throw new Error(`Failed serializing construct at path '${this.node.path}' with name '${this.name}': ${e}`);
}
}
}
62 changes: 62 additions & 0 deletions src/cluster-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { sanitizeValue } from './_util';
import { ClusterApiObject } from './cluster-api-object';
import { ApiObjectMetadata, ApiObjectMetadataDefinition } from './metadata';
import { resolve } from './resolve';

/**
* Metadata associated with this object.
*/
export interface ClusterApiObjectMetadata extends Omit<ApiObjectMetadata, 'namespace'> {}

/**
* Options for `ApiObjectMetadataDefinition`.
*/
export interface ClusterApiObjectMetadataDefinitionOptions extends ClusterApiObjectMetadata {

/**
* Which ApiObject instance is the metadata attached to.
*/
readonly clusterApiObject: ClusterApiObject;

}

/**
* Object metadata.
*/
export class ClusterApiObjectMetadataDefinition extends ApiObjectMetadataDefinition {

/**
* The ApiObject this metadata is attached to.
*/
private readonly clusterApiObject: ClusterApiObject;

constructor(options: ClusterApiObjectMetadataDefinitionOptions) {
super(options);
this.name = options.name;
this.labels = { ...options.labels } ?? { };
this.annotations = { ...options.annotations } ?? { };
this.finalizers = options.finalizers ? [...options.finalizers] : [];
this.ownerReferences = options.ownerReferences ? [...options.ownerReferences] : [];
this.clusterApiObject = options.clusterApiObject;
this._additionalAttributes = options;

// otherwise apiObject is passed to the resolving logic, which expectadly fails
delete this._additionalAttributes.apiObject;

}

/**
* Synthesizes a k8s ObjectMeta for this metadata set.
*/
public toJson() {
const sanitize = (x: any) => sanitizeValue(x, { filterEmptyArrays: true, filterEmptyObjects: true });
return sanitize(resolve([], {
...this._additionalAttributes,
name: this.name,
annotations: this.annotations,
finalizers: this.finalizers,
ownerReferences: this.ownerReferences,
labels: this.labels,
}, this.clusterApiObject));
}
}
Loading