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:
parent
8dce32153f
commit
2f21ba51d1
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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");
|
||||||
|
|
||||||
|
|||||||
@ -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"),
|
||||||
|
};
|
||||||
})(),
|
})(),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
|||||||
@ -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'",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user