From ad799515f4c7af9916514cfe5c47325610aef755 Mon Sep 17 00:00:00 2001 From: Maslow Date: Fri, 25 Nov 2022 14:06:21 +0800 Subject: [PATCH] feat(server): add k8s patch method to k8s service (#430) Signed-off-by: maslow --- server/package-lock.json | 11 +++++ server/package.json | 1 + .../src/applications/applications.service.ts | 12 +++--- .../entities/application.entity.ts | 25 ++++++----- .../applications/entities/bundle.entity.ts | 2 +- .../applications/entities/runtime.entity.ts | 2 +- server/src/buckets/buckets.service.ts | 16 +++---- server/src/buckets/entities/bucket.entity.ts | 25 ++++++----- server/src/core/kubernetes.interface.ts | 42 +++++++++++++++++- server/src/core/kubernetes.service.spec.ts | 29 +++++++++++++ server/src/core/kubernetes.service.ts | 43 +++++++++++++++++++ .../src/functions/entities/function.entity.ts | 19 ++++---- server/src/functions/functions.service.ts | 16 +++---- 13 files changed, 186 insertions(+), 57 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 5c00ad7ff2..5038a725f5 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -24,6 +24,7 @@ "@prisma/client": "^4.6.1", "casdoor-nodejs-sdk": "^1.3.0", "dotenv": "^16.0.3", + "fast-json-patch": "^3.1.1", "nanoid": "^3.3.4", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", @@ -6861,6 +6862,11 @@ "node": ">=8.6.0" } }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -18147,6 +18153,11 @@ "micromatch": "^4.0.4" } }, + "fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", diff --git a/server/package.json b/server/package.json index edb19cbcc1..0d6f15a289 100644 --- a/server/package.json +++ b/server/package.json @@ -37,6 +37,7 @@ "@prisma/client": "^4.6.1", "casdoor-nodejs-sdk": "^1.3.0", "dotenv": "^16.0.3", + "fast-json-patch": "^3.1.1", "nanoid": "^3.3.4", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", diff --git a/server/src/applications/applications.service.ts b/server/src/applications/applications.service.ts index 56551c96f8..e6dbdf9acc 100644 --- a/server/src/applications/applications.service.ts +++ b/server/src/applications/applications.service.ts @@ -70,9 +70,9 @@ export class ApplicationsService { async findAll(labelSelector?: string): Promise { const res = await this.k8sClient.customObjectApi.listClusterCustomObject( - Application.Group, - Application.Version, - Application.PluralName, + Application.GVK.group, + Application.GVK.version, + Application.GVK.plural, undefined, undefined, undefined, @@ -95,10 +95,10 @@ export class ApplicationsService { try { const appRes = await this.k8sClient.customObjectApi.getNamespacedCustomObject( - Application.Group, - Application.Version, + Application.GVK.group, + Application.GVK.version, namespace, - Application.PluralName, + Application.GVK.plural, name, ) return appRes.body as Application diff --git a/server/src/applications/entities/application.entity.ts b/server/src/applications/entities/application.entity.ts index 2025456d12..caa2ab2419 100644 --- a/server/src/applications/entities/application.entity.ts +++ b/server/src/applications/entities/application.entity.ts @@ -1,7 +1,11 @@ import { KubernetesObject } from '@kubernetes/client-node' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' -import { ResourceLabels } from 'src/constants' -import { Condition, ObjectMeta } from '../../core/kubernetes.interface' +import { ResourceLabels } from '../../constants' +import { + Condition, + GroupVersionKind, + ObjectMeta, +} from '../../core/kubernetes.interface' import { BundleSpec } from './bundle.entity' import { RuntimeSpec } from './runtime.entity' @@ -84,17 +88,16 @@ export class Application implements KubernetesObject { @ApiPropertyOptional() status?: ApplicationStatus - static readonly Group = 'application.laf.dev' - static readonly Version = 'v1' - static readonly PluralName = 'applications' - static readonly Kind = 'Application' - static get GroupVersion() { - return `${this.Group}/${this.Version}` - } + static readonly GVK = new GroupVersionKind( + 'application.laf.dev', + 'v1', + 'Application', + 'applications', + ) constructor(name: string, namespace: string) { - this.apiVersion = Application.GroupVersion - this.kind = Application.Kind + this.apiVersion = Application.GVK.apiVersion + this.kind = Application.GVK.kind this.metadata = new ObjectMeta(name, namespace) this.metadata.labels = {} this.spec = new ApplicationSpec() diff --git a/server/src/applications/entities/bundle.entity.ts b/server/src/applications/entities/bundle.entity.ts index ef91128fba..2f983665da 100644 --- a/server/src/applications/entities/bundle.entity.ts +++ b/server/src/applications/entities/bundle.entity.ts @@ -1,6 +1,6 @@ import { KubernetesObject } from '@kubernetes/client-node' import { ApiProperty } from '@nestjs/swagger' -import { ObjectMeta } from 'src/core/kubernetes.interface' +import { ObjectMeta } from '../../core/kubernetes.interface' export class BundleSpec { @ApiProperty() diff --git a/server/src/applications/entities/runtime.entity.ts b/server/src/applications/entities/runtime.entity.ts index bc98cffd47..61953f6d1d 100644 --- a/server/src/applications/entities/runtime.entity.ts +++ b/server/src/applications/entities/runtime.entity.ts @@ -1,6 +1,6 @@ import { KubernetesObject } from '@kubernetes/client-node' import { ApiProperty } from '@nestjs/swagger' -import { ObjectMeta } from 'src/core/kubernetes.interface' +import { ObjectMeta } from '../../core/kubernetes.interface' export class RuntimeVersion { @ApiProperty() diff --git a/server/src/buckets/buckets.service.ts b/server/src/buckets/buckets.service.ts index 80b45d080f..f645cc527d 100644 --- a/server/src/buckets/buckets.service.ts +++ b/server/src/buckets/buckets.service.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common' -import { GetApplicationNamespaceById } from 'src/common/getter' -import { KubernetesService } from 'src/core/kubernetes.service' +import { GetApplicationNamespaceById } from '../common/getter' +import { KubernetesService } from '../core/kubernetes.service' import { CreateBucketDto } from './dto/create-bucket.dto' import { UpdateBucketDto } from './dto/update-bucket.dto' import { Bucket, BucketList } from './entities/bucket.entity' @@ -36,10 +36,10 @@ export class BucketsService { async findAll(appid: string, labelSelector?: string) { const namespace = GetApplicationNamespaceById(appid) const res = await this.k8sClient.customObjectApi.listNamespacedCustomObject( - Bucket.Group, - Bucket.Version, + Bucket.GVK.group, + Bucket.GVK.version, namespace, - Bucket.PluralName, + Bucket.GVK.plural, undefined, undefined, undefined, @@ -54,10 +54,10 @@ export class BucketsService { try { const res = await this.k8sClient.customObjectApi.getNamespacedCustomObject( - Bucket.Group, - Bucket.Version, + Bucket.GVK.group, + Bucket.GVK.version, namespace, - Bucket.PluralName, + Bucket.GVK.plural, name, ) return res.body as Bucket diff --git a/server/src/buckets/entities/bucket.entity.ts b/server/src/buckets/entities/bucket.entity.ts index e417cfc882..0512f8e495 100644 --- a/server/src/buckets/entities/bucket.entity.ts +++ b/server/src/buckets/entities/bucket.entity.ts @@ -1,6 +1,10 @@ -import { KubernetesObject, V1ObjectMeta } from '@kubernetes/client-node' +import { KubernetesObject } from '@kubernetes/client-node' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' -import { Condition, ObjectMeta } from '../../core/kubernetes.interface' +import { + Condition, + GroupVersionKind, + ObjectMeta, +} from '../../core/kubernetes.interface' export enum BucketPolicy { Private = 'private', @@ -68,17 +72,16 @@ export class Bucket implements KubernetesObject { @ApiPropertyOptional() status?: BucketStatus - static readonly Group = 'oss.laf.dev' - static readonly Version = 'v1' - static readonly PluralName = 'buckets' - static readonly Kind = 'Bucket' - static get GroupVersion() { - return `${this.Group}/${this.Version}` - } + static readonly GVK = new GroupVersionKind( + 'oss.laf.dev', + 'v1', + 'Bucket', + 'buckets', + ) constructor(name: string, namespace: string) { - this.apiVersion = Bucket.GroupVersion - this.kind = Bucket.Kind + this.apiVersion = Bucket.GVK.apiVersion + this.kind = Bucket.GVK.kind this.metadata = new ObjectMeta(name, namespace) this.spec = new BucketSpec() } diff --git a/server/src/core/kubernetes.interface.ts b/server/src/core/kubernetes.interface.ts index d462fc34ed..efddf27331 100644 --- a/server/src/core/kubernetes.interface.ts +++ b/server/src/core/kubernetes.interface.ts @@ -1,5 +1,6 @@ -import { V1ObjectMeta } from '@kubernetes/client-node' +import { KubernetesObject, V1ObjectMeta } from '@kubernetes/client-node' import { ApiProperty } from '@nestjs/swagger' +import * as assert from 'node:assert' export enum ConditionStatus { ConditionTrue = 'True', @@ -52,3 +53,42 @@ export class Condition { @ApiProperty() message?: string } + +export class GroupVersionKind { + group: string + + version: string + + kind: string + + plural: string + + constructor(group: string, version: string, kind: string, plural?: string) { + assert(group, 'group is required') + assert(version, 'version is required') + assert(kind, 'kind is required') + + this.group = group + this.version = version + this.kind = kind + this.plural = plural + if (!plural) { + this.plural = kind.toLowerCase() + 's' + } + } + + static fromKubernetesObject(obj: KubernetesObject): GroupVersionKind { + assert(obj.apiVersion, 'apiVersion is required') + assert(obj.kind, 'kind is required') + + return new GroupVersionKind( + obj.apiVersion.split('/')[0], + obj.apiVersion.split('/')[1], + obj.kind, + ) + } + + get apiVersion(): string { + return `${this.group}/${this.version}` + } +} diff --git a/server/src/core/kubernetes.service.spec.ts b/server/src/core/kubernetes.service.spec.ts index 264a6cccb0..761211f297 100644 --- a/server/src/core/kubernetes.service.spec.ts +++ b/server/src/core/kubernetes.service.spec.ts @@ -1,4 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing' +import { + Application, + ApplicationState, +} from '../applications/entities/application.entity' import { ResourceLabels } from '../constants' import { KubernetesService } from './kubernetes.service' @@ -86,3 +90,28 @@ describe.skip('list custom objects with label', () => { console.log(res.body) }) }) + +describe.skip('patch custom objects', () => { + it('should be able to patch custom objects', async () => { + const name = '1i43zq' + const namespace = name + const res = await service.customObjectApi.getNamespacedCustomObject( + Application.GVK.group, + Application.GVK.version, + namespace, + Application.GVK.plural, + name, + ) + + const data = res.body as Application + data.spec = { + ...data.spec, + state: ApplicationState.ApplicationStateRunning, + } + + const res2 = await service.patchCustomObject(data).catch((err) => { + console.log(err) + }) + console.log('patched', res2) + }) +}) diff --git a/server/src/core/kubernetes.service.ts b/server/src/core/kubernetes.service.ts index 0415db6c97..d75e7ff2f9 100644 --- a/server/src/core/kubernetes.service.ts +++ b/server/src/core/kubernetes.service.ts @@ -1,5 +1,9 @@ import { Injectable } from '@nestjs/common' import * as k8s from '@kubernetes/client-node' +import { KubernetesObject } from '@kubernetes/client-node' +import { compare } from 'fast-json-patch' +import { GroupVersionKind } from './kubernetes.interface' +import path from 'path' /** * Single instance of the Kubernetes API client. @@ -113,4 +117,43 @@ export class KubernetesService { } return deleted } + + async patchCustomObject(spec: KubernetesObject) { + const client = this.customObjectApi + const gvk = GroupVersionKind.fromKubernetesObject(spec) + + // get the current spec + const res = await client.getNamespacedCustomObject( + gvk.group, + gvk.version, + spec.metadata.namespace, + gvk.plural, + spec.metadata.name, + ) + const currentSpec = res.body as KubernetesObject + + // calculate the patch + const patch = compare(currentSpec, spec) + const options = { + headers: { + 'Content-Type': k8s.PatchUtils.PATCH_FORMAT_JSON_PATCH, + }, + } + + // apply the patch + const response = await client.patchNamespacedCustomObject( + gvk.group, + gvk.version, + spec.metadata.namespace, + gvk.plural, + spec.metadata.name, + patch, + undefined, + undefined, + undefined, + options, + ) + + return response.body + } } diff --git a/server/src/functions/entities/function.entity.ts b/server/src/functions/entities/function.entity.ts index 839b178d08..db0c63c0d4 100644 --- a/server/src/functions/entities/function.entity.ts +++ b/server/src/functions/entities/function.entity.ts @@ -1,6 +1,6 @@ import { KubernetesListObject, KubernetesObject } from '@kubernetes/client-node' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' -import { ObjectMeta } from 'src/core/kubernetes.interface' +import { GroupVersionKind, ObjectMeta } from '../../core/kubernetes.interface' export class CloudFunctionSource { @ApiProperty() @@ -62,17 +62,16 @@ export class CloudFunction implements KubernetesObject { @ApiPropertyOptional() status?: CloudFunctionStatus - static readonly Group = 'runtime.laf.dev' - static readonly Version = 'v1' - static readonly PluralName = 'functions' - static readonly Kind = 'Function' - static get GroupVersion() { - return `${this.Group}/${this.Version}` - } + static readonly GVK = new GroupVersionKind( + 'runtime.laf.dev', + 'v1', + 'Function', + 'functions', + ) constructor(name: string, namespace: string) { - this.apiVersion = CloudFunction.GroupVersion - this.kind = CloudFunction.Kind + this.apiVersion = CloudFunction.GVK.apiVersion + this.kind = CloudFunction.GVK.kind this.metadata = { name, namespace, diff --git a/server/src/functions/functions.service.ts b/server/src/functions/functions.service.ts index 07aff0dd23..4b02fab2a5 100644 --- a/server/src/functions/functions.service.ts +++ b/server/src/functions/functions.service.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common' -import { GetApplicationNamespaceById } from 'src/common/getter' -import { KubernetesService } from 'src/core/kubernetes.service' +import { GetApplicationNamespaceById } from '../common/getter' +import { KubernetesService } from '../core/kubernetes.service' import { CreateFunctionDto } from './dto/create-function.dto' import { UpdateFunctionDto } from './dto/update-function.dto' import { CloudFunction, CloudFunctionList } from './entities/function.entity' @@ -43,10 +43,10 @@ export class FunctionsService { async findAll(appid: string, labelSelector?: string) { const namespace = GetApplicationNamespaceById(appid) const res = await this.k8sClient.customObjectApi.listNamespacedCustomObject( - CloudFunction.Group, - CloudFunction.Version, + CloudFunction.GVK.group, + CloudFunction.GVK.version, namespace, - CloudFunction.PluralName, + CloudFunction.GVK.plural, undefined, undefined, undefined, @@ -67,10 +67,10 @@ export class FunctionsService { try { const res = await this.k8sClient.customObjectApi.getNamespacedCustomObject( - CloudFunction.Group, - CloudFunction.Version, + CloudFunction.GVK.group, + CloudFunction.GVK.version, namespace, - CloudFunction.PluralName, + CloudFunction.GVK.plural, name, ) return res.body as CloudFunction