1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

chore: Refactor legacy code to use pattern matching

Also add missing unit tests to cover more.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
Iku-turso 2023-05-09 13:37:59 +03:00 committed by Janne Savolainen
parent bba0759d27
commit befbe62e63
2 changed files with 251 additions and 168 deletions

View File

@ -21,96 +21,130 @@ import { parseKubeApi } from "../kube-api-parse";
type KubeApiParseTestData = [string, IKubeApiParsed]; type KubeApiParseTestData = [string, IKubeApiParsed];
const tests: KubeApiParseTestData[] = [ const tests: KubeApiParseTestData[] = [
["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", { [
apiBase: "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions", "http://some-irrelevant-domain/api/v1/secrets?some-irrelevant-parameter=some-irrelevant-value",
apiPrefix: "/apis", {
apiGroup: "apiextensions.k8s.io", apiBase: "/api/v1/secrets",
apiVersion: "v1beta1", apiPrefix: "/api",
apiVersionWithGroup: "apiextensions.k8s.io/v1beta1", apiGroup: "",
namespace: undefined, apiVersion: "v1",
resource: "customresourcedefinitions", apiVersionWithGroup: "v1",
name: "prometheuses.monitoring.coreos.com", resource: "secrets",
}], name: undefined,
["/api/v1/namespaces/kube-system/pods/coredns-6955765f44-v8p27", { namespace: undefined,
apiBase: "/api/v1/pods", },
apiPrefix: "/api", ],
apiGroup: "", [
apiVersion: "v1", "/api/v1/secrets",
apiVersionWithGroup: "v1", {
namespace: "kube-system", apiBase: "/api/v1/secrets",
resource: "pods", apiPrefix: "/api",
name: "coredns-6955765f44-v8p27", apiGroup: "",
}], apiVersion: "v1",
["/apis/stable.example.com/foo1/crontabs", { apiVersionWithGroup: "v1",
apiBase: "/apis/stable.example.com/foo1/crontabs", resource: "secrets",
apiPrefix: "/apis", name: undefined,
apiGroup: "stable.example.com", namespace: undefined,
apiVersion: "foo1", },
apiVersionWithGroup: "stable.example.com/foo1", ],
resource: "crontabs",
name: undefined, [
namespace: undefined, "/api/v1/namespaces",
}], {
["/apis/cluster.k8s.io/v1alpha1/clusters", { apiBase: "/api/v1/namespaces",
apiBase: "/apis/cluster.k8s.io/v1alpha1/clusters", apiPrefix: "/api",
apiPrefix: "/apis", apiGroup: "",
apiGroup: "cluster.k8s.io", apiVersion: "v1",
apiVersion: "v1alpha1", apiVersionWithGroup: "v1",
apiVersionWithGroup: "cluster.k8s.io/v1alpha1", resource: "namespaces",
resource: "clusters", name: undefined,
name: undefined, namespace: undefined,
namespace: undefined, },
}], ],
["/api/v1/namespaces", {
apiBase: "/api/v1/namespaces", [
apiPrefix: "/api", "/api/v1/nodes/minikube",
apiGroup: "", {
apiVersion: "v1", apiBase: "/api/v1/nodes",
apiVersionWithGroup: "v1", apiPrefix: "/api",
resource: "namespaces", apiGroup: "",
name: undefined, apiVersion: "v1",
namespace: undefined, apiVersionWithGroup: "v1",
}], resource: "nodes",
["/api/v1/secrets", { name: "minikube",
apiBase: "/api/v1/secrets", namespace: undefined,
apiPrefix: "/api", },
apiGroup: "", ],
apiVersion: "v1",
apiVersionWithGroup: "v1", [
resource: "secrets", "/api/foo-bar/nodes/minikube",
name: undefined, {
namespace: undefined, apiBase: "/api/foo-bar/nodes",
}], apiPrefix: "/api",
["/api/v1/nodes/minikube", { apiGroup: "",
apiBase: "/api/v1/nodes", apiVersion: "foo-bar",
apiPrefix: "/api", apiVersionWithGroup: "foo-bar",
apiGroup: "", resource: "nodes",
apiVersion: "v1", name: "minikube",
apiVersionWithGroup: "v1", namespace: undefined,
resource: "nodes", },
name: "minikube", ],
namespace: undefined,
}], [
["/api/foo-bar/nodes/minikube", { "/api/v1/namespaces/kube-public",
apiBase: "/api/foo-bar/nodes", {
apiPrefix: "/api", apiBase: "/api/v1/namespaces",
apiGroup: "", apiPrefix: "/api",
apiVersion: "foo-bar", apiGroup: "",
apiVersionWithGroup: "foo-bar", apiVersion: "v1",
resource: "nodes", apiVersionWithGroup: "v1",
name: "minikube", resource: "namespaces",
namespace: undefined, name: "kube-public",
}], namespace: undefined,
["/api/v1/namespaces/kube-public", { },
apiBase: "/api/v1/namespaces", ],
apiPrefix: "/api",
apiGroup: "", [
apiVersion: "v1", "/apis/stable.example.com/foo1/crontabs",
apiVersionWithGroup: "v1", {
resource: "namespaces", apiBase: "/apis/stable.example.com/foo1/crontabs",
name: "kube-public", apiPrefix: "/apis",
namespace: undefined, 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", "/apis/apps/v1/namespaces/default/deployments/some-deployment",
@ -125,20 +159,73 @@ const tests: KubeApiParseTestData[] = [
resource: "deployments", 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 = [ const invalidTests = [
undefined, 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", () => { 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); 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); expect(parseKubeApi(url as never)).toBe(undefined);
}); });
}); });

View File

@ -3,9 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { export interface IKubeApiLinkRef {
apiPrefix?: string; apiPrefix?: string;
@ -22,92 +25,46 @@ export interface IKubeApiParsed extends IKubeApiLinkRef {
apiVersionWithGroup: string; apiVersionWithGroup: string;
} }
export function parseKubeApi(path: string): IKubeApiParsed | undefined { export function parseKubeApi(
const apiPath = new URL(path, "https://localhost").pathname; path: string | undefined,
const [, prefix, ...parts] = apiPath.split("/"); ): IKubeApiParsed | undefined {
const apiPrefix = `/${prefix}`; if (!path) {
const [left, right, namespaced] = array.split(parts, "namespaces"); return undefined;
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);
}
}
} }
const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/"); const parsedPath = getParsedPath(path);
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
if (!parsedPath) {
return undefined;
}
const { apiOrApis, apiGroup, namespace, apiVersion, resource, name } =
parsedPath;
return { return {
apiBase, apiBase: getApiBase(apiOrApis, apiGroup, apiVersion, resource),
apiPrefix, apiGroup, apiPrefix: getApiPrefix(apiOrApis),
apiVersion, apiVersionWithGroup, apiGroup: getApiGroup(apiGroup),
namespace, resource, name, 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; return "apiGroup" in refOrParsed && !!refOrParsed.apiGroup;
} }
export function createKubeApiURL(linkRef: IKubeApiLinkRef): string; export function createKubeApiURL(linkRef: IKubeApiLinkRef): string;
export function createKubeApiURL(linkParsed: IKubeApiParsed): string; export function createKubeApiURL(linkParsed: IKubeApiParsed): string;
export function createKubeApiURL(ref: IKubeApiLinkRef | IKubeApiParsed): string { export function createKubeApiURL(
ref: IKubeApiLinkRef | IKubeApiParsed,
): string {
if (isIKubeApiParsed(ref)) { if (isIKubeApiParsed(ref)) {
return createKubeApiURL({ return createKubeApiURL({
apiPrefix: ref.apiPrefix, apiPrefix: ref.apiPrefix,
@ -133,3 +90,42 @@ export function createKubeApiURL(ref: IKubeApiLinkRef | IKubeApiParsed): string
return parts.join("/"); return parts.join("/");
} }
const getKubeApiPathMatch = getMatchFor(
/^\/(?<apiOrApis>apis)\/(?<apiGroup>[^/]+?)\/(?<apiVersion>[^/]+?)\/namespaces\/(?<namespace>[^/]+?)\/(?<resource>[^/]+?)\/(?<name>[^/]+?)$/,
/^\/(?<apiOrApis>apis)\/(?<apiGroup>[^/]+?)\/(?<apiVersion>[^/]+?)\/namespaces\/(?<namespace>[^/]+?)\/(?<resource>[^/]+?)$/,
/^\/(?<apiOrApis>api)\/(?<apiVersion>[^/]+?)\/namespaces\/(?<namespace>[^/]+?)\/(?<resource>[^/]+?)\/(?<name>[^/]+?)$/,
/^\/(?<apiOrApis>apis)\/(?<apiGroup>[^/]+?)\/(?<apiVersion>[^/]+?)\/(?<resource>[^/]+?)\/(?<name>[^/]+?)$/,
/^\/(?<apiOrApis>api)\/(?<apiVersion>[^/]+?)\/namespaces\/(?<namespace>[^/]+?)\/(?<resource>[^/]+?)$/,
/^\/(?<apiOrApis>apis)\/(?<apiGroup>[^/]+?)\/(?<apiVersion>[^/]+?)\/(?<resource>[^/]+?)$/,
/^\/(?<apiOrApis>api)\/(?<apiVersion>[^/]+?)\/(?<resource>[^/]+?)\/(?<name>[^/]+?)$/,
/^\/(?<apiOrApis>api)\/(?<apiVersion>[^/]+?)\/(?<resource>[^/]+?)$/,
);
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;