From befbe62e63511064a50b3fb3bd7e3c0509f98ffa Mon Sep 17 00:00:00 2001 From: Iku-turso Date: Tue, 9 May 2023 13:37:59 +0300 Subject: [PATCH] chore: Refactor legacy code to use pattern matching Also add missing unit tests to cover more. Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso --- .../k8s-api/__tests__/kube-api-parse.test.ts | 273 ++++++++++++------ .../core/src/common/k8s-api/kube-api-parse.ts | 146 +++++----- 2 files changed, 251 insertions(+), 168 deletions(-) diff --git a/packages/core/src/common/k8s-api/__tests__/kube-api-parse.test.ts b/packages/core/src/common/k8s-api/__tests__/kube-api-parse.test.ts index 38dfacd656..f0f0635d21 100644 --- a/packages/core/src/common/k8s-api/__tests__/kube-api-parse.test.ts +++ b/packages/core/src/common/k8s-api/__tests__/kube-api-parse.test.ts @@ -21,96 +21,130 @@ import { parseKubeApi } from "../kube-api-parse"; type KubeApiParseTestData = [string, IKubeApiParsed]; const tests: KubeApiParseTestData[] = [ - ["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", { - apiBase: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions", - apiPrefix: "/apis", - apiGroup: "apiextensions.k8s.io", - apiVersion: "v1beta1", - apiVersionWithGroup: "apiextensions.k8s.io/v1beta1", - namespace: undefined, - resource: "customresourcedefinitions", - name: "prometheuses.monitoring.coreos.com", - }], - ["/api/v1/namespaces/kube-system/pods/coredns-6955765f44-v8p27", { - apiBase: "/api/v1/pods", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "v1", - apiVersionWithGroup: "v1", - namespace: "kube-system", - resource: "pods", - name: "coredns-6955765f44-v8p27", - }], - ["/apis/stable.example.com/foo1/crontabs", { - apiBase: "/apis/stable.example.com/foo1/crontabs", - apiPrefix: "/apis", - apiGroup: "stable.example.com", - apiVersion: "foo1", - apiVersionWithGroup: "stable.example.com/foo1", - resource: "crontabs", - name: undefined, - namespace: undefined, - }], - ["/apis/cluster.k8s.io/v1alpha1/clusters", { - apiBase: "/apis/cluster.k8s.io/v1alpha1/clusters", - apiPrefix: "/apis", - apiGroup: "cluster.k8s.io", - apiVersion: "v1alpha1", - apiVersionWithGroup: "cluster.k8s.io/v1alpha1", - resource: "clusters", - name: undefined, - namespace: undefined, - }], - ["/api/v1/namespaces", { - apiBase: "/api/v1/namespaces", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "v1", - apiVersionWithGroup: "v1", - resource: "namespaces", - name: undefined, - namespace: undefined, - }], - ["/api/v1/secrets", { - apiBase: "/api/v1/secrets", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "v1", - apiVersionWithGroup: "v1", - resource: "secrets", - name: undefined, - namespace: undefined, - }], - ["/api/v1/nodes/minikube", { - apiBase: "/api/v1/nodes", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "v1", - apiVersionWithGroup: "v1", - resource: "nodes", - name: "minikube", - namespace: undefined, - }], - ["/api/foo-bar/nodes/minikube", { - apiBase: "/api/foo-bar/nodes", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "foo-bar", - apiVersionWithGroup: "foo-bar", - resource: "nodes", - name: "minikube", - namespace: undefined, - }], - ["/api/v1/namespaces/kube-public", { - apiBase: "/api/v1/namespaces", - apiPrefix: "/api", - apiGroup: "", - apiVersion: "v1", - apiVersionWithGroup: "v1", - resource: "namespaces", - name: "kube-public", - namespace: undefined, - }], + [ + "http://some-irrelevant-domain/api/v1/secrets?some-irrelevant-parameter=some-irrelevant-value", + { + apiBase: "/api/v1/secrets", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "secrets", + name: undefined, + namespace: undefined, + }, + ], + [ + "/api/v1/secrets", + { + apiBase: "/api/v1/secrets", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "secrets", + name: undefined, + namespace: undefined, + }, + ], + + [ + "/api/v1/namespaces", + { + apiBase: "/api/v1/namespaces", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "namespaces", + name: undefined, + namespace: undefined, + }, + ], + + [ + "/api/v1/nodes/minikube", + { + apiBase: "/api/v1/nodes", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "nodes", + name: "minikube", + namespace: undefined, + }, + ], + + [ + "/api/foo-bar/nodes/minikube", + { + apiBase: "/api/foo-bar/nodes", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "foo-bar", + apiVersionWithGroup: "foo-bar", + resource: "nodes", + name: "minikube", + namespace: undefined, + }, + ], + + [ + "/api/v1/namespaces/kube-public", + { + apiBase: "/api/v1/namespaces", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + resource: "namespaces", + name: "kube-public", + namespace: undefined, + }, + ], + + [ + "/apis/stable.example.com/foo1/crontabs", + { + apiBase: "/apis/stable.example.com/foo1/crontabs", + apiPrefix: "/apis", + apiGroup: "stable.example.com", + apiVersion: "foo1", + apiVersionWithGroup: "stable.example.com/foo1", + resource: "crontabs", + name: undefined, + namespace: undefined, + }, + ], + + [ + "/apis/cluster.k8s.io/v1alpha1/clusters", + { + apiBase: "/apis/cluster.k8s.io/v1alpha1/clusters", + apiPrefix: "/apis", + apiGroup: "cluster.k8s.io", + apiVersion: "v1alpha1", + apiVersionWithGroup: "cluster.k8s.io/v1alpha1", + resource: "clusters", + name: undefined, + namespace: undefined, + }, + ], + + [ + "/api/v1/namespaces/kube-system/pods/coredns-6955765f44-v8p27", + { + apiBase: "/api/v1/pods", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + namespace: "kube-system", + resource: "pods", + name: "coredns-6955765f44-v8p27", + }, + ], [ "/apis/apps/v1/namespaces/default/deployments/some-deployment", @@ -125,20 +159,73 @@ const tests: KubeApiParseTestData[] = [ resource: "deployments", }, ], + + [ + "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", + { + apiBase: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions", + apiPrefix: "/apis", + apiGroup: "apiextensions.k8s.io", + apiVersion: "v1beta1", + apiVersionWithGroup: "apiextensions.k8s.io/v1beta1", + namespace: undefined, + resource: "customresourcedefinitions", + name: "prometheuses.monitoring.coreos.com", + }, + ], + + [ + "/api/v1/namespaces/kube-system/pods", + { + apiBase: "/api/v1/pods", + apiPrefix: "/api", + apiGroup: "", + apiVersion: "v1", + apiVersionWithGroup: "v1", + namespace: "kube-system", + resource: "pods", + name: undefined, + }, + ], + + [ + "/apis/cluster.k8s.io/v1/namespaces/kube-system/pods", + { + apiBase: "/apis/cluster.k8s.io/v1/pods", + apiPrefix: "/apis", + apiGroup: "cluster.k8s.io", + apiVersion: "v1", + apiVersionWithGroup: "cluster.k8s.io/v1", + namespace: "kube-system", + resource: "pods", + name: undefined, + }, + ], ]; const invalidTests = [ undefined, "", - "ajklsmh", + "some-invalid-path", + "//apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", + "/apis//v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", + "/apis/apiextensions.k8s.io//customresourcedefinitions/prometheuses.monitoring.coreos.com", + "/apis/apiextensions.k8s.io/v1beta1//prometheuses.monitoring.coreos.com", + "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/", + + "//v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", + "/api//v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", + "/api//customresourcedefinitions/prometheuses.monitoring.coreos.com", + "/api/v1beta1//prometheuses.monitoring.coreos.com", + "/api/v1beta1/customresourcedefinitions/", ]; describe("parseApi unit tests", () => { - it.each(tests)("testing %j", (url, expected) => { + it.each(tests)(`given path %j, parses as expected`, (url, expected) => { expect(parseKubeApi(url)).toStrictEqual(expected); }); - it.each(invalidTests)("testing %j should throw", (url) => { + it.each(invalidTests)(`given path %j, parses as undefined`, (url) => { expect(parseKubeApi(url as never)).toBe(undefined); }); }); diff --git a/packages/core/src/common/k8s-api/kube-api-parse.ts b/packages/core/src/common/k8s-api/kube-api-parse.ts index e0233b2984..b58215eef1 100644 --- a/packages/core/src/common/k8s-api/kube-api-parse.ts +++ b/packages/core/src/common/k8s-api/kube-api-parse.ts @@ -3,9 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -// Parse kube-api path and get api-version, group, etc. +import { pipeline } from "@ogre-tools/fp"; +import { compact, join } from "lodash/fp"; +import { getMatchFor } from "./get-match-for"; +import { prepend } from "./prepend"; -import { array } from "@k8slens/utilities"; +// Parse kube-api path and get api-version, group, etc. export interface IKubeApiLinkRef { apiPrefix?: string; @@ -22,92 +25,46 @@ export interface IKubeApiParsed extends IKubeApiLinkRef { apiVersionWithGroup: string; } -export function parseKubeApi(path: string): IKubeApiParsed | undefined { - const apiPath = new URL(path, "https://localhost").pathname; - const [, prefix, ...parts] = apiPath.split("/"); - const apiPrefix = `/${prefix}`; - const [left, right, namespaced] = array.split(parts, "namespaces"); - let apiGroup: string; - let apiVersion: string | undefined; - let namespace: string | undefined; - let resource: string; - let name: string | undefined; - - if (namespaced) { - switch (right.length) { - case 1: - name = right[0]; - // fallthrough - case 0: - resource = "namespaces"; // special case this due to `split` removing namespaces - break; - default: - [namespace, resource, name] = right; - break; - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - apiVersion = left.at(-1)!; - const rest = left.slice(0, -1); - - apiGroup = rest.join("/"); - } else { - if (left.length === 0) { - return undefined; - } - - if (left.length === 1 || left.length === 2) { - [apiVersion, resource] = left; - apiGroup = ""; - } else if (left.length === 4) { - [apiGroup, apiVersion, resource, name] = left; - } else { - /** - * 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 - * separated by '/' - * - * Solution is to create a heuristic. 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); - } - } +export function parseKubeApi( + path: string | undefined, +): IKubeApiParsed | undefined { + if (!path) { + return undefined; } - const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/"); - const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/"); + const parsedPath = getParsedPath(path); + + if (!parsedPath) { + return undefined; + } + + const { apiOrApis, apiGroup, namespace, apiVersion, resource, name } = + parsedPath; return { - apiBase, - apiPrefix, apiGroup, - apiVersion, apiVersionWithGroup, - namespace, resource, name, + apiBase: getApiBase(apiOrApis, apiGroup, apiVersion, resource), + apiPrefix: getApiPrefix(apiOrApis), + apiGroup: getApiGroup(apiGroup), + apiVersion, + apiVersionWithGroup: getApiVersionWithGroup(apiGroup, apiVersion), + namespace, + resource, + name, }; } -function isIKubeApiParsed(refOrParsed: IKubeApiLinkRef | IKubeApiParsed): refOrParsed is IKubeApiParsed { +function isIKubeApiParsed( + refOrParsed: IKubeApiLinkRef | IKubeApiParsed, +): refOrParsed is IKubeApiParsed { return "apiGroup" in refOrParsed && !!refOrParsed.apiGroup; } export function createKubeApiURL(linkRef: IKubeApiLinkRef): string; export function createKubeApiURL(linkParsed: IKubeApiParsed): string; -export function createKubeApiURL(ref: IKubeApiLinkRef | IKubeApiParsed): string { +export function createKubeApiURL( + ref: IKubeApiLinkRef | IKubeApiParsed, +): string { if (isIKubeApiParsed(ref)) { return createKubeApiURL({ apiPrefix: ref.apiPrefix, @@ -133,3 +90,42 @@ export function createKubeApiURL(ref: IKubeApiLinkRef | IKubeApiParsed): string return parts.join("/"); } + +const getKubeApiPathMatch = getMatchFor( + /^\/(?apis)\/(?[^/]+?)\/(?[^/]+?)\/namespaces\/(?[^/]+?)\/(?[^/]+?)\/(?[^/]+?)$/, + /^\/(?apis)\/(?[^/]+?)\/(?[^/]+?)\/namespaces\/(?[^/]+?)\/(?[^/]+?)$/, + /^\/(?api)\/(?[^/]+?)\/namespaces\/(?[^/]+?)\/(?[^/]+?)\/(?[^/]+?)$/, + /^\/(?apis)\/(?[^/]+?)\/(?[^/]+?)\/(?[^/]+?)\/(?[^/]+?)$/, + /^\/(?api)\/(?[^/]+?)\/namespaces\/(?[^/]+?)\/(?[^/]+?)$/, + /^\/(?apis)\/(?[^/]+?)\/(?[^/]+?)\/(?[^/]+?)$/, + /^\/(?api)\/(?[^/]+?)\/(?[^/]+?)\/(?[^/]+?)$/, + /^\/(?api)\/(?[^/]+?)\/(?[^/]+?)$/, +); + +const getParsedPath = (path: string) => + pipeline(path, withoutDomainAddressOrParameters, getKubeApiPathMatch, (match) => match?.groups); + +const joinTruthy = (delimiter: string) => (toBeJoined: string[]) => + pipeline(toBeJoined, compact, join(delimiter)); + +const getApiBase = ( + apiOrApis: string, + apiGroup: string, + apiVersion: string, + resource: string, +) => + pipeline( + [apiOrApis, apiGroup, apiVersion, resource], + joinTruthy("/"), + prepend("/"), + ); + +const getApiPrefix = prepend("/"); + +const getApiVersionWithGroup = (apiGroup: string, apiVersion: string) => + joinTruthy("/")([apiGroup, apiVersion]); + +const getApiGroup = (apiGroup: string) => apiGroup || ""; + +const withoutDomainAddressOrParameters = (path: string) => + new URL(path, "http://irrelevant").pathname;