mirror of
https://github.com/morten-olsen/homelab-operator.git
synced 2026-02-08 01:36:28 +01:00
Migrate to zod
This commit is contained in:
@@ -6,14 +6,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "3.3.1",
|
"@eslint/eslintrc": "3.3.1",
|
||||||
"@eslint/js": "9.32.0",
|
"@eslint/js": "9.32.0",
|
||||||
"@pnpm/find-workspace-packages": "6.0.9",
|
|
||||||
"@types/bun": "latest",
|
|
||||||
"eslint": "9.32.0",
|
"eslint": "9.32.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-prettier": "5.5.3",
|
"eslint-plugin-prettier": "5.5.3",
|
||||||
"nodemon": "^3.1.10",
|
|
||||||
"openapi-typescript": "^7.8.0",
|
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"typescript-eslint": "8.38.0"
|
"typescript-eslint": "8.38.0"
|
||||||
@@ -24,10 +20,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@goauthentik/api": "2025.6.3-1751754396",
|
"@goauthentik/api": "2025.6.3-1751754396",
|
||||||
"@kubernetes/client-node": "^1.3.0",
|
"@kubernetes/client-node": "^1.3.0",
|
||||||
"@sinclair/typebox": "^0.34.38",
|
|
||||||
"dotenv": "^17.2.1",
|
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"openapi-fetch": "^0.14.0",
|
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"yaml": "^2.8.0",
|
"yaml": "^2.8.0",
|
||||||
|
|||||||
1447
pnpm-lock.yaml
generated
1447
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,21 @@
|
|||||||
import { Type } from '@sinclair/typebox';
|
|
||||||
import { SubModeEnum } from '@goauthentik/api';
|
import { SubModeEnum } from '@goauthentik/api';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { CustomResource, type CustomResourceHandlerOptions } from '../../../custom-resource/custom-resource.base.ts';
|
import { CustomResource, type CustomResourceHandlerOptions } from '../../../custom-resource/custom-resource.base.ts';
|
||||||
import { AuthentikService } from '../../../services/authentik/authentik.service.ts';
|
import { AuthentikService } from '../../../services/authentik/authentik.service.ts';
|
||||||
|
|
||||||
const authentikClientSpec = Type.Object({
|
const authentikClientSpec = z.object({
|
||||||
subMode: Type.Optional(Type.Unsafe<SubModeEnum>(Type.String())),
|
subMode: z.enum(SubModeEnum).optional(),
|
||||||
clientType: Type.Optional(
|
clientType: z.enum(['confidential', 'public']).optional(),
|
||||||
Type.Unsafe<'confidential' | 'public'>(
|
redirectUris: z.array(
|
||||||
Type.String({
|
z.object({
|
||||||
enum: ['confidential', 'public'],
|
url: z.string(),
|
||||||
}),
|
matchingMode: z.enum(['strict', 'regex']),
|
||||||
),
|
|
||||||
),
|
|
||||||
redirectUris: Type.Array(
|
|
||||||
Type.Object({
|
|
||||||
url: Type.String(),
|
|
||||||
matchingMode: Type.Unsafe<'strict' | 'regex'>(
|
|
||||||
Type.String({
|
|
||||||
enum: ['strict', 'regex'],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
const authentikClientSecret = Type.Object({
|
const authentikClientSecret = z.object({
|
||||||
clientSecret: Type.String(),
|
clientSecret: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
class AuthentikClient extends CustomResource<typeof authentikClientSpec> {
|
class AuthentikClient extends CustomResource<typeof authentikClientSpec> {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Type } from '@sinclair/typebox';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts';
|
import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts';
|
||||||
import { PostgresService } from '../../services/postgres/postgres.service.ts';
|
import { PostgresService } from '../../services/postgres/postgres.service.ts';
|
||||||
|
|
||||||
const postgresDatabaseSpecSchema = Type.Object({});
|
const postgresDatabaseSpecSchema = z.object({});
|
||||||
|
|
||||||
class PostgresDatabase extends CustomResource<typeof postgresDatabaseSpecSchema> {
|
class PostgresDatabase extends CustomResource<typeof postgresDatabaseSpecSchema> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -22,10 +22,10 @@ class PostgresDatabase extends CustomResource<typeof postgresDatabaseSpecSchema>
|
|||||||
const variables = await ensureSecret({
|
const variables = await ensureSecret({
|
||||||
name: `postgres-database-${request.metadata.name}`,
|
name: `postgres-database-${request.metadata.name}`,
|
||||||
namespace: request.metadata.namespace ?? 'default',
|
namespace: request.metadata.namespace ?? 'default',
|
||||||
schema: Type.Object({
|
schema: z.object({
|
||||||
name: Type.String(),
|
name: z.string(),
|
||||||
user: Type.String(),
|
user: z.string(),
|
||||||
password: Type.String(),
|
password: z.string(),
|
||||||
}),
|
}),
|
||||||
generator: async () => ({
|
generator: async () => ({
|
||||||
name: `${request.metadata.namespace || 'default'}_${request.metadata.name}`,
|
name: `${request.metadata.namespace || 'default'}_${request.metadata.name}`,
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
import { Type } from '@sinclair/typebox';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts';
|
import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts';
|
||||||
|
|
||||||
const stringValueSchema = Type.Object({
|
const stringValueSchema = z.object({
|
||||||
key: Type.String(),
|
key: z.string(),
|
||||||
chars: Type.Optional(Type.String()),
|
chars: z.string().optional(),
|
||||||
length: Type.Optional(Type.Number()),
|
length: z.number().optional(),
|
||||||
encoding: Type.Optional(
|
encoding: z.enum(['utf-8', 'base64', 'base64url', 'hex']).optional(),
|
||||||
Type.String({
|
value: z.string().optional(),
|
||||||
enum: ['utf-8', 'base64', 'base64url', 'hex'],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
value: Type.Optional(Type.String()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretRequestSpec = Type.Object({
|
const secretRequestSpec = z.object({
|
||||||
secretName: Type.Optional(Type.String()),
|
secretName: z.string().optional(),
|
||||||
data: Type.Array(stringValueSchema),
|
data: z.array(stringValueSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
class SecretRequest extends CustomResource<typeof secretRequestSpec> {
|
class SecretRequest extends CustomResource<typeof secretRequestSpec> {
|
||||||
@@ -38,7 +34,7 @@ class SecretRequest extends CustomResource<typeof secretRequestSpec> {
|
|||||||
await ensureSecret({
|
await ensureSecret({
|
||||||
name: secretName,
|
name: secretName,
|
||||||
namespace,
|
namespace,
|
||||||
schema: Type.Object({}, { additionalProperties: true }),
|
schema: z.object({}).passthrough(),
|
||||||
generator: async () => ({
|
generator: async () => ({
|
||||||
hello: 'world',
|
hello: 'world',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { type Static, type TObject, type TSchema } from '@sinclair/typebox';
|
import { z, type ZodObject } from 'zod';
|
||||||
|
|
||||||
import { GROUP } from '../utils/consts.ts';
|
import { GROUP } from '../utils/consts.ts';
|
||||||
import type { Services } from '../utils/service.ts';
|
import type { Services } from '../utils/service.ts';
|
||||||
import { noopAsync } from '../utils/types.js';
|
import { noopAsync } from '../utils/types.ts';
|
||||||
|
|
||||||
import { customResourceStatusSchema, type CustomResourceRequest } from './custom-resource.request.ts';
|
import { customResourceStatusSchema, type CustomResourceRequest } from './custom-resource.request.ts';
|
||||||
|
|
||||||
type EnsureSecretOptions<T extends TObject> = {
|
type EnsureSecretOptions<T extends ZodObject> = {
|
||||||
schema: T;
|
schema: T;
|
||||||
name: string;
|
name: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
generator: () => Promise<Static<T>>;
|
generator: () => Promise<z.infer<T>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CustomResourceHandlerOptions<TSpec extends TSchema> = {
|
type CustomResourceHandlerOptions<TSpec extends ZodObject> = {
|
||||||
request: CustomResourceRequest<TSpec>;
|
request: CustomResourceRequest<TSpec>;
|
||||||
ensureSecret: <T extends TObject>(options: EnsureSecretOptions<T>) => Promise<Static<T>>;
|
ensureSecret: <T extends ZodObject>(options: EnsureSecretOptions<T>) => Promise<z.infer<T>>;
|
||||||
services: Services;
|
services: Services;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CustomResourceConstructor<TSpec extends TSchema> = {
|
type CustomResourceConstructor<TSpec extends ZodObject> = {
|
||||||
kind: string;
|
kind: string;
|
||||||
spec: TSpec;
|
spec: TSpec;
|
||||||
names: {
|
names: {
|
||||||
@@ -28,7 +28,7 @@ type CustomResourceConstructor<TSpec extends TSchema> = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
abstract class CustomResource<TSpec extends TSchema> {
|
abstract class CustomResource<TSpec extends ZodObject> {
|
||||||
#options: CustomResourceConstructor<TSpec>;
|
#options: CustomResourceConstructor<TSpec>;
|
||||||
|
|
||||||
constructor(options: CustomResourceConstructor<TSpec>) {
|
constructor(options: CustomResourceConstructor<TSpec>) {
|
||||||
@@ -89,8 +89,16 @@ abstract class CustomResource<TSpec extends TSchema> {
|
|||||||
openAPIV3Schema: {
|
openAPIV3Schema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
spec: this.spec,
|
spec: {
|
||||||
status: customResourceStatusSchema as ExpectedAny,
|
...z.toJSONSchema(this.spec.strict(), { io: 'input' }),
|
||||||
|
$schema: undefined,
|
||||||
|
additionalProperties: undefined,
|
||||||
|
} as ExpectedAny,
|
||||||
|
status: {
|
||||||
|
...z.toJSONSchema(customResourceStatusSchema.strict(), { io: 'input' }),
|
||||||
|
$schema: undefined,
|
||||||
|
additionalProperties: undefined,
|
||||||
|
} as ExpectedAny,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -104,7 +112,7 @@ abstract class CustomResource<TSpec extends TSchema> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const createCustomResource = <TSpec extends TSchema>(
|
const createCustomResource = <TSpec extends ZodObject>(
|
||||||
options: CustomResourceConstructor<TSpec> & {
|
options: CustomResourceConstructor<TSpec> & {
|
||||||
update?: (options: CustomResourceHandlerOptions<TSpec>) => Promise<void>;
|
update?: (options: CustomResourceHandlerOptions<TSpec>) => Promise<void>;
|
||||||
create?: (options: CustomResourceHandlerOptions<TSpec>) => Promise<void>;
|
create?: (options: CustomResourceHandlerOptions<TSpec>) => Promise<void>;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ApiException, Watch } from '@kubernetes/client-node';
|
import { ApiException, Watch } from '@kubernetes/client-node';
|
||||||
import { type TObject } from '@sinclair/typebox';
|
import type { ZodObject } from 'zod';
|
||||||
|
|
||||||
import { K8sService } from '../services/k8s.ts';
|
import { K8sService } from '../services/k8s.ts';
|
||||||
import type { Services } from '../utils/service.ts';
|
import type { Services } from '../utils/service.ts';
|
||||||
import { isSchemaValid } from '../utils/schemas.ts';
|
|
||||||
|
|
||||||
import { type CustomResource, type EnsureSecretOptions } from './custom-resource.base.ts';
|
import { type CustomResource, type EnsureSecretOptions } from './custom-resource.base.ts';
|
||||||
import { CustomResourceRequest } from './custom-resource.request.ts';
|
import { CustomResourceRequest } from './custom-resource.request.ts';
|
||||||
|
|
||||||
class CustomResourceRegistry {
|
class CustomResourceRegistry {
|
||||||
#services: Services;
|
#services: Services;
|
||||||
#resources = new Set<CustomResource<ExpectedAny>>();
|
#resources = new Set<CustomResource<ExpectedAny>>();
|
||||||
@@ -53,7 +53,7 @@ class CustomResourceRegistry {
|
|||||||
|
|
||||||
#ensureSecret =
|
#ensureSecret =
|
||||||
(request: CustomResourceRequest<ExpectedAny>) =>
|
(request: CustomResourceRequest<ExpectedAny>) =>
|
||||||
async <T extends TObject>(options: EnsureSecretOptions<T>) => {
|
async <T extends ZodObject>(options: EnsureSecretOptions<T>) => {
|
||||||
const { schema, name, namespace, generator } = options;
|
const { schema, name, namespace, generator } = options;
|
||||||
const { metadata } = request;
|
const { metadata } = request;
|
||||||
const k8sService = this.#services.get(K8sService);
|
const k8sService = this.#services.get(K8sService);
|
||||||
@@ -69,7 +69,7 @@ class CustomResourceRegistry {
|
|||||||
const decoded = Object.fromEntries(
|
const decoded = Object.fromEntries(
|
||||||
Object.entries(secret.data).map(([key, value]) => [key, Buffer.from(value, 'base64').toString('utf-8')]),
|
Object.entries(secret.data).map(([key, value]) => [key, Buffer.from(value, 'base64').toString('utf-8')]),
|
||||||
);
|
);
|
||||||
if (isSchemaValid(schema, decoded)) {
|
if (schema.safeParse(decoded).success) {
|
||||||
return decoded;
|
return decoded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,6 +203,7 @@ class CustomResourceRegistry {
|
|||||||
public install = async (replace = false) => {
|
public install = async (replace = false) => {
|
||||||
const k8sService = this.#services.get(K8sService);
|
const k8sService = this.#services.get(K8sService);
|
||||||
for (const crd of this.#resources) {
|
for (const crd of this.#resources) {
|
||||||
|
this.#services.log.info('Installing CRD', { kind: crd.kind });
|
||||||
try {
|
try {
|
||||||
const manifest = crd.toManifest();
|
const manifest = crd.toManifest();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Type, type Static, type TSchema } from '@sinclair/typebox';
|
|
||||||
import { ApiException, PatchStrategy, setHeaderOptions, V1MicroTime } from '@kubernetes/client-node';
|
import { ApiException, PatchStrategy, setHeaderOptions, V1MicroTime } from '@kubernetes/client-node';
|
||||||
|
import { z, type ZodObject } from 'zod';
|
||||||
|
|
||||||
import type { Services } from '../utils/service.ts';
|
import type { Services } from '../utils/service.ts';
|
||||||
import { K8sService } from '../services/k8s.ts';
|
import { K8sService } from '../services/k8s.ts';
|
||||||
@@ -31,24 +31,22 @@ type CustomResourceEvent = {
|
|||||||
type: 'Normal' | 'Warning' | 'Error';
|
type: 'Normal' | 'Warning' | 'Error';
|
||||||
};
|
};
|
||||||
|
|
||||||
const customResourceStatusSchema = Type.Object({
|
const customResourceStatusSchema = z.object({
|
||||||
observedGeneration: Type.Number(),
|
observedGeneration: z.number(),
|
||||||
conditions: Type.Array(
|
conditions: z.array(
|
||||||
Type.Object({
|
z.object({
|
||||||
type: Type.String(),
|
type: z.string(),
|
||||||
status: Type.String({
|
status: z.enum(['True', 'False', 'Unknown']),
|
||||||
enum: ['True', 'False', 'Unknown'],
|
lastTransitionTime: z.string().datetime(),
|
||||||
}),
|
reason: z.string().optional(),
|
||||||
lastTransitionTime: Type.String({ format: 'date-time' }),
|
message: z.string().optional(),
|
||||||
reason: Type.Optional(Type.String()),
|
|
||||||
message: Type.Optional(Type.String()),
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
type CustomResourceStatus = Static<typeof customResourceStatusSchema>;
|
type CustomResourceStatus = z.infer<typeof customResourceStatusSchema>;
|
||||||
|
|
||||||
class CustomResourceRequest<TSpec extends TSchema> {
|
class CustomResourceRequest<TSpec extends ZodObject> {
|
||||||
#options: CustomResourceRequestOptions;
|
#options: CustomResourceRequestOptions;
|
||||||
|
|
||||||
constructor(options: CustomResourceRequestOptions) {
|
constructor(options: CustomResourceRequestOptions) {
|
||||||
@@ -75,7 +73,7 @@ class CustomResourceRequest<TSpec extends TSchema> {
|
|||||||
return this.#options.manifest.apiVersion;
|
return this.#options.manifest.apiVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get spec(): Static<TSpec> {
|
public get spec(): z.infer<TSpec> {
|
||||||
return this.#options.manifest.spec;
|
return this.#options.manifest.spec;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +209,7 @@ class CustomResourceRequest<TSpec extends TSchema> {
|
|||||||
apiVersion: string;
|
apiVersion: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
metadata: CustomResourceRequestMetadata;
|
metadata: CustomResourceRequestMetadata;
|
||||||
spec: Static<TSpec>;
|
spec: z.infer<TSpec>;
|
||||||
status: CustomResourceStatus;
|
status: CustomResourceStatus;
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ process.on('uncaughtException', (error) => {
|
|||||||
return console.error(error.body);
|
return console.error(error.body);
|
||||||
}
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('unhandledRejection', (error) => {
|
process.on('unhandledRejection', (error) => {
|
||||||
@@ -41,4 +42,5 @@ process.on('unhandledRejection', (error) => {
|
|||||||
return console.error(error.body);
|
return console.error(error.body);
|
||||||
}
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { Static, TSchema } from '@sinclair/typebox';
|
|
||||||
import { TypeCompiler } from '@sinclair/typebox/compiler';
|
|
||||||
|
|
||||||
const isSchemaValid = <T extends TSchema>(schema: T, data: unknown): data is Static<T> => {
|
|
||||||
const compiler = TypeCompiler.Compile(schema);
|
|
||||||
return compiler.Check(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { isSchemaValid };
|
|
||||||
Reference in New Issue
Block a user