mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add ability for KubeApi to filter server versions (#6908)
* Add ability for KubeApi to filter server versions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update error message Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
b2c440354c
commit
3436dfd46f
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import type { ApiManager } from "../api-manager";
|
import type { ApiManager } from "../api-manager";
|
||||||
import type { IngressApi } from "../endpoints";
|
import type { IngressApi } from "../endpoints";
|
||||||
import { Ingress } from "../endpoints";
|
import { Ingress, HorizontalPodAutoscalerApi } from "../endpoints";
|
||||||
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
||||||
import type { Fetch } from "../../fetch/fetch.injectable";
|
import type { Fetch } from "../../fetch/fetch.injectable";
|
||||||
import fetchInjectable from "../../fetch/fetch.injectable";
|
import fetchInjectable from "../../fetch/fetch.injectable";
|
||||||
@ -21,6 +21,8 @@ import directoryForKubeConfigsInjectable from "../../app-paths/directory-for-kub
|
|||||||
import apiManagerInjectable from "../api-manager/manager.injectable";
|
import apiManagerInjectable from "../api-manager/manager.injectable";
|
||||||
import type { DiContainer } from "@ogre-tools/injectable";
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
import ingressApiInjectable from "../endpoints/ingress.api.injectable";
|
import ingressApiInjectable from "../endpoints/ingress.api.injectable";
|
||||||
|
import loggerInjectable from "../../logger.injectable";
|
||||||
|
import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
|
||||||
|
|
||||||
describe("KubeApi", () => {
|
describe("KubeApi", () => {
|
||||||
let fetchMock: AsyncFnMock<Fetch>;
|
let fetchMock: AsyncFnMock<Fetch>;
|
||||||
@ -705,4 +707,125 @@ describe("KubeApi", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("on first call to HorizontalPodAutoscalerApi.get()", () => {
|
||||||
|
let horizontalPodAutoscalerApi: HorizontalPodAutoscalerApi;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
horizontalPodAutoscalerApi = new HorizontalPodAutoscalerApi({
|
||||||
|
logger: di.inject(loggerInjectable),
|
||||||
|
maybeKubeApi: di.inject(maybeKubeApiInjectable),
|
||||||
|
}, {
|
||||||
|
allowedUsableVersions: {
|
||||||
|
autoscaling: [
|
||||||
|
"v2",
|
||||||
|
"v2beta2",
|
||||||
|
"v2beta1",
|
||||||
|
"v1",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
horizontalPodAutoscalerApi.get({
|
||||||
|
name: "foo",
|
||||||
|
namespace: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is needed because of how JS promises work
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requests version list from the api group from the initial apiBase", () => {
|
||||||
|
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||||
|
"https://127.0.0.1:12345/api-kube/apis/autoscaling",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "get",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the version list from the api group resolves with preferredVersion in allowed version", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await fetchMock.resolveSpecific(
|
||||||
|
["https://127.0.0.1:12345/api-kube/apis/autoscaling"],
|
||||||
|
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/autoscaling", JSON.stringify({
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "APIGroup",
|
||||||
|
name: "autoscaling",
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
groupVersion: "autoscaling/v1",
|
||||||
|
version: "v1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupVersion: "autoscaling/v1beta1",
|
||||||
|
version: "v2beta1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
preferredVersion: {
|
||||||
|
groupVersion: "autoscaling/v1",
|
||||||
|
version: "v1",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requests resources from the preferred version api group from the initial apiBase", () => {
|
||||||
|
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||||
|
"https://127.0.0.1:12345/api-kube/apis/autoscaling/v1",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "get",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the version list from the api group resolves with preferredVersion not allowed version", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await fetchMock.resolveSpecific(
|
||||||
|
["https://127.0.0.1:12345/api-kube/apis/autoscaling"],
|
||||||
|
createMockResponseFromString("https://127.0.0.1:12345/api-kube/apis/autoscaling", JSON.stringify({
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "APIGroup",
|
||||||
|
name: "autoscaling",
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
groupVersion: "autoscaling/v2",
|
||||||
|
version: "v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupVersion: "autoscaling/v2beta1",
|
||||||
|
version: "v2beta1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupVersion: "autoscaling/v3",
|
||||||
|
version: "v3",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
preferredVersion: {
|
||||||
|
groupVersion: "autoscaling/v3",
|
||||||
|
version: "v3",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requests resources from the non preferred version from the initial apiBase", () => {
|
||||||
|
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||||
|
"https://127.0.0.1:12345/api-kube/apis/autoscaling/v2",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "get",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -371,6 +371,14 @@ export class HorizontalPodAutoscaler extends KubeObject<
|
|||||||
export class HorizontalPodAutoscalerApi extends KubeApi<HorizontalPodAutoscaler> {
|
export class HorizontalPodAutoscalerApi extends KubeApi<HorizontalPodAutoscaler> {
|
||||||
constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) {
|
||||||
super(deps, {
|
super(deps, {
|
||||||
|
allowedUsableVersions: {
|
||||||
|
autoscaling: [
|
||||||
|
"v2",
|
||||||
|
"v2beta2",
|
||||||
|
"v2beta1",
|
||||||
|
"v1",
|
||||||
|
],
|
||||||
|
},
|
||||||
...opts ?? {},
|
...opts ?? {},
|
||||||
objectConstructor: HorizontalPodAutoscaler,
|
objectConstructor: HorizontalPodAutoscaler,
|
||||||
checkPreferredVersion: true,
|
checkPreferredVersion: true,
|
||||||
|
|||||||
@ -58,14 +58,32 @@ export interface DerivedKubeApiOptions {
|
|||||||
/**
|
/**
|
||||||
* If the API uses a different API endpoint (e.g. apiBase) depending on the cluster version,
|
* If the API uses a different API endpoint (e.g. apiBase) depending on the cluster version,
|
||||||
* fallback API bases can be listed individually.
|
* fallback API bases can be listed individually.
|
||||||
|
*
|
||||||
* The first (existing) API base is used in the requests, if apiBase is not found.
|
* The first (existing) API base is used in the requests, if apiBase is not found.
|
||||||
* This option only has effect if checkPreferredVersion is true.
|
*
|
||||||
|
* This option only has effect if {@link DerivedKubeApiOptions.checkPreferredVersion} is `true`.
|
||||||
*/
|
*/
|
||||||
fallbackApiBases?: string[];
|
fallbackApiBases?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This option is useful for protecting against newer versions on the same apiBase from being
|
||||||
|
* used. So that if a certain type only supports `v1`, or `v2` of some kind and then the `v3`
|
||||||
|
* version becomes the `preferredVersion` on the server but still has `v2` then the `v2` version
|
||||||
|
* will be used instead.
|
||||||
|
*
|
||||||
|
* This can help to prevent crashes in the future if the shape of a kind sufficently changes.
|
||||||
|
*
|
||||||
|
* The order is important. It should be sorted and the first entry should be the most preferable.
|
||||||
|
*
|
||||||
|
* This option only has effect if {@link DerivedKubeApiOptions.checkPreferredVersion} is `true`
|
||||||
|
*/
|
||||||
|
allowedUsableVersions?: Partial<Record<string, [string, ...string[]]>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If `true` then will check all declared apiBases against the kube api server
|
* If `true` then will check all declared apiBases against the kube api server
|
||||||
* for the first accepted one.
|
* for the first accepted one.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
*/
|
*/
|
||||||
checkPreferredVersion?: boolean;
|
checkPreferredVersion?: boolean;
|
||||||
|
|
||||||
@ -133,10 +151,10 @@ export interface KubeApiResourceVersionList {
|
|||||||
|
|
||||||
const not = <T>(fn: (val: T) => boolean) => (val: T) => !(fn(val));
|
const not = <T>(fn: (val: T) => boolean) => (val: T) => !(fn(val));
|
||||||
|
|
||||||
const getOrderedVersions = (list: KubeApiResourceVersionList): KubeApiResourceVersion[] => [
|
const getOrderedVersions = (list: KubeApiResourceVersionList, allowedUsableVersions: string[] | undefined): KubeApiResourceVersion[] => [
|
||||||
list.preferredVersion,
|
list.preferredVersion,
|
||||||
...list.versions.filter(not(matches(list.preferredVersion))),
|
...list.versions.filter(not(matches(list.preferredVersion))),
|
||||||
];
|
].filter(({ version }) => !allowedUsableVersions || allowedUsableVersions.includes(version));
|
||||||
|
|
||||||
export type PropagationPolicy = undefined | "Orphan" | "Foreground" | "Background";
|
export type PropagationPolicy = undefined | "Orphan" | "Foreground" | "Background";
|
||||||
|
|
||||||
@ -233,6 +251,7 @@ export class KubeApi<
|
|||||||
protected readonly doCheckPreferredVersion: boolean;
|
protected readonly doCheckPreferredVersion: boolean;
|
||||||
protected readonly fullApiPathname: string;
|
protected readonly fullApiPathname: string;
|
||||||
protected readonly fallbackApiBases: string[] | undefined;
|
protected readonly fallbackApiBases: string[] | undefined;
|
||||||
|
protected readonly allowedUsableVersions: Partial<Record<string, string[]>> | undefined;
|
||||||
|
|
||||||
constructor(protected readonly dependencies: KubeApiDependencies, opts: KubeApiOptions<Object, Data>) {
|
constructor(protected readonly dependencies: KubeApiDependencies, opts: KubeApiOptions<Object, Data>) {
|
||||||
const {
|
const {
|
||||||
@ -243,6 +262,7 @@ export class KubeApi<
|
|||||||
apiBase: fullApiPathname = objectConstructor.apiBase,
|
apiBase: fullApiPathname = objectConstructor.apiBase,
|
||||||
checkPreferredVersion: doCheckPreferredVersion = false,
|
checkPreferredVersion: doCheckPreferredVersion = false,
|
||||||
fallbackApiBases,
|
fallbackApiBases,
|
||||||
|
allowedUsableVersions,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
assert(fullApiPathname, "apiBase MUST be provied either via KubeApiOptions.apiBase or KubeApiOptions.objectConstructor.apiBase");
|
assert(fullApiPathname, "apiBase MUST be provied either via KubeApiOptions.apiBase or KubeApiOptions.objectConstructor.apiBase");
|
||||||
@ -255,6 +275,7 @@ export class KubeApi<
|
|||||||
|
|
||||||
this.doCheckPreferredVersion = doCheckPreferredVersion;
|
this.doCheckPreferredVersion = doCheckPreferredVersion;
|
||||||
this.fallbackApiBases = fallbackApiBases;
|
this.fallbackApiBases = fallbackApiBases;
|
||||||
|
this.allowedUsableVersions = allowedUsableVersions;
|
||||||
this.fullApiPathname = fullApiPathname;
|
this.fullApiPathname = fullApiPathname;
|
||||||
this.kind = kind;
|
this.kind = kind;
|
||||||
this.isNamespaced = isNamespaced ?? objectConstructor.namespaced ?? false;
|
this.isNamespaced = isNamespaced ?? objectConstructor.namespaced ?? false;
|
||||||
@ -291,7 +312,7 @@ export class KubeApi<
|
|||||||
try {
|
try {
|
||||||
const { apiPrefix, apiGroup, resource } = parseKubeApi(apiUrl);
|
const { apiPrefix, apiGroup, resource } = parseKubeApi(apiUrl);
|
||||||
const list = await this.request.get(`${apiPrefix}/${apiGroup}`) as KubeApiResourceVersionList;
|
const list = await this.request.get(`${apiPrefix}/${apiGroup}`) as KubeApiResourceVersionList;
|
||||||
const resourceVersions = getOrderedVersions(list);
|
const resourceVersions = getOrderedVersions(list, this.allowedUsableVersions?.[apiGroup]);
|
||||||
|
|
||||||
for (const resourceVersion of resourceVersions) {
|
for (const resourceVersion of resourceVersions) {
|
||||||
const { resources } = await this.request.get(`${apiPrefix}/${resourceVersion.groupVersion}`) as KubeApiResourceList;
|
const { resources } = await this.request.get(`${apiPrefix}/${resourceVersion.groupVersion}`) as KubeApiResourceList;
|
||||||
@ -313,8 +334,8 @@ export class KubeApi<
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async checkPreferredVersion() {
|
protected async checkPreferredVersion() {
|
||||||
if (this.fallbackApiBases && !this.doCheckPreferredVersion) {
|
if (!this.doCheckPreferredVersion && (this.fallbackApiBases || this.allowedUsableVersions)) {
|
||||||
throw new Error("checkPreferredVersion must be enabled if fallbackApiBases is set in KubeApi");
|
throw new Error("checkPreferredVersion must be enabled if either fallbackApiBases or allowedUsableVersions are set in KubeApi");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.doCheckPreferredVersion && this.apiVersionPreferred === undefined) {
|
if (this.doCheckPreferredVersion && this.apiVersionPreferred === undefined) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user