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

chore: Change parseKubeApi into a no-throw version

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-04-20 15:48:38 -04:00
parent 8dce32153f
commit 2f21ba51d1
10 changed files with 113 additions and 80 deletions

View File

@ -114,7 +114,7 @@ const tests: KubeApiParseTestData[] = [
}], }],
]; ];
const throwtests = [ const invalidTests = [
undefined, undefined,
"", "",
"ajklsmh", "ajklsmh",
@ -125,7 +125,7 @@ describe("parseApi unit tests", () => {
expect(parseKubeApi(url)).toStrictEqual(expected); expect(parseKubeApi(url)).toStrictEqual(expected);
}); });
it.each(throwtests)("testing %j should throw", (url) => { it.each(invalidTests)("testing %j should throw", (url) => {
expect(() => parseKubeApi(url as never)).toThrowError("invalid apiPath"); expect(parseKubeApi(url as never)).toBe(undefined);
}); });
}); });

View File

@ -74,9 +74,13 @@ export class ApiManager {
return iter.find(this.apis.values(), pathOrCallback); return iter.find(this.apis.values(), pathOrCallback);
} }
const { apiBase } = parseKubeApi(pathOrCallback); const parsedApi = parseKubeApi(pathOrCallback);
return this.apis.get(apiBase); if (!parsedApi) {
return undefined;
}
return this.apis.get(parsedApi.apiBase);
} }
getApiByKind(kind: string, apiVersion: string) { getApiByKind(kind: string, apiVersion: string) {
@ -141,9 +145,10 @@ export class ApiManager {
} }
const { apiBase } = typeof apiOrBase === "string" const { apiBase } = typeof apiOrBase === "string"
? parseKubeApi(apiOrBase) ? parseKubeApi(apiOrBase) ?? {}
: apiOrBase; : apiOrBase;
const api = this.getApi(apiBase);
const api = apiBase && this.getApi(apiBase);
if (!api) { if (!api) {
return undefined; return undefined;

View File

@ -22,16 +22,16 @@ export interface IKubeApiParsed extends IKubeApiLinkRef {
apiVersionWithGroup: string; apiVersionWithGroup: string;
} }
export function parseKubeApi(path: string): IKubeApiParsed { export function parseKubeApi(path: string): IKubeApiParsed | undefined {
const apiPath = new URL(path, "https://localhost").pathname; const apiPath = new URL(path, "https://localhost").pathname;
const [, prefix, ...parts] = apiPath.split("/"); const [, prefix, ...parts] = apiPath.split("/");
const apiPrefix = `/${prefix}`; const apiPrefix = `/${prefix}`;
const [left, right, namespaced] = array.split(parts, "namespaces"); const [left, right, namespaced] = array.split(parts, "namespaces");
let apiGroup!: string; let apiGroup: string;
let apiVersion!: string; let apiVersion: string | undefined;
let namespace!: string; let namespace: string | undefined;
let resource!: string; let resource: string;
let name!: string; let name: string | undefined;
if (namespaced) { if (namespaced) {
switch (right.length) { switch (right.length) {
@ -46,26 +46,21 @@ export function parseKubeApi(path: string): IKubeApiParsed {
break; break;
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion let rest: string[];
apiVersion = left.pop()!;
apiGroup = left.join("/"); [apiVersion, ...rest] = left;
apiGroup = rest.join("/");
} else { } else {
switch (left.length) { if (left.length === 0) {
case 0: return undefined;
throw new Error(`invalid apiPath: ${apiPath}`); }
case 4:
[apiGroup, apiVersion, resource, name] = left; if (left.length === 1 || left.length === 2) {
break; [apiVersion, resource] = left;
case 2: apiGroup = "";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion } else if (left.length === 4) {
resource = left.pop()!; [apiGroup, apiVersion, resource, name] = left;
// fallthrough } else {
case 1:
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
apiVersion = left.pop()!;
apiGroup = "";
break;
default:
/** /**
* Given that * Given that
* - `apiVersion` is `GROUP/VERSION` and * - `apiVersion` is `GROUP/VERSION` and
@ -82,15 +77,14 @@ export function parseKubeApi(path: string): IKubeApiParsed {
* 3. otherwise assume apiVersion <- left[0] * 3. otherwise assume apiVersion <- left[0]
* 4. always resource, name <- left[(0 or 1)+1..] * 4. always resource, name <- left[(0 or 1)+1..]
*/ */
if (left[0].includes(".") || left[1].match(/^v[0-9]/)) { if (left[0].includes(".") || left[1].match(/^v[0-9]/)) {
[apiGroup, apiVersion] = left; [apiGroup, apiVersion] = left;
resource = left.slice(2).join("/"); resource = left.slice(2).join("/");
} else { } else {
apiGroup = ""; apiGroup = "";
apiVersion = left[0]; apiVersion = left[0];
[resource, name] = left.slice(1); [resource, name] = left.slice(1);
} }
break;
} }
} }
@ -98,7 +92,7 @@ export function parseKubeApi(path: string): IKubeApiParsed {
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/"); const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
if (!apiBase) { if (!apiBase) {
throw new Error(`invalid apiPath: ${apiPath}`); return undefined;
} }
return { return {
@ -110,7 +104,7 @@ export function parseKubeApi(path: string): IKubeApiParsed {
} }
function isIKubeApiParsed(refOrParsed: IKubeApiLinkRef | IKubeApiParsed): refOrParsed is IKubeApiParsed { function isIKubeApiParsed(refOrParsed: IKubeApiLinkRef | IKubeApiParsed): refOrParsed is IKubeApiParsed {
return "apiGroup" in refOrParsed; return "apiGroup" in refOrParsed && !!refOrParsed.apiGroup;
} }
export function createKubeApiURL(linkRef: IKubeApiLinkRef): string; export function createKubeApiURL(linkRef: IKubeApiLinkRef): string;

View File

@ -264,12 +264,16 @@ export class KubeApi<
allowedUsableVersions, allowedUsableVersions,
} = opts; } = opts;
assert(fullApiPathname, "apiBase MUST be provied either via KubeApiOptions.apiBase or KubeApiOptions.objectConstructor.apiBase"); assert(fullApiPathname, "apiBase MUST be provided either via KubeApiOptions.apiBase or KubeApiOptions.objectConstructor.apiBase");
assert(request, "request MUST be provided if not in a cluster page frame context"); assert(request, "request MUST be provided if not in a cluster page frame context");
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(fullApiPathname); const parsedApi = parseKubeApi(fullApiPathname);
assert(kind, "kind MUST be provied either via KubeApiOptions.kind or KubeApiOptions.objectConstructor.kind"); assert(parsedApi, "apiBase MUST be a valid kube api pathname");
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parsedApi;
assert(kind, "kind MUST be provided either via KubeApiOptions.kind or KubeApiOptions.objectConstructor.kind");
assert(apiPrefix, "apiBase MUST be parsable as a kubeApi selfLink style string"); assert(apiPrefix, "apiBase MUST be parsable as a kubeApi selfLink style string");
this.doCheckPreferredVersion = doCheckPreferredVersion; this.doCheckPreferredVersion = doCheckPreferredVersion;
@ -308,24 +312,26 @@ export class KubeApi<
const apiBases = new Set(rawApiBases); const apiBases = new Set(rawApiBases);
for (const apiUrl of apiBases) { for (const apiUrl of apiBases) {
try { const parsedApi = parseKubeApi(apiUrl);
const { apiPrefix, apiGroup, resource } = parseKubeApi(apiUrl);
const list = await this.request.get(`${apiPrefix}/${apiGroup}`) as KubeApiResourceVersionList;
const resourceVersions = getOrderedVersions(list, this.allowedUsableVersions?.[apiGroup]);
for (const resourceVersion of resourceVersions) { if (!parsedApi) {
const { resources } = await this.request.get(`${apiPrefix}/${resourceVersion.groupVersion}`) as KubeApiResourceList; continue;
}
if (resources.some(({ name }) => name === resource)) { const { apiPrefix, apiGroup, resource } = parsedApi;
return { const list = await this.request.get(`${apiPrefix}/${apiGroup}`) as KubeApiResourceVersionList;
apiPrefix, const resourceVersions = getOrderedVersions(list, this.allowedUsableVersions?.[apiGroup]);
apiGroup,
apiVersionPreferred: resourceVersion.version, for (const resourceVersion of resourceVersions) {
}; const { resources } = await this.request.get(`${apiPrefix}/${resourceVersion.groupVersion}`) as KubeApiResourceList;
}
if (resources.some(({ name }) => name === resource)) {
return {
apiPrefix,
apiGroup,
apiVersionPreferred: resourceVersion.version,
};
} }
} catch (error) {
// Exception is ignored as we can try the next url
} }
} }

View File

@ -7,18 +7,18 @@ import { parseKubeApi } from "../kube-api-parse";
import { kubeApiInjectionToken } from "./kube-api-injection-token"; import { kubeApiInjectionToken } from "./kube-api-injection-token";
import type { KubeApi } from "../kube-api"; import type { KubeApi } from "../kube-api";
export type GetKubeApiFromPath = (apiPath: string) => KubeApi | undefined;
const getKubeApiFromPathInjectable = getInjectable({ const getKubeApiFromPathInjectable = getInjectable({
id: "get-kube-api-from-path", id: "get-kube-api-from-path",
instantiate: (di) => { instantiate: (di): GetKubeApiFromPath => {
const kubeApis = di.injectMany(kubeApiInjectionToken); const kubeApis = di.injectMany(kubeApiInjectionToken);
return (apiPath: string) => { return (apiPath: string) => {
const parsed = parseKubeApi(apiPath); const parsed = parseKubeApi(apiPath);
const kubeApi = kubeApis.find((api) => api.apiBase === parsed.apiBase); return kubeApis.find((api) => api.apiBase === parsed?.apiBase);
return (kubeApi as KubeApi) || undefined;
}; };
}, },
}); });

View File

@ -324,7 +324,11 @@ export class KubeObjectStore<
@action @action
async loadFromPath(resourcePath: string) { async loadFromPath(resourcePath: string) {
const { namespace, name } = parseKubeApi(resourcePath); const parsedApi = parseKubeApi(resourcePath);
assert(parsedApi, "resourcePath must be a valid kube api");
const { namespace, name } = parsedApi;
assert(name, "name must be part of resourcePath"); assert(name, "name must be part of resourcePath");

View File

@ -4,6 +4,7 @@
*/ */
import { getInjectable, createInstantiationTargetDecorator, instantiationDecoratorToken } from "@ogre-tools/injectable"; import { getInjectable, createInstantiationTargetDecorator, instantiationDecoratorToken } from "@ogre-tools/injectable";
import { pick } from "lodash"; import { pick } from "lodash";
import { inspect } from "util";
import { parseKubeApi } from "../../../common/k8s-api/kube-api-parse"; import { parseKubeApi } from "../../../common/k8s-api/kube-api-parse";
import showDetailsInjectable from "../../../renderer/components/kube-detail-params/show-details.injectable"; import showDetailsInjectable from "../../../renderer/components/kube-detail-params/show-details.injectable";
import emitTelemetryInjectable from "./emit-telemetry.injectable"; import emitTelemetryInjectable from "./emit-telemetry.injectable";
@ -21,13 +22,15 @@ const telemetryDecoratorForShowDetailsInjectable = getInjectable({
? { ? {
action: "open", action: "open",
...(() => { ...(() => {
try { const parsedApi = parseKubeApi(args[0]);
return {
resource: pick(parseKubeApi(args[0]), "apiPrefix", "apiVersion", "apiGroup", "namespace", "resource", "name"), if (!parsedApi) {
}; return { error: `invalid apiPath: ${inspect(args[0])}` };
} catch (error) {
return { error: `${error}` };
} }
return {
resource: pick(parsedApi, "apiPrefix", "apiVersion", "apiGroup", "namespace", "resource", "name"),
};
})(), })(),
} }
: { : {

View File

@ -21,7 +21,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
showDetails = di.inject(showDetailsInjectable); showDetails = di.inject(showDetailsInjectable);
}); });
it("when showDetails is called with no selflink (ie closing) should emit telemetry with param indicating closing the drawer", () => { it("when showDetails is called with no selfLink (ie closing) should emit telemetry with param indicating closing the drawer", () => {
showDetails(undefined); showDetails(undefined);
expect(emitAppEventMock).toBeCalledWith({ expect(emitAppEventMock).toBeCalledWith({
@ -34,7 +34,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
}); });
}); });
it("when showDetails is called with empty selflink (ie closing) should emit telemetry with param indicating closing the drawer", () => { it("when showDetails is called with empty selfLink (ie closing) should emit telemetry with param indicating closing the drawer", () => {
showDetails(""); showDetails("");
expect(emitAppEventMock).toBeCalledWith({ expect(emitAppEventMock).toBeCalledWith({
@ -47,7 +47,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
}); });
}); });
it("when showDetails is called with valid selflink should emit telemetry with param indicating opening the drawer with that resource", () => { it("when showDetails is called with valid selfLink should emit telemetry with param indicating opening the drawer with that resource", () => {
showDetails("/api/v1/namespaces/default/pods/some-name"); showDetails("/api/v1/namespaces/default/pods/some-name");
expect(emitAppEventMock).toBeCalledWith({ expect(emitAppEventMock).toBeCalledWith({
@ -68,7 +68,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
}); });
}); });
it("when showDetails is called with invalid selflink should emit telemetry with param indicating opening the drawer but also show error", () => { it("when showDetails is called with invalid selfLink should emit telemetry with param indicating opening the drawer but also show error", () => {
showDetails("some-non-self-link-value"); showDetails("some-non-self-link-value");
expect(emitAppEventMock).toBeCalledWith({ expect(emitAppEventMock).toBeCalledWith({
@ -77,7 +77,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
name: "show-details", name: "show-details",
params: { params: {
action: "open", action: "open",
error: "Error: invalid apiPath: /some-non-self-link-value", error: "invalid apiPath: 'some-non-self-link-value'",
}, },
}); });
}); });

View File

@ -20,7 +20,7 @@ const callForResourceInjectable = getInjectable({
return async (apiPath: string) => { return async (apiPath: string) => {
const parsed = parseKubeApi(apiPath); const parsed = parseKubeApi(apiPath);
if (!parsed.name) { if (!parsed?.name) {
return { callWasSuccessful: false, error: "Invalid API path" }; return { callWasSuccessful: false, error: "Invalid API path" };
} }

View File

@ -59,11 +59,17 @@ interface Dependencies {
readonly tabId: string; readonly tabId: string;
} }
function getEditSelfLinkFor(object: RawKubeObject): string { function getEditSelfLinkFor(object: RawKubeObject): string | undefined {
const lensVersionLabel = object.metadata.labels?.[EditResourceLabelName]; const lensVersionLabel = object.metadata.labels?.[EditResourceLabelName];
if (lensVersionLabel) { if (lensVersionLabel) {
const { apiVersionWithGroup, ...parsedApi } = parseKubeApi(object.metadata.selfLink); const parsedKubeApi = parseKubeApi(object.metadata.selfLink);
if (!parsedKubeApi) {
return undefined;
}
const { apiVersionWithGroup, ...parsedApi } = parsedKubeApi;
parsedApi.apiVersion = lensVersionLabel; parsedApi.apiVersion = lensVersionLabel;
@ -139,6 +145,10 @@ export class EditResourceModel {
if (result?.response?.metadata.labels?.[EditResourceLabelName]) { if (result?.response?.metadata.labels?.[EditResourceLabelName]) {
const parsed = parseKubeApi(this.selfLink); const parsed = parseKubeApi(this.selfLink);
if (!parsed) {
return void this.dependencies.showErrorNotification(`Object's selfLink is invalid: "${this.selfLink}"`);
}
parsed.apiVersion = result.response.metadata.labels[EditResourceLabelName]; parsed.apiVersion = result.response.metadata.labels[EditResourceLabelName];
result = await this.dependencies.callForResource(createKubeApiURL(parsed)); result = await this.dependencies.callForResource(createKubeApiURL(parsed));
@ -186,6 +196,17 @@ export class EditResourceModel {
const patches = createPatch(firstVersion, currentVersion); const patches = createPatch(firstVersion, currentVersion);
const selfLink = getEditSelfLinkFor(currentVersion); const selfLink = getEditSelfLinkFor(currentVersion);
if (!selfLink) {
this.dependencies.showErrorNotification((
<p>
{`Cannot save resource, unknown selfLink: "${currentVersion.metadata.selfLink}"`}
</p>
));
return null;
}
const result = await this.dependencies.callForPatchResource(this.resource, patches); const result = await this.dependencies.callForPatchResource(this.resource, patches);
if (!result.callWasSuccessful) { if (!result.callWasSuccessful) {