This commit is contained in:
Morten Olsen
2025-07-28 22:27:40 +02:00
parent b35782a7d8
commit 48f1bde404
25 changed files with 3341 additions and 235 deletions

View File

@@ -1,14 +1,15 @@
import { type Static, type TSchema } from "@sinclair/typebox";
import { GROUP } from "../utils/consts.ts";
import type { Services } from "../utils/service.ts";
import { statusSchema } from "./custom-resource.status.ts";
import type { CustomResourceRequest } from "./custom-resource.request.ts";
import { type TSchema } from '@sinclair/typebox';
import { GROUP } from '../utils/consts.ts';
import type { Services } from '../utils/service.ts';
import { statusSchema } from './custom-resource.status.ts';
import type { CustomResourceRequest } from './custom-resource.request.ts';
type CustomResourceHandlerOptions<TSpec extends TSchema> = {
request: CustomResourceRequest<TSpec>;
services: Services;
}
};
type CustomResourceConstructor<TSpec extends TSchema> = {
kind: string;
@@ -17,25 +18,21 @@ type CustomResourceConstructor<TSpec extends TSchema> = {
plural: string;
singular: string;
};
}
};
abstract class CustomResource<
TSpec extends TSchema
> {
abstract class CustomResource<TSpec extends TSchema> {
#options: CustomResourceConstructor<TSpec>;
constructor(options: CustomResourceConstructor<TSpec>) {
this.#options = options;
}
public readonly version = 'v1';
public get name() {
return `${this.#options.names.plural}.${this.group}`;
}
public get version() {
return 'v1';
}
public get group() {
return GROUP;
}
@@ -75,27 +72,28 @@ abstract class CustomResource<
singular: this.#options.names.singular,
},
scope: 'Namespaced',
versions: [{
name: this.version,
served: true,
storage: true,
schema: {
openAPIV3Schema: {
type: 'object',
properties: {
spec: this.spec,
status: statusSchema as any,
}
}
versions: [
{
name: this.version,
served: true,
storage: true,
schema: {
openAPIV3Schema: {
type: 'object',
properties: {
spec: this.spec,
status: statusSchema,
},
},
},
subresources: {
status: {},
},
},
subresources: {
status: {}
}
}]
}
}
}
],
},
};
};
}
export { CustomResource, type CustomResourceConstructor, type CustomResourceHandlerOptions };
export { CustomResource, type CustomResourceConstructor, type CustomResourceHandlerOptions };

View File

@@ -1,13 +1,15 @@
import { ApiException, Watch } from "@kubernetes/client-node";
import { K8sService } from "../services/k8s.ts";
import type { Services } from "../utils/service.ts";
import { type CustomResource } from "./custom-resource.base.ts";
import { CustomResourceRequest } from "./custom-resource.request.ts";
import { ApiException, Watch } from '@kubernetes/client-node';
import { K8sService } from '../services/k8s.ts';
import type { Services } from '../utils/service.ts';
import { type CustomResource } from './custom-resource.base.ts';
import { CustomResourceRequest } from './custom-resource.request.ts';
class CustomResourceRegistry {
#services: Services;
#resources: Set<CustomResource<any>> = new Set();
#watchers: Map<string, AbortController> = new Map();
#resources = new Set<CustomResource<any>>();
#watchers = new Map<string, AbortController>();
constructor(services: Services) {
this.#services = services;
@@ -19,11 +21,11 @@ class CustomResourceRegistry {
public getByKind = (kind: string) => {
return Array.from(this.#resources).find((r) => r.kind === kind);
}
};
public register = (resource: CustomResource<any>) => {
this.#resources.add(resource);
}
};
public unregister = (resource: CustomResource<any>) => {
this.#resources.delete(resource);
@@ -33,7 +35,7 @@ class CustomResourceRegistry {
this.#watchers.delete(kind);
}
});
}
};
public watch = async () => {
const k8sService = this.#services.get(K8sService);
@@ -46,7 +48,7 @@ class CustomResourceRegistry {
const controller = await watcher.watch(path, {}, this.#onResourceEvent, this.#onError);
this.#watchers.set(resource.kind, controller);
}
}
};
#onResourceEvent = async (type: string, obj: any) => {
console.log(type, this.kinds);
@@ -79,12 +81,12 @@ class CustomResourceRegistry {
await handler?.({
request,
services: this.#services,
})
}
});
};
#onError = (error: any) => {
console.error(error);
}
};
public install = async (replace = false) => {
const k8sService = this.#services.get(K8sService);
@@ -107,7 +109,7 @@ class CustomResourceRegistry {
throw error;
}
}
}
};
}
export { CustomResourceRegistry };
export { CustomResourceRegistry };

