mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Support networking.k8s.io/v1 for Ingress (#1439)
* Support networking.k8s.io/v1 for Ingress Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com> * Add helper method to Ingress Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com> * Fix lint errors. Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
parent
57a879c2e8
commit
22ff706f4c
82
src/renderer/api/__tests__/kube-api.test.ts
Normal file
82
src/renderer/api/__tests__/kube-api.test.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
describe("KubeApi", () => {
|
||||
it("uses url from apiBase if apiBase contains the resource", async () => {
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
if (request.url === "/api-kube/apis/networking.k8s.io/v1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses"
|
||||
}] as any []
|
||||
})
|
||||
};
|
||||
} else if (request.url === "/api-kube/apis/extensions/v1beta1") {
|
||||
// Even if the old API contains ingresses, KubeApi should prefer the apiBase url
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses"
|
||||
}] as any []
|
||||
})
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any []
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
const kubeApi = new KubeApi({
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
|
||||
await kubeApi.get();
|
||||
expect(kubeApi.apiPrefix).toEqual("/apis");
|
||||
expect(kubeApi.apiGroup).toEqual("networking.k8s.io");
|
||||
});
|
||||
|
||||
it("uses url from fallbackApiBases if apiBase lacks the resource", async () => {
|
||||
(fetch as any).mockResponse(async (request: any) => {
|
||||
if (request.url === "/api-kube/apis/networking.k8s.io/v1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any []
|
||||
})
|
||||
};
|
||||
} else if (request.url === "/api-kube/apis/extensions/v1beta1") {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [{
|
||||
name: "ingresses"
|
||||
}] as any []
|
||||
})
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
body: JSON.stringify({
|
||||
resources: [] as any []
|
||||
})
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const apiBase = "apis/networking.k8s.io/v1/ingresses";
|
||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
const kubeApi = new KubeApi({
|
||||
apiBase,
|
||||
fallbackApiBases: [fallbackApiBase],
|
||||
checkPreferredVersion: true,
|
||||
});
|
||||
|
||||
await kubeApi.get();
|
||||
expect(kubeApi.apiPrefix).toEqual("/apis");
|
||||
expect(kubeApi.apiGroup).toEqual("extensions");
|
||||
});
|
||||
});
|
||||
@ -29,11 +29,42 @@ export interface ILoadBalancerIngress {
|
||||
hostname?: string;
|
||||
ip?: string;
|
||||
}
|
||||
|
||||
// extensions/v1beta1
|
||||
interface IExtensionsBackend {
|
||||
serviceName: string;
|
||||
servicePort: number;
|
||||
}
|
||||
|
||||
// networking.k8s.io/v1
|
||||
interface INetworkingBackend {
|
||||
service: IIngressService;
|
||||
}
|
||||
|
||||
export type IIngressBackend = IExtensionsBackend | INetworkingBackend;
|
||||
|
||||
export interface IIngressService {
|
||||
name: string;
|
||||
port: {
|
||||
name?: string;
|
||||
number?: number;
|
||||
}
|
||||
}
|
||||
|
||||
export const getBackendServiceNamePort = (backend: IIngressBackend) => {
|
||||
// .service is available with networking.k8s.io/v1, otherwise using extensions/v1beta1 interface
|
||||
const serviceName = "service" in backend ? backend.service.name : backend.serviceName;
|
||||
// Port is specified either with a number or name
|
||||
const servicePort = "service" in backend ? backend.service.port.number ?? backend.service.port.name : backend.servicePort;
|
||||
|
||||
return { serviceName, servicePort };
|
||||
};
|
||||
|
||||
@autobind()
|
||||
export class Ingress extends KubeObject {
|
||||
static kind = "Ingress";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/extensions/v1beta1/ingresses";
|
||||
static apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||
|
||||
spec: {
|
||||
tls: {
|
||||
@ -44,17 +75,20 @@ export class Ingress extends KubeObject {
|
||||
http: {
|
||||
paths: {
|
||||
path?: string;
|
||||
backend: {
|
||||
serviceName: string;
|
||||
servicePort: number;
|
||||
};
|
||||
backend: IIngressBackend;
|
||||
}[];
|
||||
};
|
||||
}[];
|
||||
backend?: {
|
||||
serviceName: string;
|
||||
servicePort: number;
|
||||
};
|
||||
// extensions/v1beta1
|
||||
backend?: IExtensionsBackend;
|
||||
// networking.k8s.io/v1
|
||||
defaultBackend?: INetworkingBackend & {
|
||||
resource: {
|
||||
apiGroup: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
}
|
||||
}
|
||||
};
|
||||
status: {
|
||||
loadBalancer: {
|
||||
@ -75,7 +109,9 @@ export class Ingress extends KubeObject {
|
||||
const host = rule.host ? rule.host : "*";
|
||||
if (rule.http && rule.http.paths) {
|
||||
rule.http.paths.forEach(path => {
|
||||
routes.push(protocol + "://" + host + (path.path || "/") + " ⇢ " + path.backend.serviceName + ":" + path.backend.servicePort);
|
||||
const { serviceName, servicePort } = getBackendServiceNamePort(path.backend);
|
||||
|
||||
routes.push(protocol + "://" + host + (path.path || "/") + " ⇢ " + serviceName + ":" + servicePort);
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -83,6 +119,17 @@ export class Ingress extends KubeObject {
|
||||
return routes;
|
||||
}
|
||||
|
||||
getServiceNamePort() {
|
||||
const { spec } = this;
|
||||
const serviceName = spec?.defaultBackend?.service.name ?? spec?.backend?.serviceName;
|
||||
const servicePort = spec?.defaultBackend?.service.port.number ?? spec?.defaultBackend?.service.port.name ?? spec?.backend?.servicePort;
|
||||
|
||||
return {
|
||||
serviceName,
|
||||
servicePort
|
||||
};
|
||||
}
|
||||
|
||||
getHosts() {
|
||||
const { spec: { rules } } = this;
|
||||
if (!rules) return [];
|
||||
@ -91,22 +138,24 @@ export class Ingress extends KubeObject {
|
||||
|
||||
getPorts() {
|
||||
const ports: number[] = [];
|
||||
const { spec: { tls, rules, backend } } = this;
|
||||
const { spec: { tls, rules, backend, defaultBackend } } = this;
|
||||
const httpPort = 80;
|
||||
const tlsPort = 443;
|
||||
|
||||
// Note: not using the port name (string)
|
||||
const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort;
|
||||
|
||||
if (rules && rules.length > 0) {
|
||||
if (rules.some(rule => rule.hasOwnProperty("http"))) {
|
||||
ports.push(httpPort);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (backend && backend.servicePort) {
|
||||
ports.push(backend.servicePort);
|
||||
}
|
||||
} else if (servicePort !== undefined) {
|
||||
ports.push(Number(servicePort));
|
||||
}
|
||||
if (tls && tls.length > 0) {
|
||||
ports.push(tlsPort);
|
||||
}
|
||||
|
||||
return ports.join(", ");
|
||||
}
|
||||
|
||||
@ -121,4 +170,8 @@ export class Ingress extends KubeObject {
|
||||
|
||||
export const ingressApi = new IngressApi({
|
||||
objectConstructor: Ingress,
|
||||
});
|
||||
// Add fallback for Kubernetes <1.19
|
||||
checkPreferredVersion: true,
|
||||
fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"],
|
||||
logStuff: true
|
||||
} as any);
|
||||
|
||||
@ -8,10 +8,22 @@ import { apiKube } from "./index";
|
||||
import { kubeWatchApi } from "./kube-watch-api";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
|
||||
import { apiKubePrefix, isDevelopment } from "../../common/vars";
|
||||
import { apiKubePrefix, isDevelopment, isTestEnv } from "../../common/vars";
|
||||
|
||||
export interface IKubeApiOptions<T extends KubeObject> {
|
||||
apiBase?: string; // base api-path for listing all resources, e.g. "/api/v1/pods"
|
||||
/**
|
||||
* base api-path for listing all resources, e.g. "/api/v1/pods"
|
||||
*/
|
||||
apiBase?: string;
|
||||
|
||||
/**
|
||||
* If the API uses a different API endpoint (e.g. apiBase) depending on the cluster version,
|
||||
* fallback API bases can be listed individually.
|
||||
* The first (existing) API base is used in the requests, if apiBase is not found.
|
||||
* This option only has effect if checkPreferredVersion is true.
|
||||
*/
|
||||
fallbackApiBases?: string[];
|
||||
|
||||
objectConstructor?: IKubeObjectConstructor<T>;
|
||||
request?: KubeJsonApi;
|
||||
isNamespaced?: boolean;
|
||||
@ -35,6 +47,17 @@ export interface IKubePreferredVersion {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IKubeResourceList {
|
||||
resources: {
|
||||
kind: string;
|
||||
name: string;
|
||||
namespaced: boolean;
|
||||
singularName: string;
|
||||
storageVersionHash: string;
|
||||
verbs: string[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface IKubeApiCluster {
|
||||
id: string;
|
||||
}
|
||||
@ -85,7 +108,7 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
if (!options.apiBase) {
|
||||
options.apiBase = objectConstructor.apiBase;
|
||||
}
|
||||
const { apiBase, apiPrefix, apiGroup, apiVersion, apiVersionWithGroup, resource } = KubeApi.parseApi(options.apiBase);
|
||||
const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = KubeApi.parseApi(options.apiBase);
|
||||
|
||||
this.kind = kind;
|
||||
this.isNamespaced = isNamespaced;
|
||||
@ -108,8 +131,73 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
.join("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest API prefix/group that contains the required resource.
|
||||
* First tries options.apiBase, then urls in order from options.fallbackApiBases.
|
||||
*/
|
||||
private async getLatestApiPrefixGroup() {
|
||||
// Note that this.options.apiBase is the "full" url, whereas this.apiBase is parsed
|
||||
const apiBases = [this.options.apiBase, ...this.options.fallbackApiBases];
|
||||
|
||||
for (const apiUrl of apiBases) {
|
||||
// Split e.g. "/apis/extensions/v1beta1/ingresses" to parts
|
||||
const { apiPrefix, apiGroup, apiVersionWithGroup, resource } = KubeApi.parseApi(apiUrl);
|
||||
|
||||
// Request available resources
|
||||
try {
|
||||
const response = await this.request.get<IKubeResourceList>(`${apiPrefix}/${apiVersionWithGroup}`);
|
||||
|
||||
// If the resource is found in the group, use this apiUrl
|
||||
if (response.resources?.find(kubeResource => kubeResource.name === resource)) {
|
||||
return { apiPrefix, apiGroup };
|
||||
}
|
||||
} catch (error) {
|
||||
// Exception is ignored as we can try the next url
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid throwing in tests
|
||||
if (isTestEnv) {
|
||||
return {
|
||||
apiPrefix: this.apiPrefix,
|
||||
apiGroup: this.apiGroup
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Can't find working API for the Kubernetes resource ${this.apiResource}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the apiPrefix and apiGroup to be used for fetching the preferred version.
|
||||
*/
|
||||
private async getPreferredVersionPrefixGroup() {
|
||||
if (this.options.fallbackApiBases) {
|
||||
return this.getLatestApiPrefixGroup();
|
||||
} else {
|
||||
return {
|
||||
apiPrefix: this.apiPrefix,
|
||||
apiGroup: this.apiGroup
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async checkPreferredVersion() {
|
||||
if (this.options.fallbackApiBases && !this.options.checkPreferredVersion) {
|
||||
throw new Error("checkPreferredVersion must be enabled if fallbackApiBases is set in KubeApi");
|
||||
}
|
||||
|
||||
if (this.options.checkPreferredVersion && this.apiVersionPreferred === undefined) {
|
||||
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
|
||||
});
|
||||
|
||||
const res = await this.request.get<IKubePreferredVersion>(`${this.apiPrefix}/${this.apiGroup}`);
|
||||
Object.defineProperty(this, "apiVersionPreferred", {
|
||||
value: res?.preferredVersion?.version ?? null,
|
||||
|
||||
@ -14,6 +14,7 @@ import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { IngressCharts } from "./ingress-charts";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Ingress> {
|
||||
}
|
||||
@ -48,7 +49,9 @@ export class IngressDetails extends React.Component<Props> {
|
||||
</TableHead>
|
||||
{
|
||||
rule.http.paths.map((path, index) => {
|
||||
const backend = `${path.backend.serviceName}:${path.backend.servicePort}`;
|
||||
const { serviceName, servicePort } = getBackendServiceNamePort(path.backend);
|
||||
const backend =`${serviceName}:${servicePort}`;
|
||||
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="path">{path.path || ""}</TableCell>
|
||||
@ -100,6 +103,9 @@ export class IngressDetails extends React.Component<Props> {
|
||||
<Trans>Network</Trans>,
|
||||
<Trans>Duration</Trans>,
|
||||
];
|
||||
|
||||
const { serviceName, servicePort } = ingress.getServiceNamePort();
|
||||
|
||||
return (
|
||||
<div className="IngressDetails">
|
||||
<ResourceMetrics
|
||||
@ -117,9 +123,9 @@ export class IngressDetails extends React.Component<Props> {
|
||||
{spec.tls.map((tls, index) => <p key={index}>{tls.secretName}</p>)}
|
||||
</DrawerItem>
|
||||
}
|
||||
{spec.backend && spec.backend.serviceName && spec.backend.servicePort &&
|
||||
{serviceName && servicePort &&
|
||||
<DrawerItem name={<Trans>Service</Trans>}>
|
||||
{spec.backend.serviceName}:{spec.backend.servicePort}
|
||||
{serviceName}:{servicePort}
|
||||
</DrawerItem>
|
||||
}
|
||||
<DrawerTitle title={<Trans>Rules</Trans>}/>
|
||||
@ -134,14 +140,14 @@ export class IngressDetails extends React.Component<Props> {
|
||||
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "Ingress",
|
||||
apiVersions: ["extensions/v1beta1"],
|
||||
apiVersions: ["networking.k8s.io/v1", "extensions/v1beta1"],
|
||||
components: {
|
||||
Details: (props) => <IngressDetails {...props} />
|
||||
}
|
||||
});
|
||||
kubeObjectDetailRegistry.add({
|
||||
kind: "Ingress",
|
||||
apiVersions: ["extensions/v1beta1"],
|
||||
apiVersions: ["networking.k8s.io/v1", "extensions/v1beta1"],
|
||||
priority: 5,
|
||||
components: {
|
||||
Details: (props) => <KubeEventDetails {...props} />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user