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;
|
hostname?: string;
|
||||||
ip?: 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()
|
@autobind()
|
||||||
export class Ingress extends KubeObject {
|
export class Ingress extends KubeObject {
|
||||||
static kind = "Ingress";
|
static kind = "Ingress";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
static apiBase = "/apis/extensions/v1beta1/ingresses";
|
static apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||||
|
|
||||||
spec: {
|
spec: {
|
||||||
tls: {
|
tls: {
|
||||||
@ -44,17 +75,20 @@ export class Ingress extends KubeObject {
|
|||||||
http: {
|
http: {
|
||||||
paths: {
|
paths: {
|
||||||
path?: string;
|
path?: string;
|
||||||
backend: {
|
backend: IIngressBackend;
|
||||||
serviceName: string;
|
|
||||||
servicePort: number;
|
|
||||||
};
|
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
backend?: {
|
// extensions/v1beta1
|
||||||
serviceName: string;
|
backend?: IExtensionsBackend;
|
||||||
servicePort: number;
|
// networking.k8s.io/v1
|
||||||
};
|
defaultBackend?: INetworkingBackend & {
|
||||||
|
resource: {
|
||||||
|
apiGroup: string;
|
||||||
|
kind: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
status: {
|
status: {
|
||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
@ -75,7 +109,9 @@ export class Ingress extends KubeObject {
|
|||||||
const host = rule.host ? rule.host : "*";
|
const host = rule.host ? rule.host : "*";
|
||||||
if (rule.http && rule.http.paths) {
|
if (rule.http && rule.http.paths) {
|
||||||
rule.http.paths.forEach(path => {
|
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;
|
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() {
|
getHosts() {
|
||||||
const { spec: { rules } } = this;
|
const { spec: { rules } } = this;
|
||||||
if (!rules) return [];
|
if (!rules) return [];
|
||||||
@ -91,22 +138,24 @@ export class Ingress extends KubeObject {
|
|||||||
|
|
||||||
getPorts() {
|
getPorts() {
|
||||||
const ports: number[] = [];
|
const ports: number[] = [];
|
||||||
const { spec: { tls, rules, backend } } = this;
|
const { spec: { tls, rules, backend, defaultBackend } } = this;
|
||||||
const httpPort = 80;
|
const httpPort = 80;
|
||||||
const tlsPort = 443;
|
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 && rules.length > 0) {
|
||||||
if (rules.some(rule => rule.hasOwnProperty("http"))) {
|
if (rules.some(rule => rule.hasOwnProperty("http"))) {
|
||||||
ports.push(httpPort);
|
ports.push(httpPort);
|
||||||
}
|
}
|
||||||
}
|
} else if (servicePort !== undefined) {
|
||||||
else {
|
ports.push(Number(servicePort));
|
||||||
if (backend && backend.servicePort) {
|
|
||||||
ports.push(backend.servicePort);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (tls && tls.length > 0) {
|
if (tls && tls.length > 0) {
|
||||||
ports.push(tlsPort);
|
ports.push(tlsPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ports.join(", ");
|
return ports.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,4 +170,8 @@ export class Ingress extends KubeObject {
|
|||||||
|
|
||||||
export const ingressApi = new IngressApi({
|
export const ingressApi = new IngressApi({
|
||||||
objectConstructor: Ingress,
|
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 { kubeWatchApi } from "./kube-watch-api";
|
||||||
import { apiManager } from "./api-manager";
|
import { apiManager } from "./api-manager";
|
||||||
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
|
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> {
|
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>;
|
objectConstructor?: IKubeObjectConstructor<T>;
|
||||||
request?: KubeJsonApi;
|
request?: KubeJsonApi;
|
||||||
isNamespaced?: boolean;
|
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 {
|
export interface IKubeApiCluster {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
@ -85,7 +108,7 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
if (!options.apiBase) {
|
if (!options.apiBase) {
|
||||||
options.apiBase = objectConstructor.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.kind = kind;
|
||||||
this.isNamespaced = isNamespaced;
|
this.isNamespaced = isNamespaced;
|
||||||
@ -108,8 +131,73 @@ export class KubeApi<T extends KubeObject = any> {
|
|||||||
.join("/");
|
.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() {
|
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) {
|
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}`);
|
const res = await this.request.get<IKubePreferredVersion>(`${this.apiPrefix}/${this.apiGroup}`);
|
||||||
Object.defineProperty(this, "apiVersionPreferred", {
|
Object.defineProperty(this, "apiVersionPreferred", {
|
||||||
value: res?.preferredVersion?.version ?? null,
|
value: res?.preferredVersion?.version ?? null,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { KubeObjectDetailsProps } from "../kube-object";
|
|||||||
import { IngressCharts } from "./ingress-charts";
|
import { IngressCharts } from "./ingress-charts";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<Ingress> {
|
interface Props extends KubeObjectDetailsProps<Ingress> {
|
||||||
}
|
}
|
||||||
@ -48,7 +49,9 @@ export class IngressDetails extends React.Component<Props> {
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
{
|
{
|
||||||
rule.http.paths.map((path, index) => {
|
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 (
|
return (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
<TableCell className="path">{path.path || ""}</TableCell>
|
<TableCell className="path">{path.path || ""}</TableCell>
|
||||||
@ -100,6 +103,9 @@ export class IngressDetails extends React.Component<Props> {
|
|||||||
<Trans>Network</Trans>,
|
<Trans>Network</Trans>,
|
||||||
<Trans>Duration</Trans>,
|
<Trans>Duration</Trans>,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const { serviceName, servicePort } = ingress.getServiceNamePort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="IngressDetails">
|
<div className="IngressDetails">
|
||||||
<ResourceMetrics
|
<ResourceMetrics
|
||||||
@ -117,9 +123,9 @@ export class IngressDetails extends React.Component<Props> {
|
|||||||
{spec.tls.map((tls, index) => <p key={index}>{tls.secretName}</p>)}
|
{spec.tls.map((tls, index) => <p key={index}>{tls.secretName}</p>)}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
}
|
}
|
||||||
{spec.backend && spec.backend.serviceName && spec.backend.servicePort &&
|
{serviceName && servicePort &&
|
||||||
<DrawerItem name={<Trans>Service</Trans>}>
|
<DrawerItem name={<Trans>Service</Trans>}>
|
||||||
{spec.backend.serviceName}:{spec.backend.servicePort}
|
{serviceName}:{servicePort}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
}
|
}
|
||||||
<DrawerTitle title={<Trans>Rules</Trans>}/>
|
<DrawerTitle title={<Trans>Rules</Trans>}/>
|
||||||
@ -134,14 +140,14 @@ export class IngressDetails extends React.Component<Props> {
|
|||||||
|
|
||||||
kubeObjectDetailRegistry.add({
|
kubeObjectDetailRegistry.add({
|
||||||
kind: "Ingress",
|
kind: "Ingress",
|
||||||
apiVersions: ["extensions/v1beta1"],
|
apiVersions: ["networking.k8s.io/v1", "extensions/v1beta1"],
|
||||||
components: {
|
components: {
|
||||||
Details: (props) => <IngressDetails {...props} />
|
Details: (props) => <IngressDetails {...props} />
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
kubeObjectDetailRegistry.add({
|
kubeObjectDetailRegistry.add({
|
||||||
kind: "Ingress",
|
kind: "Ingress",
|
||||||
apiVersions: ["extensions/v1beta1"],
|
apiVersions: ["networking.k8s.io/v1", "extensions/v1beta1"],
|
||||||
priority: 5,
|
priority: 5,
|
||||||
components: {
|
components: {
|
||||||
Details: (props) => <KubeEventDetails {...props} />
|
Details: (props) => <KubeEventDetails {...props} />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user