diff --git a/src/renderer/api/__test__/parseAPI.test.ts b/src/renderer/api/__test__/parseAPI.test.ts index adfb325789..abb9a9573d 100644 --- a/src/renderer/api/__test__/parseAPI.test.ts +++ b/src/renderer/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/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index afe4755019..22735c3c09 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -19,23 +19,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/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts index 3cff112e9a..235e6ec3c5 100644 --- a/src/renderer/api/kube-api.ts +++ b/src/renderer/api/kube-api.ts @@ -42,21 +42,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] = splitArray(parts, "namespaces"); + const [left, right, namespaced] = splitArray(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 = "namespaces", name] = right; // fix: "resource" is empty when "/api/v1/namespaces" } 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("/"); @@ -154,7 +198,7 @@ export class KubeApi { if (KubeObject.isJsonApiData(data)) { return new KubeObjectConstructor(data); } - + // process items list response if (KubeObject.isJsonApiDataList(data)) { const { apiVersion, items, metadata } = data; @@ -166,12 +210,12 @@ export class KubeApi { ...item, })) } - + // custom apis might return array for list response, e.g. users, groups, etc. if (Array.isArray(data)) { return data.map(data => new KubeObjectConstructor(data)); } - + return data; } @@ -189,7 +233,7 @@ export class KubeApi { async create({ name = "", namespace = "default" } = {}, data?: Partial): Promise { const apiUrl = this.getUrl({ namespace }); - + return this.request .post(apiUrl, { data: merge({