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,57 +21,9 @@ 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",
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", apiBase: "/api/v1/secrets",
apiPrefix: "/api", apiPrefix: "/api",
apiGroup: "", apiGroup: "",
@ -80,8 +32,39 @@ const tests: KubeApiParseTestData[] = [
resource: "secrets", resource: "secrets",
name: undefined, name: undefined,
namespace: undefined, namespace: undefined,
}], },
["/api/v1/nodes/minikube", { ],
[
"/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", apiBase: "/api/v1/nodes",
apiPrefix: "/api", apiPrefix: "/api",
apiGroup: "", apiGroup: "",
@ -90,8 +73,12 @@ const tests: KubeApiParseTestData[] = [
resource: "nodes", resource: "nodes",
name: "minikube", name: "minikube",
namespace: undefined, namespace: undefined,
}], },
["/api/foo-bar/nodes/minikube", { ],
[
"/api/foo-bar/nodes/minikube",
{
apiBase: "/api/foo-bar/nodes", apiBase: "/api/foo-bar/nodes",
apiPrefix: "/api", apiPrefix: "/api",
apiGroup: "", apiGroup: "",
@ -100,8 +87,12 @@ const tests: KubeApiParseTestData[] = [
resource: "nodes", resource: "nodes",
name: "minikube", name: "minikube",
namespace: undefined, namespace: undefined,
}], },
["/api/v1/namespaces/kube-public", { ],
[
"/api/v1/namespaces/kube-public",
{
apiBase: "/api/v1/namespaces", apiBase: "/api/v1/namespaces",
apiPrefix: "/api", apiPrefix: "/api",
apiGroup: "", apiGroup: "",
@ -110,7 +101,50 @@ const tests: KubeApiParseTestData[] = [
resource: "namespaces", resource: "namespaces",
name: "kube-public", name: "kube-public",
namespace: undefined, 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", "/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");
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; return undefined;
} }
if (left.length === 1 || left.length === 2) { const parsedPath = getParsedPath(path);
[apiVersion, resource] = left;
apiGroup = ""; if (!parsedPath) {
} else if (left.length === 4) { return undefined;
[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 { apiOrApis, apiGroup, namespace, apiVersion, resource, name } =
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/"); 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;