diff --git a/dashboard/client/api/__test__/parseAPI.test.ts b/dashboard/client/api/__test__/parseAPI.test.ts index 82c0c59f75..b748098622 100644 --- a/dashboard/client/api/__test__/parseAPI.test.ts +++ b/dashboard/client/api/__test__/parseAPI.test.ts @@ -45,10 +45,62 @@ const tests: ParseAPITest[] = [ namespace: undefined, }, }, + { + url: "/api/v1/namespaces", + expected: { + apiBase: "/api/v1/namespaces", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "namespaces", + name: undefined, + namespace: undefined, + }, + }, + { + url: "/api/v1/secrets", + expected: { + apiBase: "/api/v1/secrets", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "secrets", + name: undefined, + namespace: undefined, + }, + }, + { + url: "/api/v1/nodes/minikube", + expected: { + apiBase: "/api/v1/nodes", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "nodes", + name: "minikube", + namespace: undefined, + }, + }, + { + url: "/api/foo-bar/nodes/minikube", + expected: { + apiBase: "/api/foo-bar/nodes", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "foo-bar", + apiVersionWithGroup: "foo-bar", + resource: "nodes", + name: "minikube", + namespace: undefined, + }, + }, ]; jest.mock('../kube-watch-api.ts', () => 'KubeWatchApi'); -describe("parseAPI unit tests", () => { +describe("parseApi unit tests", () => { for (const i in tests) { const { url: tUrl, expected:tExpect} = tests[i]; test(`test #${parseInt(i)+1}`, () => { diff --git a/dashboard/client/api/api-manager.ts b/dashboard/client/api/api-manager.ts index 3bfb0caebd..89e1b1cf34 100644 --- a/dashboard/client/api/api-manager.ts +++ b/dashboard/client/api/api-manager.ts @@ -18,23 +18,17 @@ export class ApiManager { private views = observable.map(); getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) { - const apis = this.apis; if (typeof pathOrCallback === "string") { - let api = apis.get(pathOrCallback); - if (!api) { - const { apiBase } = KubeApi.parseApi(pathOrCallback); - api = apis.get(apiBase); - } - return api; - } - else { - return Array.from(apis.values()).find(pathOrCallback); + return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase); } + + return Array.from(this.apis.values()).find(pathOrCallback); } registerApi(apiBase: string, api: KubeApi) { - if (this.apis.has(apiBase)) return; - this.apis.set(apiBase, api); + if (!this.apis.has(apiBase)) { + this.apis.set(apiBase, api); + } } protected resolveApi(api: string | KubeApi): KubeApi { diff --git a/dashboard/client/api/kube-api.ts b/dashboard/client/api/kube-api.ts index be8016c68c..b77a538d14 100644 --- a/dashboard/client/api/kube-api.ts +++ b/dashboard/client/api/kube-api.ts @@ -8,6 +8,7 @@ import { apiKube } from "./index"; import { kubeWatchApi } from "./kube-watch-api"; import { apiManager } from "./api-manager"; import { split } from "../utils/arrays"; +import isEqual from "lodash/isEqual"; export interface IKubeApiOptions { kind: string; // resource type within api-group, e.g. "Namespace" @@ -42,23 +43,65 @@ export interface IKubeApiLinkBase extends IKubeApiLinkRef { export class KubeApi { static parseApi(apiPath = ""): IKubeApiLinkBase { apiPath = new URL(apiPath, location.origin).pathname; + const [, prefix, ...parts] = apiPath.split("/"); const apiPrefix = `/${prefix}`; - const [left, right, found] = split(parts, "namespaces"); + const [left, right, namespaced] = split(parts, "namespaces"); let apiGroup, apiVersion, namespace, resource, name; - if (found) { - if (left.length == 0) { - throw new Error(`invalid apiPath: ${apiPath}`) + if (namespaced) { + switch (right.length) { + case 0: + resource = "namespaces"; // special case this due to `split` removing namespaces + break; + case 1: + resource = right[0]; + break; + default: + [namespace, resource, name] = right; + break; } apiVersion = left.pop(); apiGroup = left.join("/"); - [namespace, resource, name] = right; } else { - [apiGroup, apiVersion, resource] = left; + switch (left.length) { + case 2: + resource = left.pop(); + case 1: + apiVersion = left.pop(); + apiGroup = ""; + break; + default: + /** + * Given that + * - `apiVersion` is `GROUP/VERSION` and + * - `VERSION` is `DNS_LABEL` which is /^[a-z0-9]((-[a-z0-9])|[a-z0-9])*$/i + * where length <= 63 + * - `GROUP` is /^D(\.D)*$/ where D is `DNS_LABEL` and length <= 253 + * + * There is no well defined selection from an array of items that were + * seperated by '/' + * + * Solution is to create a huristic. Namely: + * 1. if '.' in left[0] then apiGroup <- left[0] + * 2. if left[1] matches /^v[0-9]/ then apiGroup, apiVersion <- left[0], left[1] + * 3. otherwise assume apiVersion <- left[0] + * 4. always resource, name <- left[(0 or 1)+1..] + */ + if (left[0].includes('.') || left[1].match(/^v[0-9]/)) { + [apiGroup, apiVersion] = left; + resource = left.slice(2).join("/") + } else { + apiGroup = ""; + apiVersion = left[0]; + [resource, name] = left.slice(1) + } + break; + } } + const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/"); const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");