refact: cleanup
This commit is contained in:
15
src/services/k8s/k8s.config.ts
Normal file
15
src/services/k8s/k8s.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { KubeConfig } from '@kubernetes/client-node';
|
||||
|
||||
class K8sConfig {
|
||||
#config?: KubeConfig;
|
||||
|
||||
public get config() {
|
||||
if (!this.#config) {
|
||||
this.#config = new KubeConfig();
|
||||
this.#config.loadFromDefault();
|
||||
}
|
||||
return this.#config;
|
||||
}
|
||||
}
|
||||
|
||||
export { K8sConfig };
|
||||
82
src/services/k8s/k8s.crd.ts
Normal file
82
src/services/k8s/k8s.crd.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { ApiException, ApiextensionsV1Api } from '@kubernetes/client-node';
|
||||
import { z, type ZodType } from 'zod';
|
||||
|
||||
import { K8sConfig } from './k8s.config.ts';
|
||||
|
||||
import type { Services } from '#root/utils/services.ts';
|
||||
|
||||
type CreateCrdOptions = {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
plural?: string;
|
||||
scope: 'Cluster' | 'Namespaced';
|
||||
spec: ZodType;
|
||||
};
|
||||
class K8sCrds {
|
||||
#services: Services;
|
||||
|
||||
constructor(services: Services) {
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
install = async (definition: CreateCrdOptions) => {
|
||||
const { config } = this.#services.get(K8sConfig);
|
||||
const plural = definition.plural ?? definition.kind.toLowerCase() + 's';
|
||||
const [version, group] = definition.apiVersion.split('/').toReversed();
|
||||
const manifest = {
|
||||
apiVersion: 'apiextensions.k8s.io/v1',
|
||||
kind: 'CustomResourceDefinition',
|
||||
metadata: {
|
||||
name: `${plural}.${group}`,
|
||||
},
|
||||
spec: {
|
||||
group: group,
|
||||
names: {
|
||||
kind: definition.kind,
|
||||
plural: plural,
|
||||
singular: definition.kind.toLowerCase(),
|
||||
},
|
||||
scope: definition.scope,
|
||||
versions: [
|
||||
{
|
||||
name: version,
|
||||
served: true,
|
||||
storage: true,
|
||||
schema: {
|
||||
openAPIV3Schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
spec: {
|
||||
...z.toJSONSchema(definition.spec, { io: 'input' }),
|
||||
$schema: undefined,
|
||||
additionalProperties: undefined,
|
||||
} as ExplicitAny,
|
||||
},
|
||||
},
|
||||
},
|
||||
subresources: {
|
||||
status: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const extensionsApi = config.makeApiClient(ApiextensionsV1Api);
|
||||
try {
|
||||
await extensionsApi.createCustomResourceDefinition({
|
||||
body: manifest,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ApiException && error.code === 409) {
|
||||
await extensionsApi.patchCustomResourceDefinition({
|
||||
name: manifest.metadata.name,
|
||||
body: [{ op: 'replace', path: '/spec', value: manifest.spec }],
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { K8sCrds };
|
||||
62
src/services/k8s/k8s.resources.ts
Normal file
62
src/services/k8s/k8s.resources.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { V1Secret, type KubernetesObject } from '@kubernetes/client-node';
|
||||
|
||||
import { K8sWatcher } from './k8s.watcher.ts';
|
||||
import type { K8sBackboneClient, K8sBackboneTopic } from './k8s.schemas.ts';
|
||||
import { K8sConfig } from './k8s.config.ts';
|
||||
|
||||
import type { Services } from '#root/utils/services.ts';
|
||||
|
||||
class K8sResources {
|
||||
#services: Services;
|
||||
#secrets?: K8sWatcher<V1Secret>;
|
||||
#clients?: K8sWatcher<KubernetesObject & { spec: K8sBackboneClient }>;
|
||||
#topics?: K8sWatcher<KubernetesObject & { spec: K8sBackboneTopic }>;
|
||||
|
||||
constructor(services: Services) {
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
public get secrets() {
|
||||
if (!this.#secrets) {
|
||||
const { config } = this.#services.get(K8sConfig);
|
||||
this.#secrets = new K8sWatcher({
|
||||
config,
|
||||
apiVersion: 'v1',
|
||||
kind: 'Secret',
|
||||
});
|
||||
}
|
||||
return this.#secrets;
|
||||
}
|
||||
|
||||
public get clients() {
|
||||
if (!this.#clients) {
|
||||
const { config } = this.#services.get(K8sConfig);
|
||||
this.#clients = new K8sWatcher({
|
||||
config,
|
||||
apiVersion: 'backbone.mortenolsen.pro/v1',
|
||||
kind: 'Client',
|
||||
});
|
||||
}
|
||||
return this.#clients;
|
||||
}
|
||||
|
||||
public get topics() {
|
||||
if (!this.#topics) {
|
||||
const { config } = this.#services.get(K8sConfig);
|
||||
this.#topics = new K8sWatcher({
|
||||
config,
|
||||
apiVersion: 'backbone.mortenolsen.pro/v1',
|
||||
kind: 'Topic',
|
||||
});
|
||||
}
|
||||
return this.#topics;
|
||||
}
|
||||
|
||||
public start = async () => {
|
||||
await this.secrets.start();
|
||||
await this.clients.start();
|
||||
await this.topics.start();
|
||||
};
|
||||
}
|
||||
|
||||
export { K8sResources };
|
||||
19
src/services/k8s/k8s.schemas.ts
Normal file
19
src/services/k8s/k8s.schemas.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { statementSchema } from '#root/auth/auth.schemas.ts';
|
||||
|
||||
const k8sBackboneClientSchema = z.object({
|
||||
statements: z.array(statementSchema),
|
||||
});
|
||||
|
||||
type K8sBackboneClient = z.infer<typeof k8sBackboneClientSchema>;
|
||||
|
||||
const k8sBackboneTopicSchema = z.object({
|
||||
matches: z.array(z.string()),
|
||||
schema: z.record(z.string(), z.object()),
|
||||
});
|
||||
|
||||
type K8sBackboneTopic = z.infer<typeof k8sBackboneTopicSchema>;
|
||||
|
||||
export type { K8sBackboneClient, K8sBackboneTopic };
|
||||
export { k8sBackboneClientSchema, k8sBackboneTopicSchema };
|
||||
37
src/services/k8s/k8s.ts
Normal file
37
src/services/k8s/k8s.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { K8sResources } from './k8s.resources.ts';
|
||||
import { K8sCrds } from './k8s.crd.ts';
|
||||
import { k8sBackboneClientSchema, k8sBackboneTopicSchema } from './k8s.schemas.ts';
|
||||
|
||||
import { API_VERSION } from '#root/utils/consts.ts';
|
||||
import type { Services } from '#root/utils/services.ts';
|
||||
|
||||
class K8sService {
|
||||
#services: Services;
|
||||
|
||||
constructor(services: Services) {
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
public get resources() {
|
||||
return this.#services.get(K8sResources);
|
||||
}
|
||||
|
||||
public setup = async () => {
|
||||
const crds = this.#services.get(K8sCrds);
|
||||
await crds.install({
|
||||
apiVersion: API_VERSION,
|
||||
kind: 'Client',
|
||||
scope: 'Namespaced',
|
||||
spec: k8sBackboneClientSchema,
|
||||
});
|
||||
await crds.install({
|
||||
apiVersion: API_VERSION,
|
||||
kind: 'Topic',
|
||||
scope: 'Namespaced',
|
||||
spec: k8sBackboneTopicSchema,
|
||||
});
|
||||
await this.resources.start();
|
||||
};
|
||||
}
|
||||
|
||||
export { K8sService };
|
||||
87
src/services/k8s/k8s.watcher.ts
Normal file
87
src/services/k8s/k8s.watcher.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
KubeConfig,
|
||||
KubernetesObjectApi,
|
||||
makeInformer,
|
||||
type Informer,
|
||||
type KubernetesObject,
|
||||
} from '@kubernetes/client-node';
|
||||
|
||||
import { EventEmitter } from '#root/utils/event-emitter.ts';
|
||||
|
||||
type K8sWatcherOptions = {
|
||||
config: KubeConfig;
|
||||
apiVersion: string;
|
||||
plural?: string;
|
||||
kind: string;
|
||||
selector?: string;
|
||||
};
|
||||
|
||||
type K8sWatcherEvents<TType extends KubernetesObject> = {
|
||||
updated: (manifest: TType) => void;
|
||||
removed: (manifest: TType) => void;
|
||||
};
|
||||
|
||||
class K8sWatcher<TType extends KubernetesObject> extends EventEmitter<K8sWatcherEvents<TType>> {
|
||||
#options: K8sWatcherOptions;
|
||||
#informer: Informer<TType>;
|
||||
#manifests: Map<string, TType>;
|
||||
|
||||
constructor(options: K8sWatcherOptions) {
|
||||
super();
|
||||
this.#options = options;
|
||||
this.#manifests = new Map();
|
||||
this.#informer = this.#setupInformer();
|
||||
}
|
||||
|
||||
public get manifests() {
|
||||
return Array.from(this.#manifests.values());
|
||||
}
|
||||
|
||||
#setupInformer = () => {
|
||||
const { config, apiVersion, kind, plural, selector } = this.#options;
|
||||
const objectApi = config.makeApiClient(KubernetesObjectApi);
|
||||
const derivedPlural = plural ?? kind.toLowerCase() + 's';
|
||||
const [version, group] = apiVersion.split('/').toReversed();
|
||||
const path = group ? `/apis/${group}/${version}/${derivedPlural}` : `/api/${version}/${derivedPlural}`;
|
||||
|
||||
const informer = makeInformer<TType>(
|
||||
config,
|
||||
path,
|
||||
async () => {
|
||||
return objectApi.list(apiVersion, kind);
|
||||
},
|
||||
selector,
|
||||
);
|
||||
informer.on('add', this.#handleResource.bind(this, 'add'));
|
||||
informer.on('update', this.#handleResource.bind(this, 'update'));
|
||||
informer.on('delete', this.#handleResource.bind(this, 'delete'));
|
||||
informer.on('error', (err) => {
|
||||
console.log('Watcher failed, will retry in 3 seconds', path, err);
|
||||
setTimeout(this.start, 3000);
|
||||
});
|
||||
return informer;
|
||||
};
|
||||
|
||||
#handleResource = (action: string, manifest: TType) => {
|
||||
const uid = manifest.metadata?.uid;
|
||||
if (!uid) {
|
||||
return;
|
||||
}
|
||||
if (action === 'delete') {
|
||||
this.#manifests.delete(uid);
|
||||
return this.emit('removed', manifest);
|
||||
}
|
||||
this.#manifests.set(uid, manifest);
|
||||
this.emit('updated', manifest);
|
||||
};
|
||||
|
||||
public start = () => {
|
||||
return this.#informer.start();
|
||||
};
|
||||
|
||||
public stop = () => {
|
||||
return this.#informer.stop();
|
||||
};
|
||||
}
|
||||
|
||||
export { K8sWatcher };
|
||||
23
src/services/sessions/sessions.provider.ts
Normal file
23
src/services/sessions/sessions.provider.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { AuthProvider } from '#root/auth/auth.provider.ts';
|
||||
|
||||
class SessionProvider {
|
||||
#handlers: Map<string, AuthProvider>;
|
||||
|
||||
constructor() {
|
||||
this.#handlers = new Map();
|
||||
}
|
||||
|
||||
public register = (name: string, provider: AuthProvider) => {
|
||||
this.#handlers.set(name, provider);
|
||||
};
|
||||
|
||||
public validate = (provider: string, token: string) => {
|
||||
const handler = this.#handlers.get(provider);
|
||||
if (!handler) {
|
||||
throw new Error('Provider not available');
|
||||
}
|
||||
return handler.getAccess(token);
|
||||
};
|
||||
}
|
||||
|
||||
export { SessionProvider };
|
||||
34
src/services/sessions/sessions.session.ts
Normal file
34
src/services/sessions/sessions.session.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { validate } from './sessions.utils.ts';
|
||||
|
||||
import type { Statement } from '#root/auth/auth.schemas.ts';
|
||||
|
||||
type SessionOptions = {
|
||||
statements: Statement[];
|
||||
};
|
||||
|
||||
type ValidateOptions = {
|
||||
action: string;
|
||||
resource: string;
|
||||
};
|
||||
|
||||
class Session {
|
||||
#options: SessionOptions;
|
||||
|
||||
constructor(options: SessionOptions) {
|
||||
this.#options = options;
|
||||
}
|
||||
|
||||
public get statements() {
|
||||
return this.#options.statements;
|
||||
}
|
||||
|
||||
public validate = (options: ValidateOptions) => {
|
||||
const { statements } = this.#options;
|
||||
return validate({
|
||||
...options,
|
||||
statements,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export { Session };
|
||||
25
src/services/sessions/sessions.utils.ts
Normal file
25
src/services/sessions/sessions.utils.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import micromatch from 'micromatch';
|
||||
|
||||
import type { Statement } from '#root/auth/auth.schemas.ts';
|
||||
|
||||
type ValidateOptions = {
|
||||
action: string;
|
||||
resource: string;
|
||||
statements: Statement[];
|
||||
};
|
||||
|
||||
const validate = (options: ValidateOptions) => {
|
||||
const { statements, resource, action } = options;
|
||||
const matches = statements.filter(
|
||||
(statement) => micromatch.isMatch(resource, statement.resources) && micromatch.isMatch(action, statement.actions),
|
||||
);
|
||||
if (matches.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (matches.find((statement) => statement.effect === 'disallow')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export { validate };
|
||||
Reference in New Issue
Block a user