1
0
mirror of https://github.com/lensapp/lens.git synced 2024-09-11 09:25:26 +03: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];
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);
});
});

View File

@ -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(
/^\/(?<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;