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 { delay } from "../../utils/delay";
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 {
static kind = "Pod";
@ -33,7 +39,11 @@ class TestKubeObject extends KubeObject {
static apiBase = "/api/v1/pods";
}
class TestKubeApi extends KubeApi<TestKubeObject> { }
class TestKubeApi extends KubeApi<TestKubeObject> {
public async checkPreferredVersion() {
return super.checkPreferredVersion();
}
}
describe("forRemoteCluster", () => {
it("builds api client for KubeObject", async () => {
@ -184,6 +194,94 @@ describe("KubeApi", () => {
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", () => {
let api: TestKubeApi;

View File

@ -37,7 +37,7 @@ export interface IKubeApiLinkRef {
apiPrefix?: string;
apiVersion: string;
resource: string;
name: string;
name?: string;
namespace?: string;
}
@ -145,15 +145,18 @@ function _parseKubeApi(path: string): IKubeApiParsed {
};
}
export function createKubeApiURL(ref: IKubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
let { namespace } = ref;
export function createKubeApiURL({ apiPrefix = "/apis", resource, apiVersion, name, namespace }: IKubeApiLinkRef): string {
const parts = [apiPrefix, apiVersion];
if (namespace) {
namespace = `namespaces/${namespace}`;
parts.push("namespaces", namespace);
}
return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => v)
.join("/");
parts.push(resource);
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 type { Patch } from "rfc6902";
/**
* The options used for creating a `KubeApi`
*/
export interface IKubeApiOptions<T extends KubeObject> {
/**
* 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;
@ -52,11 +57,33 @@ export interface IKubeApiOptions<T extends KubeObject> {
*/
fallbackApiBases?: string[];
objectConstructor: KubeObjectConstructor<T>;
request?: KubeJsonApi;
isNamespaced?: boolean;
kind?: string;
/**
* If `true` then will check all declared apiBases against the kube api server
* for the first accepted one.
*/
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 {
@ -249,11 +276,11 @@ export interface DeleteResourceDescriptor extends ResourceDescriptor {
export class KubeApi<T extends KubeObject> {
readonly kind: string;
readonly apiBase: string;
readonly apiPrefix: string;
readonly apiGroup: string;
readonly apiVersion: string;
readonly apiVersionPreferred?: string;
apiBase: string;
apiPrefix: string;
apiGroup: string;
apiVersionPreferred?: string;
readonly apiResource: string;
readonly isNamespaced: boolean;
@ -264,23 +291,18 @@ export class KubeApi<T extends KubeObject> {
private watchId = 1;
constructor(protected options: IKubeApiOptions<T>) {
const {
objectConstructor,
request = apiKube,
kind = options.objectConstructor?.kind,
isNamespaced = options.objectConstructor?.namespaced,
} = options || {};
const { objectConstructor, request, kind, isNamespaced } = options;
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase || objectConstructor.apiBase);
this.kind = kind;
this.isNamespaced = isNamespaced;
this.options = options;
this.kind = kind ?? objectConstructor.kind;
this.isNamespaced = isNamespaced ?? objectConstructor.namespaced ?? false;
this.apiBase = apiBase;
this.apiPrefix = apiPrefix;
this.apiGroup = apiGroup;
this.apiVersion = apiVersion;
this.apiResource = resource;
this.request = request;
this.request = request ?? apiKube;
this.objectConstructor = objectConstructor;
this.parseResponse = this.parseResponse.bind(this);
@ -353,21 +375,16 @@ export class KubeApi<T extends KubeObject> {
const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup();
// The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them
Object.defineProperty(this, "apiPrefix", {
value: apiPrefix,
});
Object.defineProperty(this, "apiGroup", {
value: apiGroup,
});
this.apiPrefix = apiPrefix;
this.apiGroup = 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", {
value: res?.preferredVersion?.version ?? null,
});
this.apiVersionPreferred = res?.preferredVersion?.version ?? null;
if (this.apiVersionPreferred) {
Object.defineProperty(this, "apiBase", { value: this.getUrl() });
this.apiBase = this.computeApiBase();
apiManager.registerApi(this.apiBase, this);
}
}
@ -385,7 +402,15 @@ export class KubeApi<T extends KubeObject> {
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({
apiPrefix: this.apiPrefix,
apiVersion: this.apiVersionWithGroup,

View File

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

View File

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