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

Fix IngressApi being registered with a wrong new apiBase (#4485)

This commit is contained in:
Sebastian Malton 2021-12-03 15:19:01 -05:00 committed by GitHub
parent 7973f4bce2
commit 304941d397
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 174 additions and 51 deletions

View File

@ -26,6 +26,12 @@ import { KubeObject } from "../kube-object";
import AbortController from "abort-controller"; import AbortController from "abort-controller";
import { delay } from "../../utils/delay"; import { delay } from "../../utils/delay";
import { PassThrough } from "stream"; import { PassThrough } from "stream";
import { ApiManager, apiManager } from "../api-manager";
import { Ingress, Pod } from "../endpoints";
jest.mock("../api-manager");
const mockApiManager = apiManager as jest.Mocked<ApiManager>;
class TestKubeObject extends KubeObject { class TestKubeObject extends KubeObject {
static kind = "Pod"; static kind = "Pod";
@ -33,7 +39,11 @@ class TestKubeObject extends KubeObject {
static apiBase = "/api/v1/pods"; static apiBase = "/api/v1/pods";
} }
class TestKubeApi extends KubeApi<TestKubeObject> { } class TestKubeApi extends KubeApi<TestKubeObject> {
public async checkPreferredVersion() {
return super.checkPreferredVersion();
}
}
describe("forRemoteCluster", () => { describe("forRemoteCluster", () => {
it("builds api client for KubeObject", async () => { it("builds api client for KubeObject", async () => {
@ -184,6 +194,94 @@ describe("KubeApi", () => {
expect(kubeApi.apiGroup).toEqual("extensions"); expect(kubeApi.apiGroup).toEqual("extensions");
}); });
describe("checkPreferredVersion", () => {
it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred", async () => {
expect.hasAssertions();
const api = new TestKubeApi({
objectConstructor: Ingress,
checkPreferredVersion: true,
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
request: {
get: jest.fn()
.mockImplementationOnce((path: string) => {
expect(path).toBe("/apis/networking.k8s.io/v1");
throw new Error("no");
})
.mockImplementationOnce((path: string) => {
expect(path).toBe("/apis/extensions/v1beta1");
return {
resources: [
{
name: "ingresses",
},
],
};
})
.mockImplementationOnce((path: string) => {
expect(path).toBe("/apis/extensions");
return {
preferredVersion: {
version: "v1beta1",
},
};
}),
} as any,
});
await api.checkPreferredVersion();
expect(api.apiVersionPreferred).toBe("v1beta1");
expect(mockApiManager.registerApi).toBeCalledWith("/apis/extensions/v1beta1/ingresses", expect.anything());
});
it("registers with apiManager if checkPreferredVersion changes apiVersionPreferred with non-grouped apis", async () => {
expect.hasAssertions();
const api = new TestKubeApi({
objectConstructor: Pod,
checkPreferredVersion: true,
fallbackApiBases: ["/api/v1beta1/pods"],
request: {
get: jest.fn()
.mockImplementationOnce((path: string) => {
expect(path).toBe("/api/v1");
throw new Error("no");
})
.mockImplementationOnce((path: string) => {
expect(path).toBe("/api/v1beta1");
return {
resources: [
{
name: "pods",
},
],
};
})
.mockImplementationOnce((path: string) => {
expect(path).toBe("/api");
return {
preferredVersion: {
version: "v1beta1",
},
};
}),
} as any,
});
await api.checkPreferredVersion();
expect(api.apiVersionPreferred).toBe("v1beta1");
expect(mockApiManager.registerApi).toBeCalledWith("/api/v1beta1/pods", expect.anything());
});
});
describe("patch", () => { describe("patch", () => {
let api: TestKubeApi; let api: TestKubeApi;

View File

@ -37,7 +37,7 @@ export interface IKubeApiLinkRef {
apiPrefix?: string; apiPrefix?: string;
apiVersion: string; apiVersion: string;
resource: string; resource: string;
name: string; name?: string;
namespace?: string; namespace?: string;
} }
@ -145,15 +145,18 @@ function _parseKubeApi(path: string): IKubeApiParsed {
}; };
} }
export function createKubeApiURL(ref: IKubeApiLinkRef): string { export function createKubeApiURL({ apiPrefix = "/apis", resource, apiVersion, name, namespace }: IKubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref; const parts = [apiPrefix, apiVersion];
let { namespace } = ref;
if (namespace) { if (namespace) {
namespace = `namespaces/${namespace}`; parts.push("namespaces", namespace);
} }
return [apiPrefix, apiVersion, namespace, resource, name] parts.push(resource);
.filter(v => v)
.join("/"); if (name) {
parts.push(name);
}
return parts.join("/");
} }

View File

@ -38,9 +38,14 @@ import AbortController from "abort-controller";
import { Agent, AgentOptions } from "https"; import { Agent, AgentOptions } from "https";
import type { Patch } from "rfc6902"; import type { Patch } from "rfc6902";
/**
* The options used for creating a `KubeApi`
*/
export interface IKubeApiOptions<T extends KubeObject> { export interface IKubeApiOptions<T extends KubeObject> {
/** /**
* base api-path for listing all resources, e.g. "/api/v1/pods" * base api-path for listing all resources, e.g. "/api/v1/pods"
*
* If not specified then will be the one on the `objectConstructor`
*/ */
apiBase?: string; apiBase?: string;
@ -52,11 +57,33 @@ export interface IKubeApiOptions<T extends KubeObject> {
*/ */
fallbackApiBases?: string[]; fallbackApiBases?: string[];
objectConstructor: KubeObjectConstructor<T>; /**
request?: KubeJsonApi; * If `true` then will check all declared apiBases against the kube api server
isNamespaced?: boolean; * for the first accepted one.
kind?: string; */
checkPreferredVersion?: boolean; checkPreferredVersion?: boolean;
/**
* The constructor for the kube objects returned from the API
*/
objectConstructor: KubeObjectConstructor<T>;
/**
* The api instance to use for making requests
*
* @default apiKube
*/
request?: KubeJsonApi;
/**
* @deprecated should be specified by `objectConstructor`
*/
isNamespaced?: boolean;
/**
* @deprecated should be specified by `objectConstructor`
*/
kind?: string;
} }
export interface IKubeApiQueryParams { export interface IKubeApiQueryParams {
@ -249,11 +276,11 @@ export interface DeleteResourceDescriptor extends ResourceDescriptor {
export class KubeApi<T extends KubeObject> { export class KubeApi<T extends KubeObject> {
readonly kind: string; readonly kind: string;
readonly apiBase: string;
readonly apiPrefix: string;
readonly apiGroup: string;
readonly apiVersion: string; readonly apiVersion: string;
readonly apiVersionPreferred?: string; apiBase: string;
apiPrefix: string;
apiGroup: string;
apiVersionPreferred?: string;
readonly apiResource: string; readonly apiResource: string;
readonly isNamespaced: boolean; readonly isNamespaced: boolean;
@ -264,23 +291,18 @@ export class KubeApi<T extends KubeObject> {
private watchId = 1; private watchId = 1;
constructor(protected options: IKubeApiOptions<T>) { constructor(protected options: IKubeApiOptions<T>) {
const { const { objectConstructor, request, kind, isNamespaced } = options;
objectConstructor,
request = apiKube,
kind = options.objectConstructor?.kind,
isNamespaced = options.objectConstructor?.namespaced,
} = options || {};
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase || objectConstructor.apiBase); const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase || objectConstructor.apiBase);
this.kind = kind; this.options = options;
this.isNamespaced = isNamespaced; this.kind = kind ?? objectConstructor.kind;
this.isNamespaced = isNamespaced ?? objectConstructor.namespaced ?? false;
this.apiBase = apiBase; this.apiBase = apiBase;
this.apiPrefix = apiPrefix; this.apiPrefix = apiPrefix;
this.apiGroup = apiGroup; this.apiGroup = apiGroup;
this.apiVersion = apiVersion; this.apiVersion = apiVersion;
this.apiResource = resource; this.apiResource = resource;
this.request = request; this.request = request ?? apiKube;
this.objectConstructor = objectConstructor; this.objectConstructor = objectConstructor;
this.parseResponse = this.parseResponse.bind(this); this.parseResponse = this.parseResponse.bind(this);
@ -353,21 +375,16 @@ export class KubeApi<T extends KubeObject> {
const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup(); const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup();
// The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them // The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them
Object.defineProperty(this, "apiPrefix", { this.apiPrefix = apiPrefix;
value: apiPrefix, this.apiGroup = apiGroup;
});
Object.defineProperty(this, "apiGroup", {
value: apiGroup,
});
const res = await this.request.get<IKubePreferredVersion>(`${this.apiPrefix}/${this.apiGroup}`); const url = [apiPrefix, apiGroup].filter(Boolean).join("/");
const res = await this.request.get<IKubePreferredVersion>(url);
Object.defineProperty(this, "apiVersionPreferred", { this.apiVersionPreferred = res?.preferredVersion?.version ?? null;
value: res?.preferredVersion?.version ?? null,
});
if (this.apiVersionPreferred) { if (this.apiVersionPreferred) {
Object.defineProperty(this, "apiBase", { value: this.getUrl() }); this.apiBase = this.computeApiBase();
apiManager.registerApi(this.apiBase, this); apiManager.registerApi(this.apiBase, this);
} }
} }
@ -385,7 +402,15 @@ export class KubeApi<T extends KubeObject> {
return this.list(params, { limit: 1 }); return this.list(params, { limit: 1 });
} }
getUrl({ name, namespace = "default" }: Partial<ResourceDescriptor> = {}, query?: Partial<IKubeApiQueryParams>) { private computeApiBase(): string {
return createKubeApiURL({
apiPrefix: this.apiPrefix,
apiVersion: this.apiVersionWithGroup,
resource: this.apiResource,
});
}
getUrl({ name, namespace }: Partial<ResourceDescriptor> = {}, query?: Partial<IKubeApiQueryParams>) {
const resourcePath = createKubeApiURL({ const resourcePath = createKubeApiURL({
apiPrefix: this.apiPrefix, apiPrefix: this.apiPrefix,
apiVersion: this.apiVersionWithGroup, apiVersion: this.apiVersionWithGroup,

View File

@ -57,9 +57,7 @@ export class CrdResources extends React.Component<Props> {
} }
@computed get store() { @computed get store() {
if (!this.crd) return null; return apiManager.getStore(this.crd?.getResourceApiBase());
return apiManager.getStore(this.crd.getResourceApiBase());
} }
render() { render() {

View File

@ -29,15 +29,14 @@ import { CRDResourceStore } from "./crd-resource.store";
import { KubeObject } from "../../../common/k8s-api/kube-object"; import { KubeObject } from "../../../common/k8s-api/kube-object";
function initStore(crd: CustomResourceDefinition) { function initStore(crd: CustomResourceDefinition) {
const apiBase = crd.getResourceApiBase(); const objectConstructor = class extends KubeObject {
const kind = crd.getResourceKind(); static readonly kind = crd.getResourceKind();
const isNamespaced = crd.isNamespaced(); static readonly namespaced = crd.isNamespaced();
const api = apiManager.getApi(apiBase) ?? new KubeApi({ static readonly apiBase = crd.getResourceApiBase();
objectConstructor: KubeObject, };
apiBase,
kind, const api = apiManager.getApi(objectConstructor.apiBase)
isNamespaced, ?? new KubeApi({ objectConstructor });
});
if (!apiManager.getStore(api)) { if (!apiManager.getStore(api)) {
apiManager.registerStore(new CRDResourceStore(api)); apiManager.registerStore(new CRDResourceStore(api));