View File

@@ -1,9 +1,11 @@
import type { Static, TSchema } from "@sinclair/typebox";
import type { Services } from "../utils/service.ts";
import { K8sService } from "../services/k8s.ts";
import { CustomResourceRegistry } from "./custom-resource.registry.ts";
import { CustomResourceStatus, type CustomResourceStatusType } from "./custom-resource.status.ts";
import { ApiException, PatchStrategy, setHeaderOptions } from "@kubernetes/client-node";
import type { Static, TSchema } from '@sinclair/typebox';
import { ApiException, PatchStrategy, setHeaderOptions } from '@kubernetes/client-node';
import type { Services } from '../utils/service.ts';
import { K8sService } from '../services/k8s.ts';
import { CustomResourceRegistry } from './custom-resource.registry.ts';
import { CustomResourceStatus, type CustomResourceStatusType } from './custom-resource.status.ts';
type CustomResourceRequestOptions = {
type: 'ADDED' | 'DELETED' | 'MODIFIED';
@@ -22,7 +24,7 @@ type CustomResourceRequestMetadata = Record<string, string> & {
generation: number;
};
class CustomResourceRequest<TSpec extends TSchema>{
class CustomResourceRequest<TSpec extends TSchema> {
#options: CustomResourceRequestOptions;
constructor(options: CustomResourceRequestOptions) {
@@ -59,18 +61,19 @@ class CustomResourceRequest<TSpec extends TSchema>{
public isOwnerOf = (manifest: any) => {
const ownerRef = manifest?.metadata?.ownerReferences || [];
return ownerRef.some((ref: any) =>
ref.apiVersion === this.apiVersion &&
ref.kind === this.kind &&
ref.name === this.metadata.name &&
ref.uid === this.metadata.uid
return ownerRef.some(
(ref: any) =>
ref.apiVersion === this.apiVersion &&
ref.kind === this.kind &&
ref.name === this.metadata.name &&
ref.uid === this.metadata.uid,
);
}
};
public setStatus = async (status: CustomResourceStatusType) => {
const { manifest, services } = this.#options;
const { kind, metadata } = manifest;
const registry = services.get(CustomResourceRegistry);
const registry = services.get(CustomResourceRegistry);
const crd = registry.getByKind(kind);
if (!crd) {
throw new Error(`Custom resource ${kind} not found`);
@@ -80,17 +83,20 @@ class CustomResourceRequest<TSpec extends TSchema>{
const { namespace = 'default', name } = metadata;
const response = await k8sService.customObjectsApi.patchNamespacedCustomObjectStatus({
group: crd.group,
version: crd.version,
namespace,
plural: crd.names.plural,
name,
body: { status },
fieldValidation: 'Strict',
}, setHeaderOptions('Content-Type', PatchStrategy.MergePatch))
const response = await k8sService.customObjectsApi.patchNamespacedCustomObjectStatus(
{
group: crd.group,
version: crd.version,
namespace,
plural: crd.names.plural,
name,
body: { status },
fieldValidation: 'Strict',
},
setHeaderOptions('Content-Type', PatchStrategy.MergePatch),
);
return response;
}
};
public getCurrent = async () => {
const { manifest, services } = this.#options;
@@ -101,19 +107,19 @@ class CustomResourceRequest<TSpec extends TSchema>{
throw new Error(`Custom resource ${manifest.kind} not found`);
}
try {
const resource = await k8sService.customObjectsApi.getNamespacedCustomObject({
group: crd.group,
version: crd.version,
plural: crd.names.plural,
namespace: manifest.metadata.namespace,
name: manifest.metadata.name,
});
return resource as {
apiVersion: string;
kind: string;
metadata: CustomResourceRequestMetadata;
spec: Static<TSpec>;
status: CustomResourceStatusType;
const resource = await k8sService.customObjectsApi.getNamespacedCustomObject({
group: crd.group,
version: crd.version,
plural: crd.names.plural,
namespace: manifest.metadata.namespace,
name: manifest.metadata.name,
});
return resource as {
apiVersion: string;
kind: string;
metadata: CustomResourceRequestMetadata;
spec: Static<TSpec>;
status: CustomResourceStatusType;
};
} catch (error) {
if (error instanceof ApiException && error.code === 404) {
@@ -121,10 +127,10 @@ class CustomResourceRequest<TSpec extends TSchema>{
}
throw error;
}
}
};
public getStatus = async () => {
const resource = await this.getCurrent()
const resource = await this.getCurrent();
if (!resource || !resource.status) {
return new CustomResourceStatus({
status: {
@@ -140,8 +146,7 @@ class CustomResourceRequest<TSpec extends TSchema>{
generation: resource.metadata.generation,
save: this.setStatus,
});
}
};
}
export { CustomResourceRequest };
export { CustomResourceRequest };

View File

@@ -1,25 +1,27 @@
import { Type, type Static } from "@sinclair/typebox";
import { Type, type Static } from '@sinclair/typebox';
type CustomResourceStatusType= Static<typeof statusSchema>;
type CustomResourceStatusType = Static<typeof statusSchema>;
const statusSchema = Type.Object({
observedGeneration: Type.Number(),
conditions: Type.Array(Type.Object({
type: Type.String(),
status: Type.String({
enum: ['True', 'False', 'Unknown']
conditions: Type.Array(
Type.Object({
type: Type.String(),
status: Type.String({
enum: ['True', 'False', 'Unknown'],
}),
lastTransitionTime: Type.String(),
reason: Type.String(),
message: Type.String(),
}),
lastTransitionTime: Type.String(),
reason: Type.String(),
message: Type.String(),
})),
),
});
type CustomResourceStatusOptions = {
status?: CustomResourceStatusType;
generation: number;
save: (status: CustomResourceStatusType) => Promise<void>;
}
};
class CustomResourceStatus {
#status: CustomResourceStatusType;
@@ -49,9 +51,12 @@ class CustomResourceStatus {
public getCondition = (type: string) => {
return this.#status.conditions?.find((condition) => condition.type === type)?.status;
}
};
public setCondition = (type: string, condition: Omit<CustomResourceStatusType['conditions'][number], 'type' | 'lastTransitionTime'>) => {
public setCondition = (
type: string,
condition: Omit<CustomResourceStatusType['conditions'][number], 'type' | 'lastTransitionTime'>,
) => {
const currentCondition = this.getCondition(type);
const newCondition = {
...condition,
@@ -59,22 +64,22 @@ class CustomResourceStatus {
lastTransitionTime: new Date().toISOString(),
};
if (currentCondition) {
this.#status.conditions = this.#status.conditions.map((c) => c.type === type ? newCondition : c);
this.#status.conditions = this.#status.conditions.map((c) => (c.type === type ? newCondition : c));
} else {
this.#status.conditions.push(newCondition);
}
}
};
public save = async () => {
await this.#save({
...this.#status,
observedGeneration: this.#generation,
});
}
};
public toJSON = () => {
return this.#status;
}
};
}
export { CustomResourceStatus, statusSchema, type CustomResourceStatusType };
export { CustomResourceStatus, statusSchema, type CustomResourceStatusType };