diff --git a/src/common/k8s-api/__tests__/kube-api.test.ts b/src/common/k8s-api/__tests__/kube-api.test.ts index 10fd82b696..322423d683 100644 --- a/src/common/k8s-api/__tests__/kube-api.test.ts +++ b/src/common/k8s-api/__tests__/kube-api.test.ts @@ -26,6 +26,12 @@ import { KubeObject } from "../kube-object"; import AbortController from "abort-controller"; import { delay } from "../../utils/delay"; import { PassThrough } from "stream"; +import { ApiManager, apiManager } from "../api-manager"; +import { Ingress, Pod } from "../endpoints"; + +jest.mock("../api-manager"); + +const mockApiManager = apiManager as jest.Mocked; class TestKubeObject extends KubeObject { static kind = "Pod"; @@ -33,7 +39,11 @@ class TestKubeObject extends KubeObject { static apiBase = "/api/v1/pods"; } -class TestKubeApi extends KubeApi { } +class TestKubeApi extends KubeApi { + public async checkPreferredVersion() { + return super.checkPreferredVersion(); + } +} describe("forRemoteCluster", () => { it("builds api client for KubeObject", async () => { @@ -184,6 +194,94 @@ describe("KubeApi", () => { expect(kubeApi.apiGroup).toEqual("extensions"); }); + describe("checkPreferredVersion", () => { + it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred", async () => { + expect.hasAssertions(); + + const api = new TestKubeApi({ + objectConstructor: Ingress, + checkPreferredVersion: true, + fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"], + request: { + get: jest.fn() + .mockImplementationOnce((path: string) => { + expect(path).toBe("/apis/networking.k8s.io/v1"); + + throw new Error("no"); + }) + .mockImplementationOnce((path: string) => { + expect(path).toBe("/apis/extensions/v1beta1"); + + return { + resources: [ + { + name: "ingresses", + }, + ], + }; + }) + .mockImplementationOnce((path: string) => { + expect(path).toBe("/apis/extensions"); + + return { + preferredVersion: { + version: "v1beta1", + }, + }; + }), + } as any, + }); + + await api.checkPreferredVersion(); + + expect(api.apiVersionPreferred).toBe("v1beta1"); + expect(mockApiManager.registerApi).toBeCalledWith("/apis/extensions/v1beta1/ingresses", expect.anything()); + }); + + it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred with non-grouped apis", async () => { + expect.hasAssertions(); + + const api = new TestKubeApi({ + objectConstructor: Pod, + checkPreferredVersion: true, + fallbackApiBases: ["/api/v1beta1/pods"], + request: { + get: jest.fn() + .mockImplementationOnce((path: string) => { + expect(path).toBe("/api/v1"); + + throw new Error("no"); + }) + .mockImplementationOnce((path: string) => { + expect(path).toBe("/api/v1beta1"); + + return { + resources: [ + { + name: "pods", + }, + ], + }; + }) + .mockImplementationOnce((path: string) => { + expect(path).toBe("/api"); + + return { + preferredVersion: { + version: "v1beta1", + }, + }; + }), + } as any, + }); + + await api.checkPreferredVersion(); + + expect(api.apiVersionPreferred).toBe("v1beta1"); + expect(mockApiManager.registerApi).toBeCalledWith("/api/v1beta1/pods", expect.anything()); + }); + }); + describe("patch", () => { let api: TestKubeApi; diff --git a/src/common/k8s-api/kube-api-parse.ts b/src/common/k8s-api/kube-api-parse.ts index 57c835ed6b..a5d137be02 100644 --- a/src/common/k8s-api/kube-api-parse.ts +++ b/src/common/k8s-api/kube-api-parse.ts @@ -37,7 +37,7 @@ export interface IKubeApiLinkRef { apiPrefix?: string; apiVersion: string; resource: string; - name: string; + name?: string; namespace?: string; } @@ -145,15 +145,18 @@ function _parseKubeApi(path: string): IKubeApiParsed { }; } -export function createKubeApiURL(ref: IKubeApiLinkRef): string { - const { apiPrefix = "/apis", resource, apiVersion, name } = ref; - let { namespace } = ref; +export function createKubeApiURL({ apiPrefix = "/apis", resource, apiVersion, name, namespace }: IKubeApiLinkRef): string { + const parts = [apiPrefix, apiVersion]; if (namespace) { - namespace = `namespaces/${namespace}`; + parts.push("namespaces", namespace); } - return [apiPrefix, apiVersion, namespace, resource, name] - .filter(v => v) - .join("/"); + parts.push(resource); + + if (name) { + parts.push(name); + } + + return parts.join("/"); } diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index a8b19d81b5..0f31758ab2 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -38,9 +38,14 @@ import AbortController from "abort-controller"; import { Agent, AgentOptions } from "https"; import type { Patch } from "rfc6902"; +/** + * The options used for creating a `KubeApi` + */ export interface IKubeApiOptions { /** * base api-path for listing all resources, e.g. "/api/v1/pods" + * + * If not specified then will be the one on the `objectConstructor` */ apiBase?: string; @@ -52,11 +57,33 @@ export interface IKubeApiOptions { */ fallbackApiBases?: string[]; - objectConstructor: KubeObjectConstructor; - request?: KubeJsonApi; - isNamespaced?: boolean; - kind?: string; + /** + * If `true` then will check all declared apiBases against the kube api server + * for the first accepted one. + */ checkPreferredVersion?: boolean; + + /** + * The constructor for the kube objects returned from the API + */ + objectConstructor: KubeObjectConstructor; + + /** + * The api instance to use for making requests + * + * @default apiKube + */ + request?: KubeJsonApi; + + /** + * @deprecated should be specified by `objectConstructor` + */ + isNamespaced?: boolean; + + /** + * @deprecated should be specified by `objectConstructor` + */ + kind?: string; } export interface IKubeApiQueryParams { @@ -249,11 +276,11 @@ export interface DeleteResourceDescriptor extends ResourceDescriptor { export class KubeApi { readonly kind: string; - readonly apiBase: string; - readonly apiPrefix: string; - readonly apiGroup: string; readonly apiVersion: string; - readonly apiVersionPreferred?: string; + apiBase: string; + apiPrefix: string; + apiGroup: string; + apiVersionPreferred?: string; readonly apiResource: string; readonly isNamespaced: boolean; @@ -264,23 +291,18 @@ export class KubeApi { private watchId = 1; constructor(protected options: IKubeApiOptions) { - const { - objectConstructor, - request = apiKube, - kind = options.objectConstructor?.kind, - isNamespaced = options.objectConstructor?.namespaced, - } = options || {}; - + const { objectConstructor, request, kind, isNamespaced } = options; const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase || objectConstructor.apiBase); - this.kind = kind; - this.isNamespaced = isNamespaced; + this.options = options; + this.kind = kind ?? objectConstructor.kind; + this.isNamespaced = isNamespaced ?? objectConstructor.namespaced ?? false; this.apiBase = apiBase; this.apiPrefix = apiPrefix; this.apiGroup = apiGroup; this.apiVersion = apiVersion; this.apiResource = resource; - this.request = request; + this.request = request ?? apiKube; this.objectConstructor = objectConstructor; this.parseResponse = this.parseResponse.bind(this); @@ -353,21 +375,16 @@ export class KubeApi { const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup(); // The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them - Object.defineProperty(this, "apiPrefix", { - value: apiPrefix, - }); - Object.defineProperty(this, "apiGroup", { - value: apiGroup, - }); + this.apiPrefix = apiPrefix; + this.apiGroup = apiGroup; - const res = await this.request.get(`${this.apiPrefix}/${this.apiGroup}`); + const url = [apiPrefix, apiGroup].filter(Boolean).join("/"); + const res = await this.request.get(url); - Object.defineProperty(this, "apiVersionPreferred", { - value: res?.preferredVersion?.version ?? null, - }); + this.apiVersionPreferred = res?.preferredVersion?.version ?? null; if (this.apiVersionPreferred) { - Object.defineProperty(this, "apiBase", { value: this.getUrl() }); + this.apiBase = this.computeApiBase(); apiManager.registerApi(this.apiBase, this); } } @@ -385,7 +402,15 @@ export class KubeApi { return this.list(params, { limit: 1 }); } - getUrl({ name, namespace = "default" }: Partial = {}, query?: Partial) { + private computeApiBase(): string { + return createKubeApiURL({ + apiPrefix: this.apiPrefix, + apiVersion: this.apiVersionWithGroup, + resource: this.apiResource, + }); + } + + getUrl({ name, namespace }: Partial = {}, query?: Partial) { const resourcePath = createKubeApiURL({ apiPrefix: this.apiPrefix, apiVersion: this.apiVersionWithGroup, diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index d0eb4779c7..1c8f55aaab 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -57,9 +57,7 @@ export class CrdResources extends React.Component { } @computed get store() { - if (!this.crd) return null; - - return apiManager.getStore(this.crd.getResourceApiBase()); + return apiManager.getStore(this.crd?.getResourceApiBase()); } render() { diff --git a/src/renderer/components/+custom-resources/crd.store.ts b/src/renderer/components/+custom-resources/crd.store.ts index 83c8b5cdb6..2cdcbee55b 100644 --- a/src/renderer/components/+custom-resources/crd.store.ts +++ b/src/renderer/components/+custom-resources/crd.store.ts @@ -29,15 +29,14 @@ import { CRDResourceStore } from "./crd-resource.store"; import { KubeObject } from "../../../common/k8s-api/kube-object"; function initStore(crd: CustomResourceDefinition) { - const apiBase = crd.getResourceApiBase(); - const kind = crd.getResourceKind(); - const isNamespaced = crd.isNamespaced(); - const api = apiManager.getApi(apiBase) ?? new KubeApi({ - objectConstructor: KubeObject, - apiBase, - kind, - isNamespaced, - }); + const objectConstructor = class extends KubeObject { + static readonly kind = crd.getResourceKind(); + static readonly namespaced = crd.isNamespaced(); + static readonly apiBase = crd.getResourceApiBase(); + }; + + const api = apiManager.getApi(objectConstructor.apiBase) + ?? new KubeApi({ objectConstructor }); if (!apiManager.getStore(api)) { apiManager.registerStore(new CRDResourceStore(api));