mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
linting
This commit is contained in:
18
.prettierrc.json
Normal file
18
.prettierrc.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"insertPragma": false,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"printWidth": 120,
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"requirePragma": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"useTabs": false,
|
||||||
|
"singleAttributePerLine": false
|
||||||
|
}
|
||||||
14
.u8.json
Normal file
14
.u8.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"values": {
|
||||||
|
"monoRepo": false
|
||||||
|
},
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"timestamp": "2025-07-28T20:25:00.416Z",
|
||||||
|
"template": "eslint",
|
||||||
|
"values": {
|
||||||
|
"monoRepo": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
51
eslint.config.mjs
Normal file
51
eslint.config.mjs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
import importPlugin from 'eslint-plugin-import';
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: import.meta.__dirname,
|
||||||
|
resolvePluginsRelativeTo: import.meta.__dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.strict,
|
||||||
|
...tseslint.configs.stylistic,
|
||||||
|
eslintConfigPrettier,
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsxx}'],
|
||||||
|
extends: [importPlugin.flatConfigs.recommended, importPlugin.flatConfigs.typescript],
|
||||||
|
rules: {
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'import/extensions': ['error', 'ignorePackages'],
|
||||||
|
'import/exports-last': 'error',
|
||||||
|
'import/no-default-export': 'error',
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||||
|
'newlines-between': 'always',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'import/no-duplicates': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**.d.ts'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/triple-slash-reference': 'off',
|
||||||
|
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...compat.extends('plugin:prettier/recommended'),
|
||||||
|
{
|
||||||
|
ignores: ['**/node_modules/', '**/dist/', '**/.turbo/', '**/generated/'],
|
||||||
|
},
|
||||||
|
);
|
||||||
15
package.json
15
package.json
@@ -5,7 +5,17 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"nodemon": "^3.1.10"
|
"nodemon": "^3.1.10",
|
||||||
|
"@eslint/eslintrc": "3.3.1",
|
||||||
|
"@eslint/js": "9.32.0",
|
||||||
|
"@pnpm/find-workspace-packages": "6.0.9",
|
||||||
|
"eslint": "9.32.0",
|
||||||
|
"eslint-config-prettier": "10.1.8",
|
||||||
|
"eslint-plugin-import": "2.32.0",
|
||||||
|
"eslint-plugin-prettier": "5.5.3",
|
||||||
|
"prettier": "3.6.2",
|
||||||
|
"typescript": "5.8.3",
|
||||||
|
"typescript-eslint": "8.38.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
@@ -22,5 +32,8 @@
|
|||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"sqlite3"
|
"sqlite3"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test:lint": "eslint"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3046
pnpm-lock.yaml
generated
3046
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,8 @@
|
|||||||
apiVersion: 'homelab.mortenolsen.pro/v1'
|
apiVersion: 'homelab.mortenolsen.pro/v1';
|
||||||
kind: 'PostgresDatabase'
|
kind: 'PostgresDatabase';
|
||||||
metadata:
|
name: 'test2';
|
||||||
name: 'test2'
|
namespace: 'playground';
|
||||||
namespace: 'playground'
|
foo: 'bar';
|
||||||
labels:
|
foo: 'bar';
|
||||||
foo: 'bar'
|
{
|
||||||
annotations:
|
}
|
||||||
foo: 'bar'
|
|
||||||
spec: {}
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from '@sinclair/typebox';
|
||||||
import { CustomResource, type CustomResourceHandlerOptions } from "../../custom-resource/custom-resource.base.ts";
|
import { ApiException, type V1Secret } from '@kubernetes/client-node';
|
||||||
import { K8sService } from "../../services/k8s.ts";
|
|
||||||
import { ApiException, type V1Secret } from "@kubernetes/client-node";
|
|
||||||
import type { CustomResourceRequest } from "../../custom-resource/custom-resource.request.ts";
|
|
||||||
import { PostgresService } from "../../services/postgres/postgres.service.ts";
|
|
||||||
|
|
||||||
const postgresDatabaseSpecSchema = Type.Object({
|
import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts';
|
||||||
});
|
import { K8sService } from '../../services/k8s.ts';
|
||||||
|
import type { CustomResourceRequest } from '../../custom-resource/custom-resource.request.ts';
|
||||||
|
import { PostgresService } from '../../services/postgres/postgres.service.ts';
|
||||||
|
|
||||||
|
const postgresDatabaseSpecSchema = Type.Object({});
|
||||||
|
|
||||||
class PostgresDatabase extends CustomResource<typeof postgresDatabaseSpecSchema> {
|
class PostgresDatabase extends CustomResource<typeof postgresDatabaseSpecSchema> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -51,7 +51,7 @@ class PostgresDatabase extends CustomResource<typeof postgresDatabaseSpecSchema>
|
|||||||
name: Buffer.from(`${metadata.namespace}_${metadata.name}`).toString('base64'),
|
name: Buffer.from(`${metadata.namespace}_${metadata.name}`).toString('base64'),
|
||||||
user: Buffer.from(metadata.name).toString('base64'),
|
user: Buffer.from(metadata.name).toString('base64'),
|
||||||
password: Buffer.from(crypto.randomUUID()).toString('base64'),
|
password: Buffer.from(crypto.randomUUID()).toString('base64'),
|
||||||
}
|
};
|
||||||
const namespace = metadata.namespace ?? 'default';
|
const namespace = metadata.namespace ?? 'default';
|
||||||
|
|
||||||
services.log.debug('Creating secret', { data });
|
services.log.debug('Creating secret', { data });
|
||||||
@@ -77,7 +77,7 @@ class PostgresDatabase extends CustomResource<typeof postgresDatabaseSpecSchema>
|
|||||||
});
|
});
|
||||||
services.log.debug('Secret created', { response });
|
services.log.debug('Secret created', { response });
|
||||||
return response.data!;
|
return response.data!;
|
||||||
}
|
};
|
||||||
|
|
||||||
public update = async (options: CustomResourceHandlerOptions<typeof postgresDatabaseSpecSchema>) => {
|
public update = async (options: CustomResourceHandlerOptions<typeof postgresDatabaseSpecSchema>) => {
|
||||||
const { request, services } = options;
|
const { request, services } = options;
|
||||||
@@ -113,7 +113,7 @@ class PostgresDatabase extends CustomResource<typeof postgresDatabaseSpecSchema>
|
|||||||
services.log.error('Error updating PostgresRole', { error });
|
services.log.error('Error updating PostgresRole', { error });
|
||||||
return await status.save();
|
return await status.save();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PostgresDatabase };
|
export { PostgresDatabase };
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from '@sinclair/typebox';
|
||||||
import { CustomResource, type CustomResourceHandlerOptions } from "../../custom-resource/custom-resource.base.ts";
|
import { ApiException, type V1Secret } from '@kubernetes/client-node';
|
||||||
import { K8sService } from "../../services/k8s.ts";
|
|
||||||
import { ApiException, type V1Secret } from "@kubernetes/client-node";
|
import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts';
|
||||||
|
import { K8sService } from '../../services/k8s.ts';
|
||||||
|
|
||||||
const stringValueSchema = Type.String({
|
const stringValueSchema = Type.String({
|
||||||
key: Type.String(),
|
key: Type.String(),
|
||||||
chars: Type.Optional(Type.String()),
|
chars: Type.Optional(Type.String()),
|
||||||
length: Type.Optional(Type.Number()),
|
length: Type.Optional(Type.Number()),
|
||||||
encoding: Type.Optional(Type.String({
|
encoding: Type.Optional(
|
||||||
enum: ['utf-8', 'base64', 'base64url', 'hex'],
|
Type.String({
|
||||||
})),
|
enum: ['utf-8', 'base64', 'base64url', 'hex'],
|
||||||
|
}),
|
||||||
|
),
|
||||||
value: Type.Optional(Type.String()),
|
value: Type.Optional(Type.String()),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,11 +74,11 @@ class SecretRequest extends CustomResource<typeof secretRequestSpec> {
|
|||||||
type: 'Opaque',
|
type: 'Opaque',
|
||||||
data: {
|
data: {
|
||||||
// TODO: generate data from spec
|
// TODO: generate data from spec
|
||||||
'test': 'test',
|
test: 'test',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
public update = async (options: CustomResourceHandlerOptions<typeof secretRequestSpec>) => {
|
public update = async (options: CustomResourceHandlerOptions<typeof secretRequestSpec>) => {
|
||||||
const { request } = options;
|
const { request } = options;
|
||||||
@@ -88,14 +91,14 @@ class SecretRequest extends CustomResource<typeof secretRequestSpec> {
|
|||||||
message: 'Secret created',
|
message: 'Secret created',
|
||||||
});
|
});
|
||||||
return await status.save();
|
return await status.save();
|
||||||
} catch (error) {
|
} catch {
|
||||||
status.setCondition('Ready', {
|
status.setCondition('Ready', {
|
||||||
status: 'False',
|
status: 'False',
|
||||||
reason: 'SecretNotCreated',
|
reason: 'SecretNotCreated',
|
||||||
message: 'Secret not created',
|
message: 'Secret not created',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { SecretRequest };
|
export { SecretRequest };
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { type Static, type TSchema } from "@sinclair/typebox";
|
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";
|
|
||||||
|
|
||||||
|
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> = {
|
type CustomResourceHandlerOptions<TSpec extends TSchema> = {
|
||||||
request: CustomResourceRequest<TSpec>;
|
request: CustomResourceRequest<TSpec>;
|
||||||
services: Services;
|
services: Services;
|
||||||
}
|
};
|
||||||
|
|
||||||
type CustomResourceConstructor<TSpec extends TSchema> = {
|
type CustomResourceConstructor<TSpec extends TSchema> = {
|
||||||
kind: string;
|
kind: string;
|
||||||
@@ -17,25 +18,21 @@ type CustomResourceConstructor<TSpec extends TSchema> = {
|
|||||||
plural: string;
|
plural: string;
|
||||||
singular: string;
|
singular: string;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
abstract class CustomResource<
|
abstract class CustomResource<TSpec extends TSchema> {
|
||||||
TSpec extends TSchema
|
|
||||||
> {
|
|
||||||
#options: CustomResourceConstructor<TSpec>;
|
#options: CustomResourceConstructor<TSpec>;
|
||||||
|
|
||||||
constructor(options: CustomResourceConstructor<TSpec>) {
|
constructor(options: CustomResourceConstructor<TSpec>) {
|
||||||
this.#options = options;
|
this.#options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly version = 'v1';
|
||||||
|
|
||||||
public get name() {
|
public get name() {
|
||||||
return `${this.#options.names.plural}.${this.group}`;
|
return `${this.#options.names.plural}.${this.group}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get version() {
|
|
||||||
return 'v1';
|
|
||||||
}
|
|
||||||
|
|
||||||
public get group() {
|
public get group() {
|
||||||
return GROUP;
|
return GROUP;
|
||||||
}
|
}
|
||||||
@@ -75,27 +72,28 @@ abstract class CustomResource<
|
|||||||
singular: this.#options.names.singular,
|
singular: this.#options.names.singular,
|
||||||
},
|
},
|
||||||
scope: 'Namespaced',
|
scope: 'Namespaced',
|
||||||
versions: [{
|
versions: [
|
||||||
name: this.version,
|
{
|
||||||
served: true,
|
name: this.version,
|
||||||
storage: true,
|
served: true,
|
||||||
schema: {
|
storage: true,
|
||||||
openAPIV3Schema: {
|
schema: {
|
||||||
type: 'object',
|
openAPIV3Schema: {
|
||||||
properties: {
|
type: 'object',
|
||||||
spec: this.spec,
|
properties: {
|
||||||
status: statusSchema as any,
|
spec: this.spec,
|
||||||
}
|
status: statusSchema,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subresources: {
|
||||||
|
status: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
subresources: {
|
],
|
||||||
status: {}
|
},
|
||||||
}
|
};
|
||||||
}]
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { CustomResource, type CustomResourceConstructor, type CustomResourceHandlerOptions };
|
||||||
export { CustomResource, type CustomResourceConstructor, type CustomResourceHandlerOptions };
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { ApiException, Watch } from "@kubernetes/client-node";
|
import { ApiException, Watch } from '@kubernetes/client-node';
|
||||||
import { K8sService } from "../services/k8s.ts";
|
|
||||||
import type { Services } from "../utils/service.ts";
|
import { K8sService } from '../services/k8s.ts';
|
||||||
import { type CustomResource } from "./custom-resource.base.ts";
|
import type { Services } from '../utils/service.ts';
|
||||||
import { CustomResourceRequest } from "./custom-resource.request.ts";
|
|
||||||
|
import { type CustomResource } from './custom-resource.base.ts';
|
||||||
|
import { CustomResourceRequest } from './custom-resource.request.ts';
|
||||||
|
|
||||||
class CustomResourceRegistry {
|
class CustomResourceRegistry {
|
||||||
#services: Services;
|
#services: Services;
|
||||||
#resources: Set<CustomResource<any>> = new Set();
|
#resources = new Set<CustomResource<any>>();
|
||||||
#watchers: Map<string, AbortController> = new Map();
|
#watchers = new Map<string, AbortController>();
|
||||||
|
|
||||||
constructor(services: Services) {
|
constructor(services: Services) {
|
||||||
this.#services = services;
|
this.#services = services;
|
||||||
@@ -19,11 +21,11 @@ class CustomResourceRegistry {
|
|||||||
|
|
||||||
public getByKind = (kind: string) => {
|
public getByKind = (kind: string) => {
|
||||||
return Array.from(this.#resources).find((r) => r.kind === kind);
|
return Array.from(this.#resources).find((r) => r.kind === kind);
|
||||||
}
|
};
|
||||||
|
|
||||||
public register = (resource: CustomResource<any>) => {
|
public register = (resource: CustomResource<any>) => {
|
||||||
this.#resources.add(resource);
|
this.#resources.add(resource);
|
||||||
}
|
};
|
||||||
|
|
||||||
public unregister = (resource: CustomResource<any>) => {
|
public unregister = (resource: CustomResource<any>) => {
|
||||||
this.#resources.delete(resource);
|
this.#resources.delete(resource);
|
||||||
@@ -33,7 +35,7 @@ class CustomResourceRegistry {
|
|||||||
this.#watchers.delete(kind);
|
this.#watchers.delete(kind);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
public watch = async () => {
|
public watch = async () => {
|
||||||
const k8sService = this.#services.get(K8sService);
|
const k8sService = this.#services.get(K8sService);
|
||||||
@@ -46,7 +48,7 @@ class CustomResourceRegistry {
|
|||||||
const controller = await watcher.watch(path, {}, this.#onResourceEvent, this.#onError);
|
const controller = await watcher.watch(path, {}, this.#onResourceEvent, this.#onError);
|
||||||
this.#watchers.set(resource.kind, controller);
|
this.#watchers.set(resource.kind, controller);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
#onResourceEvent = async (type: string, obj: any) => {
|
#onResourceEvent = async (type: string, obj: any) => {
|
||||||
console.log(type, this.kinds);
|
console.log(type, this.kinds);
|
||||||
@@ -79,12 +81,12 @@ class CustomResourceRegistry {
|
|||||||
await handler?.({
|
await handler?.({
|
||||||
request,
|
request,
|
||||||
services: this.#services,
|
services: this.#services,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
#onError = (error: any) => {
|
#onError = (error: any) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
};
|
||||||
|
|
||||||
public install = async (replace = false) => {
|
public install = async (replace = false) => {
|
||||||
const k8sService = this.#services.get(K8sService);
|
const k8sService = this.#services.get(K8sService);
|
||||||
@@ -107,7 +109,7 @@ class CustomResourceRegistry {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { CustomResourceRegistry };
|
export { CustomResourceRegistry };
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { Static, TSchema } from "@sinclair/typebox";
|
import type { Static, TSchema } from '@sinclair/typebox';
|
||||||
import type { Services } from "../utils/service.ts";
|
import { ApiException, PatchStrategy, setHeaderOptions } from '@kubernetes/client-node';
|
||||||
import { K8sService } from "../services/k8s.ts";
|
|
||||||
import { CustomResourceRegistry } from "./custom-resource.registry.ts";
|
import type { Services } from '../utils/service.ts';
|
||||||
import { CustomResourceStatus, type CustomResourceStatusType } from "./custom-resource.status.ts";
|
import { K8sService } from '../services/k8s.ts';
|
||||||
import { ApiException, PatchStrategy, setHeaderOptions } from "@kubernetes/client-node";
|
|
||||||
|
import { CustomResourceRegistry } from './custom-resource.registry.ts';
|
||||||
|
import { CustomResourceStatus, type CustomResourceStatusType } from './custom-resource.status.ts';
|
||||||
|
|
||||||
type CustomResourceRequestOptions = {
|
type CustomResourceRequestOptions = {
|
||||||
type: 'ADDED' | 'DELETED' | 'MODIFIED';
|
type: 'ADDED' | 'DELETED' | 'MODIFIED';
|
||||||
@@ -22,7 +24,7 @@ type CustomResourceRequestMetadata = Record<string, string> & {
|
|||||||
generation: number;
|
generation: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CustomResourceRequest<TSpec extends TSchema>{
|
class CustomResourceRequest<TSpec extends TSchema> {
|
||||||
#options: CustomResourceRequestOptions;
|
#options: CustomResourceRequestOptions;
|
||||||
|
|
||||||
constructor(options: CustomResourceRequestOptions) {
|
constructor(options: CustomResourceRequestOptions) {
|
||||||
@@ -59,18 +61,19 @@ class CustomResourceRequest<TSpec extends TSchema>{
|
|||||||
|
|
||||||
public isOwnerOf = (manifest: any) => {
|
public isOwnerOf = (manifest: any) => {
|
||||||
const ownerRef = manifest?.metadata?.ownerReferences || [];
|
const ownerRef = manifest?.metadata?.ownerReferences || [];
|
||||||
return ownerRef.some((ref: any) =>
|
return ownerRef.some(
|
||||||
ref.apiVersion === this.apiVersion &&
|
(ref: any) =>
|
||||||
ref.kind === this.kind &&
|
ref.apiVersion === this.apiVersion &&
|
||||||
ref.name === this.metadata.name &&
|
ref.kind === this.kind &&
|
||||||
ref.uid === this.metadata.uid
|
ref.name === this.metadata.name &&
|
||||||
|
ref.uid === this.metadata.uid,
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
public setStatus = async (status: CustomResourceStatusType) => {
|
public setStatus = async (status: CustomResourceStatusType) => {
|
||||||
const { manifest, services } = this.#options;
|
const { manifest, services } = this.#options;
|
||||||
const { kind, metadata } = manifest;
|
const { kind, metadata } = manifest;
|
||||||
const registry = services.get(CustomResourceRegistry);
|
const registry = services.get(CustomResourceRegistry);
|
||||||
const crd = registry.getByKind(kind);
|
const crd = registry.getByKind(kind);
|
||||||
if (!crd) {
|
if (!crd) {
|
||||||
throw new Error(`Custom resource ${kind} not found`);
|
throw new Error(`Custom resource ${kind} not found`);
|
||||||
@@ -80,17 +83,20 @@ class CustomResourceRequest<TSpec extends TSchema>{
|
|||||||
|
|
||||||
const { namespace = 'default', name } = metadata;
|
const { namespace = 'default', name } = metadata;
|
||||||
|
|
||||||
const response = await k8sService.customObjectsApi.patchNamespacedCustomObjectStatus({
|
const response = await k8sService.customObjectsApi.patchNamespacedCustomObjectStatus(
|
||||||
group: crd.group,
|
{
|
||||||
version: crd.version,
|
group: crd.group,
|
||||||
namespace,
|
version: crd.version,
|
||||||
plural: crd.names.plural,
|
namespace,
|
||||||
name,
|
plural: crd.names.plural,
|
||||||
body: { status },
|
name,
|
||||||
fieldValidation: 'Strict',
|
body: { status },
|
||||||
}, setHeaderOptions('Content-Type', PatchStrategy.MergePatch))
|
fieldValidation: 'Strict',
|
||||||
|
},
|
||||||
|
setHeaderOptions('Content-Type', PatchStrategy.MergePatch),
|
||||||
|
);
|
||||||
return response;
|
return response;
|
||||||
}
|
};
|
||||||
|
|
||||||
public getCurrent = async () => {
|
public getCurrent = async () => {
|
||||||
const { manifest, services } = this.#options;
|
const { manifest, services } = this.#options;
|
||||||
@@ -101,19 +107,19 @@ class CustomResourceRequest<TSpec extends TSchema>{
|
|||||||
throw new Error(`Custom resource ${manifest.kind} not found`);
|
throw new Error(`Custom resource ${manifest.kind} not found`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const resource = await k8sService.customObjectsApi.getNamespacedCustomObject({
|
const resource = await k8sService.customObjectsApi.getNamespacedCustomObject({
|
||||||
group: crd.group,
|
group: crd.group,
|
||||||
version: crd.version,
|
version: crd.version,
|
||||||
plural: crd.names.plural,
|
plural: crd.names.plural,
|
||||||
namespace: manifest.metadata.namespace,
|
namespace: manifest.metadata.namespace,
|
||||||
name: manifest.metadata.name,
|
name: manifest.metadata.name,
|
||||||
});
|
});
|
||||||
return resource as {
|
return resource as {
|
||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
metadata: CustomResourceRequestMetadata;
|
metadata: CustomResourceRequestMetadata;
|
||||||
spec: Static<TSpec>;
|
spec: Static<TSpec>;
|
||||||
status: CustomResourceStatusType;
|
status: CustomResourceStatusType;
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ApiException && error.code === 404) {
|
if (error instanceof ApiException && error.code === 404) {
|
||||||
@@ -121,10 +127,10 @@ class CustomResourceRequest<TSpec extends TSchema>{
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public getStatus = async () => {
|
public getStatus = async () => {
|
||||||
const resource = await this.getCurrent()
|
const resource = await this.getCurrent();
|
||||||
if (!resource || !resource.status) {
|
if (!resource || !resource.status) {
|
||||||
return new CustomResourceStatus({
|
return new CustomResourceStatus({
|
||||||
status: {
|
status: {
|
||||||
@@ -140,8 +146,7 @@ class CustomResourceRequest<TSpec extends TSchema>{
|
|||||||
generation: resource.metadata.generation,
|
generation: resource.metadata.generation,
|
||||||
save: this.setStatus,
|
save: this.setStatus,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { CustomResourceRequest };
|
export { CustomResourceRequest };
|
||||||
|
|||||||
@@ -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({
|
const statusSchema = Type.Object({
|
||||||
observedGeneration: Type.Number(),
|
observedGeneration: Type.Number(),
|
||||||
conditions: Type.Array(Type.Object({
|
conditions: Type.Array(
|
||||||
type: Type.String(),
|
Type.Object({
|
||||||
status: Type.String({
|
type: Type.String(),
|
||||||
enum: ['True', 'False', 'Unknown']
|
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 = {
|
type CustomResourceStatusOptions = {
|
||||||
status?: CustomResourceStatusType;
|
status?: CustomResourceStatusType;
|
||||||
generation: number;
|
generation: number;
|
||||||
save: (status: CustomResourceStatusType) => Promise<void>;
|
save: (status: CustomResourceStatusType) => Promise<void>;
|
||||||
}
|
};
|
||||||
|
|
||||||
class CustomResourceStatus {
|
class CustomResourceStatus {
|
||||||
#status: CustomResourceStatusType;
|
#status: CustomResourceStatusType;
|
||||||
@@ -49,9 +51,12 @@ class CustomResourceStatus {
|
|||||||
|
|
||||||
public getCondition = (type: string) => {
|
public getCondition = (type: string) => {
|
||||||
return this.#status.conditions?.find((condition) => condition.type === type)?.status;
|
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 currentCondition = this.getCondition(type);
|
||||||
const newCondition = {
|
const newCondition = {
|
||||||
...condition,
|
...condition,
|
||||||
@@ -59,22 +64,22 @@ class CustomResourceStatus {
|
|||||||
lastTransitionTime: new Date().toISOString(),
|
lastTransitionTime: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
if (currentCondition) {
|
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 {
|
} else {
|
||||||
this.#status.conditions.push(newCondition);
|
this.#status.conditions.push(newCondition);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public save = async () => {
|
public save = async () => {
|
||||||
await this.#save({
|
await this.#save({
|
||||||
...this.#status,
|
...this.#status,
|
||||||
observedGeneration: this.#generation,
|
observedGeneration: this.#generation,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
public toJSON = () => {
|
public toJSON = () => {
|
||||||
return this.#status;
|
return this.#status;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { CustomResourceStatus, statusSchema, type CustomResourceStatusType };
|
export { CustomResourceStatus, statusSchema, type CustomResourceStatusType };
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import knex, { type Knex } from "knex";
|
import knex, { type Knex } from 'knex';
|
||||||
import { migrationSource } from "./migrations/migrations.ts";
|
|
||||||
import { Services } from "../utils/service.ts";
|
import { Services } from '../utils/service.ts';
|
||||||
import { PostgresService } from "../services/postgres/postgres.service.ts";
|
import { PostgresService } from '../services/postgres/postgres.service.ts';
|
||||||
import { ConfigService } from "../services/config/config.ts";
|
import { ConfigService } from '../services/config/config.ts';
|
||||||
|
|
||||||
|
import { migrationSource } from './migrations/migrations.ts';
|
||||||
|
|
||||||
const DATABASE_NAME = 'homelab';
|
const DATABASE_NAME = 'homelab';
|
||||||
|
|
||||||
@@ -43,15 +45,15 @@ class DatabaseService {
|
|||||||
await db.migrate.latest();
|
await db.migrate.latest();
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
}
|
};
|
||||||
|
|
||||||
public getDb = async () => {
|
public getDb = async () => {
|
||||||
if (!this.#knex) {
|
if (!this.#knex) {
|
||||||
this.#knex = this.#setup();
|
this.#knex = this.#setup();
|
||||||
}
|
}
|
||||||
return this.#knex;
|
return this.#knex;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { tableNames, type Table } from "./migrations/migrations.ts";
|
export { tableNames, type Table } from './migrations/migrations.ts';
|
||||||
export { DatabaseService };
|
export { DatabaseService };
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Migration } from "./migrations.types.ts";
|
import type { Migration } from './migrations.types.ts';
|
||||||
|
|
||||||
const tableNames = {
|
const tableNames = {
|
||||||
secrets: 'secrets',
|
secrets: 'secrets',
|
||||||
postgresRoles: 'postgres_roles',
|
postgresRoles: 'postgres_roles',
|
||||||
}
|
};
|
||||||
|
|
||||||
const init: Migration = {
|
const init: Migration = {
|
||||||
name: 'init',
|
name: 'init',
|
||||||
@@ -27,13 +27,13 @@ const init: Migration = {
|
|||||||
await db.schema.dropTable(tableNames.secrets);
|
await db.schema.dropTable(tableNames.secrets);
|
||||||
await db.schema.dropTable(tableNames.postgresRoles);
|
await db.schema.dropTable(tableNames.postgresRoles);
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
type PostgresRoleRow = {
|
type PostgresRoleRow = {
|
||||||
name: string;
|
name: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type SecretRow = {
|
type SecretRow = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -41,11 +41,11 @@ type SecretRow = {
|
|||||||
secretName: string;
|
secretName: string;
|
||||||
template: Record<string, unknown>;
|
template: Record<string, unknown>;
|
||||||
data: Record<string, string>;
|
data: Record<string, string>;
|
||||||
}
|
};
|
||||||
|
|
||||||
type Table = {
|
type Table = {
|
||||||
secrets: SecretRow;
|
secrets: SecretRow;
|
||||||
postgresRoles: PostgresRoleRow;
|
postgresRoles: PostgresRoleRow;
|
||||||
}
|
};
|
||||||
|
|
||||||
export { init, tableNames, type PostgresRoleRow, type SecretRow, type Table };
|
export { init, tableNames, type PostgresRoleRow, type SecretRow, type Table };
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import type { Knex } from "knex";
|
import type { Knex } from 'knex';
|
||||||
import { init } from "./migrations-001.init.ts";
|
|
||||||
import type { Migration } from "./migrations.types.ts";
|
|
||||||
|
|
||||||
const migrations = [
|
import { init } from './migrations-001.init.ts';
|
||||||
init,
|
import type { Migration } from './migrations.types.ts';
|
||||||
] satisfies Migration[];
|
|
||||||
|
const migrations = [init] satisfies Migration[];
|
||||||
|
|
||||||
const migrationSource: Knex.MigrationSource<Migration> = {
|
const migrationSource: Knex.MigrationSource<Migration> = {
|
||||||
getMigrations: async () => migrations,
|
getMigrations: async () => migrations,
|
||||||
getMigrationName: (migration) => migration.name,
|
getMigrationName: (migration) => migration.name,
|
||||||
getMigration: async (migration) => migration,
|
getMigration: async (migration) => migration,
|
||||||
}
|
};
|
||||||
|
|
||||||
export { tableNames, type Table } from "./migrations-001.init.ts";
|
export { tableNames, type Table } from './migrations-001.init.ts';
|
||||||
export { migrationSource };
|
export { migrationSource };
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Knex } from "knex";
|
import type { Knex } from 'knex';
|
||||||
|
|
||||||
type Migration = {
|
type Migration = {
|
||||||
name: string;
|
name: string;
|
||||||
up: (db: Knex) => Promise<void>;
|
up: (db: Knex) => Promise<void>;
|
||||||
down: (db: Knex) => Promise<void>;
|
down: (db: Knex) => Promise<void>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type { Migration };
|
export type { Migration };
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ const registry = services.get(CustomResourceRegistry);
|
|||||||
registry.register(new SecretRequest());
|
registry.register(new SecretRequest());
|
||||||
registry.register(new PostgresDatabase());
|
registry.register(new PostgresDatabase());
|
||||||
await registry.install(true);
|
await registry.install(true);
|
||||||
await registry.watch();
|
await registry.watch();
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ class ConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ConfigService };
|
export { ConfigService };
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ class K8sService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { K8sService };
|
export { K8sService };
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
class LogService {
|
class LogService {
|
||||||
public debug = (message: string, data?: Record<string, unknown>) => {
|
public debug = (message: string, data?: Record<string, unknown>) => {
|
||||||
console.debug(message, data);
|
console.debug(message, data);
|
||||||
}
|
};
|
||||||
|
|
||||||
public info = (message: string, data?: Record<string, unknown>) => {
|
public info = (message: string, data?: Record<string, unknown>) => {
|
||||||
console.info(message, data);
|
console.info(message, data);
|
||||||
}
|
};
|
||||||
|
|
||||||
public warn = (message: string, data?: Record<string, unknown>) => {
|
public warn = (message: string, data?: Record<string, unknown>) => {
|
||||||
console.warn(message, data);
|
console.warn(message, data);
|
||||||
}
|
};
|
||||||
|
|
||||||
public error = (message: string, data?: Record<string, unknown>) => {
|
public error = (message: string, data?: Record<string, unknown>) => {
|
||||||
console.error(message, data);
|
console.error(message, data);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { LogService };
|
export { LogService };
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import knex, { type Knex } from "knex";
|
import knex, { type Knex } from 'knex';
|
||||||
import type { PostgresDatabase, PostgresRole } from "./postgres.types.ts";
|
|
||||||
import { Services } from "../../utils/service.ts";
|
import { Services } from '../../utils/service.ts';
|
||||||
import { ConfigService } from "../config/config.ts";
|
import { ConfigService } from '../config/config.ts';
|
||||||
|
|
||||||
|
import type { PostgresDatabase, PostgresRole } from './postgres.types.ts';
|
||||||
|
|
||||||
class PostgresService {
|
class PostgresService {
|
||||||
#db: Knex;
|
#db: Knex;
|
||||||
@@ -21,39 +23,24 @@ class PostgresService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public upsertRole = async (role: PostgresRole) => {
|
public upsertRole = async (role: PostgresRole) => {
|
||||||
const existingRole = await this.#db.raw(
|
const existingRole = await this.#db.raw('SELECT 1 FROM pg_roles WHERE rolname = ?', [role.name]);
|
||||||
'SELECT 1 FROM pg_roles WHERE rolname = ?',
|
|
||||||
[role.name]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingRole.rows.length === 0) {
|
if (existingRole.rows.length === 0) {
|
||||||
await this.#db.raw(
|
await this.#db.raw(`CREATE ROLE ${role.name} WITH LOGIN PASSWORD '${role.password}'`);
|
||||||
`CREATE ROLE ${role.name} WITH LOGIN PASSWORD '${role.password}'`,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await this.#db.raw(
|
await this.#db.raw(`ALTER ROLE ${role.name} WITH PASSWORD '${role.password}'`);
|
||||||
`ALTER ROLE ${role.name} WITH PASSWORD '${role.password}'`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public upsertDatabase = async (database: PostgresDatabase) => {
|
public upsertDatabase = async (database: PostgresDatabase) => {
|
||||||
const existingDatabase = await this.#db.raw(
|
const existingDatabase = await this.#db.raw('SELECT * FROM pg_database WHERE datname = ?', [database.name]);
|
||||||
'SELECT * FROM pg_database WHERE datname = ?',
|
|
||||||
[database.name]
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
if (existingDatabase.rows.length === 0) {
|
if (existingDatabase.rows.length === 0) {
|
||||||
await this.#db.raw(
|
await this.#db.raw(`CREATE DATABASE ${database.name} OWNER ${database.owner}`);
|
||||||
`CREATE DATABASE ${database.name} OWNER ${database.owner}`,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await this.#db.raw(
|
await this.#db.raw(`ALTER DATABASE ${database.name} OWNER TO ${database.owner}`);
|
||||||
`ALTER DATABASE ${database.name} OWNER TO ${database.owner}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PostgresService };
|
export { PostgresService };
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ type PostgresDatabase = {
|
|||||||
owner: string;
|
owner: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { PostgresRole, PostgresDatabase };
|
export type { PostgresRole, PostgresDatabase };
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
class SecretsService {
|
class SecretsService {}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
const GROUP = 'homelab.mortenolsen.pro';
|
const GROUP = 'homelab.mortenolsen.pro';
|
||||||
|
|
||||||
export { GROUP };
|
export { GROUP };
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { LogService } from "../services/log/log.ts";
|
import { LogService } from '../services/log/log.ts';
|
||||||
|
|
||||||
type Dependency<T> = new (services: Services) => T;
|
type Dependency<T> = new (services: Services) => T;
|
||||||
|
|
||||||
class Services {
|
class Services {
|
||||||
#instances: Map<Dependency<unknown>, unknown> = new Map();
|
#instances = new Map<Dependency<unknown>, unknown>();
|
||||||
constructor() {
|
constructor() {
|
||||||
console.log('Constructor', 'bar');
|
console.log('Constructor', 'bar');
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ class Services {
|
|||||||
this.#instances.set(dependency, new dependency(this));
|
this.#instances.set(dependency, new dependency(this));
|
||||||
}
|
}
|
||||||
return this.#instances.get(dependency) as T;
|
return this.#instances.get(dependency) as T;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Services };
|
export { Services };
|
||||||
|
|||||||
Reference in New Issue
Block a user