From 26e031fc586990e6df65d4f17d70ff0892b58894 Mon Sep 17 00:00:00 2001 From: Trevor Nichols Date: Fri, 4 Sep 2020 16:39:31 +1000 Subject: [PATCH] Update CRD api to use preferred version and implement v1 differences (#718) * Update CRD api to use preferred version and implement v1 differences Signed-off-by: Trevor Nichols * Fix unit test failure Signed-off-by: Trevor Nichols * Register alternate API base URL if preferred version changes. Signed-off-by: Trevor Nichols * Adjust for validation moving to the version.schema Signed-off-by: Trevor Nichols --- src/main/logger.ts | 2 +- src/renderer/api/endpoints/crd.api.ts | 54 ++++++++++-------- src/renderer/api/kube-api-versioned.ts | 56 +++++++++++++++++++ .../+custom-resources/crd-details.tsx | 4 +- .../crd-resource-details.tsx | 2 +- .../+custom-resources/crd-resources.tsx | 8 +-- 6 files changed, 95 insertions(+), 31 deletions(-) create mode 100644 src/renderer/api/kube-api-versioned.ts diff --git a/src/main/logger.ts b/src/main/logger.ts index 0d720b65ac..4068aaacd2 100644 --- a/src/main/logger.ts +++ b/src/main/logger.ts @@ -11,7 +11,7 @@ const fileOptions: winston.transports.FileTransportOptions = { handleExceptions: false, level: isDebugging ? "debug" : "info", filename: "lens.log", - dirname: (app || remote.app).getPath("logs"), + dirname: (app ?? remote?.app)?.getPath("logs"), maxsize: 16 * 1024, maxFiles: 16, tailable: true, diff --git a/src/renderer/api/endpoints/crd.api.ts b/src/renderer/api/endpoints/crd.api.ts index 829719a3cb..ebacf832ca 100644 --- a/src/renderer/api/endpoints/crd.api.ts +++ b/src/renderer/api/endpoints/crd.api.ts @@ -1,13 +1,28 @@ import { KubeObject } from "../kube-object"; -import { KubeApi } from "../kube-api"; +import { VersionedKubeApi } from "../kube-api-versioned"; import { crdResourcesURL } from "../../components/+custom-resources/crd.route"; +type AdditionalPrinterColumnsCommon = { + name: string; + type: "integer" | "number" | "string" | "boolean" | "date"; + priority: number; + description: string; +} + +type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & { + jsonPath: string; +} + +type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & { + JSONPath: string; +} + export class CustomResourceDefinition extends KubeObject { static kind = "CustomResourceDefinition"; spec: { group: string; - version: string; + version?: string; // deprecated in v1 api names: { plural: string; singular: string; @@ -20,18 +35,14 @@ export class CustomResourceDefinition extends KubeObject { name: string; served: boolean; storage: boolean; + schema?: unknown; // required in v1 but not present in v1beta + additionalPrinterColumns?: AdditionalPrinterColumnsV1[] }[]; conversion: { strategy?: string; webhook?: any; }; - additionalPrinterColumns?: { - name: string; - type: "integer" | "number" | "string" | "boolean" | "date"; - priority: number; - description: string; - JSONPath: string; - }[]; + additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1 } status: { conditions: { @@ -61,8 +72,8 @@ export class CustomResourceDefinition extends KubeObject { } getResourceApiBase() { - const { version, group } = this.spec; - return `/apis/${group}/${version}/${this.getPluralName()}` + const { group } = this.spec; + return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}` } getPluralName() { @@ -87,7 +98,8 @@ export class CustomResourceDefinition extends KubeObject { } getVersion() { - return this.spec.version; + // v1 has removed the spec.version property, if it is present it must match the first version + return this.spec.versions[0]?.name ?? this.spec.version; } isNamespaced() { @@ -107,14 +119,16 @@ export class CustomResourceDefinition extends KubeObject { } getPrinterColumns(ignorePriority = true) { - const columns = this.spec.additionalPrinterColumns || []; + const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns + ?? this.spec.additionalPrinterColumns?.map(({JSONPath, ...rest}) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape + ?? []; return columns .filter(column => column.name != "Age") .filter(column => ignorePriority ? true : !column.priority); } getValidation() { - return JSON.stringify(this.spec.validation, null, 2); + return JSON.stringify(this.spec.validation ?? this.spec.versions?.[0]?.schema, null, 2); } getConditions() { @@ -130,16 +144,10 @@ export class CustomResourceDefinition extends KubeObject { } } -export const crdBetaApi = new KubeApi({ - kind: CustomResourceDefinition.kind, - apiBase: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions", - isNamespaced: false, - objectConstructor: CustomResourceDefinition, -}); - -export const crdApi = new KubeApi({ +export const crdApi = new VersionedKubeApi({ kind: CustomResourceDefinition.kind, apiBase: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions", isNamespaced: false, - objectConstructor: CustomResourceDefinition, + objectConstructor: CustomResourceDefinition }); + diff --git a/src/renderer/api/kube-api-versioned.ts b/src/renderer/api/kube-api-versioned.ts new file mode 100644 index 0000000000..f6aa94c089 --- /dev/null +++ b/src/renderer/api/kube-api-versioned.ts @@ -0,0 +1,56 @@ +import { stringify } from "querystring"; +import { KubeObject } from "./kube-object"; +import { createKubeApiURL } from "./kube-api-parse"; +import { KubeApi, IKubeApiQueryParams, IKubeApiOptions } from "./kube-api"; +import { apiManager } from "./api-manager"; + +export class VersionedKubeApi extends KubeApi { + private preferredVersion?: string; + + constructor(opts: IKubeApiOptions) { + super(opts); + + this.getPreferredVersion().then(() => { + if (this.apiBase != opts.apiBase) + apiManager.registerApi(this.apiBase, this); + }); + } + + // override this property to make read-write + apiBase: string + + async getPreferredVersion() { + if (this.preferredVersion) return; + + const apiGroupVersion = await this.request.get<{ preferredVersion?: { version: string; }; }>(`${this.apiPrefix}/${this.apiGroup}`); + + if (!apiGroupVersion?.preferredVersion) return; + + this.preferredVersion = apiGroupVersion.preferredVersion.version; + + // update apiBase + this.apiBase = this.getUrl(); + } + + async list({ namespace = "" } = {}, query?: IKubeApiQueryParams): Promise { + await this.getPreferredVersion(); + return await super.list({namespace}, query); + } + async get({ name = "", namespace = "default" } = {}, query?: IKubeApiQueryParams): Promise { + await this.getPreferredVersion(); + return super.get({ name, namespace }, query); + } + + getUrl({ name = "", namespace = "" } = {}, query?: Partial) { + const { apiPrefix, apiGroup, apiVersion, apiResource, preferredVersion, isNamespaced } = this; + + const resourcePath = createKubeApiURL({ + apiPrefix: apiPrefix, + apiVersion: `${apiGroup}/${preferredVersion ?? apiVersion}`, + resource: apiResource, + namespace: isNamespaced ? namespace : undefined, + name: name, + }); + return resourcePath + (query ? `?` + stringify(query) : ""); + } +} diff --git a/src/renderer/components/+custom-resources/crd-details.tsx b/src/renderer/components/+custom-resources/crd-details.tsx index fdf8dd3968..3270868732 100644 --- a/src/renderer/components/+custom-resources/crd-details.tsx +++ b/src/renderer/components/+custom-resources/crd-details.tsx @@ -102,13 +102,13 @@ export class CRDDetails extends React.Component { { printerColumns.map((column, index) => { - const { name, type, JSONPath } = column; + const { name, type, jsonPath } = column; return ( {name} {type} - + ) diff --git a/src/renderer/components/+custom-resources/crd-resource-details.tsx b/src/renderer/components/+custom-resources/crd-resource-details.tsx index 71b517ad5a..201d5e6e4a 100644 --- a/src/renderer/components/+custom-resources/crd-resource-details.tsx +++ b/src/renderer/components/+custom-resources/crd-resource-details.tsx @@ -57,7 +57,7 @@ export class CrdResourceDetails extends React.Component { {extraColumns.map(column => { const { name } = column; - const value = jsonPath.query(object, column.JSONPath.slice(1)); + const value = jsonPath.query(object, (column.jsonPath).slice(1)); return ( diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index 81a10cfd6f..036b0bec20 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -57,7 +57,7 @@ export class CrdResources extends React.Component { [sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp, } extraColumns.forEach(column => { - sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.query(item, column.JSONPath.slice(1)) + sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.query(item, column.jsonPath.slice(1)) }) // todo: merge extra columns and other params to predefined view const { List } = apiManager.getViews(crd.getResourceApiBase()); @@ -88,9 +88,9 @@ export class CrdResources extends React.Component { renderTableContents={(crdInstance: KubeObject) => [ crdInstance.getName(), isNamespaced && crdInstance.getNs(), - ...extraColumns.map(column => - jsonPath.query(crdInstance, column.JSONPath.slice(1)) - ), + ...extraColumns.map(column => { + return jsonPath.query(crdInstance, (column.jsonPath).slice(1)) + }), crdInstance.getAge(), ]} renderItemMenu={(item: KubeObject) => {