1
0
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:
Panu Horsmalahti 2020-11-20 11:59:11 +02:00 committed by GitHub
parent 57a879c2e8
commit 22ff706f4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 254 additions and 25 deletions

View 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");
});
});

View File

@ -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);

View File

@ -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,

View File

@ -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} />