mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix parseAPI not working with non-"namespaces" pathnames. (#450)
* fix parseApi not accepting versions that don't start with 'v' * add unit tests Signed-off-by: Sebastian Malton <smalton@mirantis.com> Co-authored-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
parent
0661954a27
commit
bce7aec87d
58
dashboard/client/api/__test__/parseAPI.test.ts
Normal file
58
dashboard/client/api/__test__/parseAPI.test.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { KubeApi, IKubeApiLinkBase } from "../kube-api";
|
||||
|
||||
interface ParseAPITest {
|
||||
url: string;
|
||||
expected: IKubeApiLinkBase;
|
||||
}
|
||||
|
||||
const tests: ParseAPITest[] = [
|
||||
{
|
||||
url: "/api/v1/namespaces/kube-system/pods/coredns-6955765f44-v8p27",
|
||||
expected: {
|
||||
apiBase: "/api/v1/pods",
|
||||
apiPrefix: "/api",
|
||||
apiGroup: "",
|
||||
apiVersion: "v1",
|
||||
apiVersionWithGroup: "v1",
|
||||
namespace: "kube-system",
|
||||
resource: "pods",
|
||||
name: "coredns-6955765f44-v8p27"
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "/apis/stable.example.com/foo1/crontabs",
|
||||
expected: {
|
||||
apiBase: "/apis/stable.example.com/foo1/crontabs",
|
||||
apiPrefix: "/apis",
|
||||
apiGroup: "stable.example.com",
|
||||
apiVersion: "foo1",
|
||||
apiVersionWithGroup: "stable.example.com/foo1",
|
||||
resource: "crontabs",
|
||||
name: undefined,
|
||||
namespace: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "/apis/cluster.k8s.io/v1alpha1/clusters",
|
||||
expected: {
|
||||
apiBase: "/apis/cluster.k8s.io/v1alpha1/clusters",
|
||||
apiPrefix: "/apis",
|
||||
apiGroup: "cluster.k8s.io",
|
||||
apiVersion: "v1alpha1",
|
||||
apiVersionWithGroup: "cluster.k8s.io/v1alpha1",
|
||||
resource: "clusters",
|
||||
name: undefined,
|
||||
namespace: undefined,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('../kube-watch-api.ts', () => 'KubeWatchApi');
|
||||
describe("parseAPI unit tests", () => {
|
||||
for (const i in tests) {
|
||||
const { url: tUrl, expected:tExpect} = tests[i];
|
||||
test(`test #${parseInt(i)+1}`, () => {
|
||||
expect(KubeApi.parseApi(tUrl)).toStrictEqual(tExpect);
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -7,6 +7,7 @@ import { IKubeObjectRef, KubeJsonApi, KubeJsonApiData, KubeJsonApiDataList } fro
|
||||
import { apiKube } from "./index";
|
||||
import { kubeWatchApi } from "./kube-watch-api";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { split } from "../utils/arrays";
|
||||
|
||||
export interface IKubeApiOptions<T extends KubeObject> {
|
||||
kind: string; // resource type within api-group, e.g. "Namespace"
|
||||
@ -32,20 +33,45 @@ export interface IKubeApiLinkRef {
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
export class KubeApi<T extends KubeObject = any> {
|
||||
static matcher = /(\/apis?.*?)\/(?:(.*?)\/)?(v.*?)(?:\/namespaces\/(.+?))?\/([^\/]+)(?:\/([^\/?]+))?.*$/
|
||||
export interface IKubeApiLinkBase extends IKubeApiLinkRef {
|
||||
apiBase: string;
|
||||
apiGroup: string;
|
||||
apiVersionWithGroup: string;
|
||||
}
|
||||
|
||||
static parseApi(apiPath = "") {
|
||||
export class KubeApi<T extends KubeObject = any> {
|
||||
static parseApi(apiPath = ""): IKubeApiLinkBase {
|
||||
apiPath = new URL(apiPath, location.origin).pathname;
|
||||
const [, apiPrefix, apiGroup = "", apiVersion, namespace, resource, name] = apiPath.match(KubeApi.matcher) || [];
|
||||
const [, prefix, ...parts] = apiPath.split("/");
|
||||
const apiPrefix = `/${prefix}`;
|
||||
|
||||
const [left, right, found] = split(parts, "namespaces");
|
||||
let apiGroup, apiVersion, namespace, resource, name;
|
||||
|
||||
if (found) {
|
||||
if (left.length == 0) {
|
||||
throw new Error(`invalid apiPath: ${apiPath}`)
|
||||
}
|
||||
|
||||
apiVersion = left.pop();
|
||||
apiGroup = left.join("/");
|
||||
[namespace, resource, name] = right;
|
||||
} else {
|
||||
[apiGroup, apiVersion, resource] = left;
|
||||
}
|
||||
const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/");
|
||||
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
|
||||
|
||||
if (!apiBase) {
|
||||
throw new Error(`invalid apiPath: ${apiPath}`)
|
||||
}
|
||||
|
||||
return {
|
||||
apiBase,
|
||||
apiPrefix, apiGroup,
|
||||
apiVersion, apiVersionWithGroup,
|
||||
namespace, resource, name,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static createLink(ref: IKubeApiLinkRef): string {
|
||||
@ -55,7 +81,7 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
namespace = `namespaces/${namespace}`
|
||||
}
|
||||
return [apiPrefix, apiVersion, namespace, resource, name]
|
||||
.filter(v => !!v)
|
||||
.filter(v => v)
|
||||
.join("/")
|
||||
}
|
||||
|
||||
@ -130,8 +156,9 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
if (KubeObject.isJsonApiData(data)) {
|
||||
return new KubeObjectConstructor(data);
|
||||
}
|
||||
|
||||
// process items list response
|
||||
else if (KubeObject.isJsonApiDataList(data)) {
|
||||
if (KubeObject.isJsonApiDataList(data)) {
|
||||
const { apiVersion, items, metadata } = data;
|
||||
this.setResourceVersion(namespace, metadata.resourceVersion);
|
||||
this.setResourceVersion("", metadata.resourceVersion);
|
||||
@ -141,10 +168,12 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
...item,
|
||||
}))
|
||||
}
|
||||
|
||||
// custom apis might return array for list response, e.g. users, groups, etc.
|
||||
else if (Array.isArray(data)) {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(data => new KubeObjectConstructor(data));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@ -162,16 +191,19 @@ export class KubeApi<T extends KubeObject = any> {
|
||||
|
||||
async create({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T> {
|
||||
const apiUrl = this.getUrl({ namespace });
|
||||
return this.request.post(apiUrl, {
|
||||
data: merge({
|
||||
kind: this.kind,
|
||||
apiVersion: this.apiVersionWithGroup,
|
||||
metadata: {
|
||||
name,
|
||||
namespace
|
||||
}
|
||||
}, data)
|
||||
}).then(this.parseResponse);
|
||||
|
||||
return this.request
|
||||
.post(apiUrl, {
|
||||
data: merge({
|
||||
kind: this.kind,
|
||||
apiVersion: this.apiVersionWithGroup,
|
||||
metadata: {
|
||||
name,
|
||||
namespace
|
||||
}
|
||||
}, data)
|
||||
})
|
||||
.then(this.parseResponse);
|
||||
}
|
||||
|
||||
async update({ name = "", namespace = "default" } = {}, data?: Partial<T>): Promise<T> {
|
||||
|
||||
@ -96,7 +96,7 @@ export class DeploymentScaleDialog extends Component<Props> {
|
||||
<Trans>Desired number of replicas</Trans>: {desiredReplicas}
|
||||
</div>
|
||||
<div className="slider-container">
|
||||
<Slider value={desiredReplicas} max={scaleMax} onChange={onChange}/>
|
||||
<Slider value={desiredReplicas} max={scaleMax} onChange={onChange as any /** see: https://github.com/mui-org/material-ui/issues/20191 */}/>
|
||||
</div>
|
||||
</div>
|
||||
{warning &&
|
||||
|
||||
@ -150,7 +150,7 @@ export function BarChart(props: Props) {
|
||||
}
|
||||
};
|
||||
const options = merge(barOptions, customOptions);
|
||||
if (!chartData.datasets.length) {
|
||||
if (chartData.datasets.length == 0) {
|
||||
return <NoMetrics/>
|
||||
}
|
||||
return (
|
||||
@ -170,9 +170,17 @@ export const memoryOptions: ChartOptions = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
callback: value => {
|
||||
if (!value) return 0;
|
||||
return parseFloat(value) < 1 ? value.toFixed(3) : bytesToUnits(parseInt(value));
|
||||
callback: (value: number | string): string => {
|
||||
if (typeof value == "string") {
|
||||
const float = parseFloat(value);
|
||||
if (float < 1) {
|
||||
return float.toFixed(3);
|
||||
}
|
||||
|
||||
return bytesToUnits(parseInt(value));
|
||||
}
|
||||
|
||||
return `${value}`;
|
||||
},
|
||||
stepSize: 1
|
||||
}
|
||||
@ -194,11 +202,12 @@ export const cpuOptions: ChartOptions = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
callback: value => {
|
||||
if (value == 0) return 0;
|
||||
if (value < 10) return value.toFixed(3);
|
||||
if (value < 100) return value.toFixed(2);
|
||||
return value.toFixed(1);
|
||||
callback: (value: number | string): string => {
|
||||
const float = parseFloat(`${value}`);
|
||||
if (float == 0) return "0";
|
||||
if (float < 10) return float.toFixed(3);
|
||||
if (float < 100) return float.toFixed(2);
|
||||
return float.toFixed(1);
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
@ -19,10 +19,7 @@ export interface ChartDataSet extends ChartDataSets {
|
||||
}
|
||||
|
||||
export interface ChartProps {
|
||||
data: {
|
||||
labels?: Array<string | string[]>;
|
||||
datasets?: ChartDataSet[];
|
||||
};
|
||||
data: ChartData;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
options?: ChartOptions; // Passed to ChartJS instance
|
||||
|
||||
@ -7,7 +7,6 @@ import { cssNames } from "../../utils";
|
||||
import { themeStore } from "../../theme.store";
|
||||
|
||||
interface Props extends ChartProps {
|
||||
data: ChartData;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import ReactSelect, { components as ReactSelectComponents } from "react-select"
|
||||
import { Props as ReactSelectProps } from "react-select/base"
|
||||
import Creatable, { CreatableProps } from "react-select/creatable"
|
||||
import { StylesConfig } from "react-select/src/styles"
|
||||
import { ActionMeta } from "react-select/src/types"
|
||||
import { ActionMeta, OptionTypeBase } from "react-select/src/types"
|
||||
import { themeStore } from "../../theme.store";
|
||||
|
||||
export { ReactSelectComponents }
|
||||
@ -31,7 +31,7 @@ export interface SelectProps<T = any> extends ReactSelectProps<T>, CreatableProp
|
||||
menuClass?: string;
|
||||
isCreatable?: boolean;
|
||||
autoConvertOptions?: boolean; // to internal format (i.e. {value: T, label: string}[]), not working with groups
|
||||
onChange?(option: T, meta?: ActionMeta): void;
|
||||
onChange?(option: T, meta?: ActionMeta<OptionTypeBase>): void;
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -76,7 +76,7 @@ export class Select extends React.Component<SelectProps> {
|
||||
}
|
||||
|
||||
@autobind()
|
||||
onChange(value: SelectOption, meta: ActionMeta) {
|
||||
onChange(value: SelectOption, meta: ActionMeta<OptionTypeBase>) {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(value, meta);
|
||||
}
|
||||
|
||||
31
dashboard/client/utils/__tests__/arrays.test.ts
Normal file
31
dashboard/client/utils/__tests__/arrays.test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { split } from "../arrays";
|
||||
|
||||
describe("split array on element tests", () => {
|
||||
test("empty array", () => {
|
||||
expect(split([], 10)).toStrictEqual([[], [], false]);
|
||||
});
|
||||
|
||||
test("one element, not in array", () => {
|
||||
expect(split([1], 10)).toStrictEqual([[1], [], false]);
|
||||
});
|
||||
|
||||
test("ten elements, not in array", () => {
|
||||
expect(split([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]);
|
||||
});
|
||||
|
||||
test("one elements, in array", () => {
|
||||
expect(split([1], 1)).toStrictEqual([[], [], true]);
|
||||
});
|
||||
|
||||
test("ten elements, in front array", () => {
|
||||
expect(split([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]);
|
||||
});
|
||||
|
||||
test("ten elements, in middle array", () => {
|
||||
expect(split([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]);
|
||||
});
|
||||
|
||||
test("ten elements, in end array", () => {
|
||||
expect(split([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]);
|
||||
});
|
||||
});
|
||||
19
dashboard/client/utils/arrays.ts
Normal file
19
dashboard/client/utils/arrays.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* This function splits an array into two sub arrays on the first instance of
|
||||
* element (from the left). If the array does not contain the element. The
|
||||
* return value is defined to be `[array, [], false]`. If the element is in
|
||||
* the array then the return value is `[left, right, true]` where `left` is
|
||||
* the elements of `array` from `[0, index)` and `right` is `(index, length)`
|
||||
* @param array the full array to split into two sub-arrays
|
||||
* @param element the element in the middle of the array
|
||||
* @returns the left and right sub-arrays which when conjoined with `element`
|
||||
* is the same as `array`, and `true`
|
||||
*/
|
||||
export function split<T>(array: Array<T>, element: T): [Array<T>, Array<T>, boolean] {
|
||||
const index = array.indexOf(element);
|
||||
if (index < 0) {
|
||||
return [array, [], false];
|
||||
}
|
||||
|
||||
return [array.slice(0, index), array.slice(index+1, array.length), true]
|
||||
}
|
||||
@ -18,7 +18,7 @@ export class PrometheusLens implements PrometheusProvider {
|
||||
port: service.spec.ports[0].port
|
||||
}
|
||||
} catch(error) {
|
||||
logger.warn(`PrometheusLens: failed to list services: ${error.toString()}`)
|
||||
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user