Skip to content

Commit

Permalink
[Infrastructure UI] Add strict payload validation to metrics_explorer…
Browse files Browse the repository at this point in the history
…_views endpoint (#160982)

closes [#157520](#157520)
## Summary

This PR adds strict payload validation to `metrics_explorer_views`
endpoint. This PR depends on this to be merged
#160852


### How to test

- Call the endpoint below trying to use invalid values. see
[here](https://github.com/elastic/kibana/pull/160982/files#diff-4573683b3b62cdf5f6426ec345b7ad6c7d6e6328237b213ca7519f686d8fa951R125-R131).

```bash
[POST|PUT] kbn:/api/infra/metrics_explorer_views
{
  "attributes": {
    "name": "Ad-hoc",
    "options": {
      "aggregation": "avg",
      "metrics": [
        {
          "aggregation": "avg",
          "field": "system.cpu.total.norm.pct",
          "color": "color0"
        },
      ],
      "source": "default",
      "groupBy": [
        "host.name"
      ]
    },
    "chartOptions": {
      "type": "line",
      "yAxisMode": "fromZero",
      "stack": false
    },
    "currentTimerange": {
      "from": "now-1h",
      "to": "now",
      "interval": ">=10s"
    }
  }
}
```

- Set up a local Kibana instance
- Navigate to `Infrastructure > Metrics Explorer`
- In the UI, use the Saved View feature and try different field
combinations

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
crespocarlos and kibanamachine authored Jul 14, 2023
1 parent a9786df commit a0a83c1
Show file tree
Hide file tree
Showing 23 changed files with 428 additions and 307 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import { either } from 'fp-ts/Either';
import { metricsExplorerViewRT } from '../../../metrics_explorer_views';

export const METRICS_EXPLORER_VIEW_URL = '/api/infra/metrics_explorer_views';
export const METRICS_EXPLORER_VIEW_URL_ENTITY = `${METRICS_EXPLORER_VIEW_URL}/{metricsExplorerViewId}`;
Expand Down Expand Up @@ -35,28 +35,6 @@ export const metricsExplorerViewRequestQueryRT = rt.partial({

export type MetricsExplorerViewRequestQuery = rt.TypeOf<typeof metricsExplorerViewRequestQueryRT>;

const metricsExplorerViewAttributesResponseRT = rt.intersection([
rt.strict({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,
}),
rt.UnknownRecord,
]);

const metricsExplorerViewResponseRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
attributes: metricsExplorerViewAttributesResponseRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
}),
])
);

export const metricsExplorerViewResponsePayloadRT = rt.type({
data: metricsExplorerViewResponseRT,
data: metricsExplorerViewRT,
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
* 2.0.
*/

import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import {
metricsExplorerViewAttributesRT,
metricsExplorerViewRT,
} from '../../../metrics_explorer_views';

export const createMetricsExplorerViewAttributesRequestPayloadRT = rt.intersection([
rt.type({
name: nonEmptyStringRt,
}),
rt.UnknownRecord,
rt.exact(rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined })),
metricsExplorerViewAttributesRT,
rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined }),
]);

export type CreateMetricsExplorerViewAttributesRequestPayload = rt.TypeOf<
Expand All @@ -23,3 +23,5 @@ export type CreateMetricsExplorerViewAttributesRequestPayload = rt.TypeOf<
export const createMetricsExplorerViewRequestPayloadRT = rt.type({
attributes: createMetricsExplorerViewAttributesRequestPayloadRT,
});

export type CreateMetricsExplorerViewResponsePayload = rt.TypeOf<typeof metricsExplorerViewRT>;
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,13 @@
* 2.0.
*/

import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';

export const findMetricsExplorerViewAttributesResponseRT = rt.strict({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,
});

const findMetricsExplorerViewResponseRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
attributes: findMetricsExplorerViewAttributesResponseRT,
}),
rt.partial({
updatedAt: rt.number,
version: rt.string,
}),
])
);
import { singleMetricsExplorerViewRT } from '../../../metrics_explorer_views';

export const findMetricsExplorerViewResponsePayloadRT = rt.type({
data: rt.array(findMetricsExplorerViewResponseRT),
data: rt.array(singleMetricsExplorerViewRT),
});

export type FindMetricsExplorerViewResponsePayload = rt.TypeOf<
typeof findMetricsExplorerViewResponsePayloadRT
>;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
*/

import * as rt from 'io-ts';
import { metricsExplorerViewRT } from '../../../metrics_explorer_views';

export const getMetricsExplorerViewRequestParamsRT = rt.type({
metricsExplorerViewId: rt.string,
});

export type GetMetricsExplorerViewResponsePayload = rt.TypeOf<typeof metricsExplorerViewRT>;
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
* 2.0.
*/

import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import {
metricsExplorerViewAttributesRT,
metricsExplorerViewRT,
} from '../../../metrics_explorer_views';

export const updateMetricsExplorerViewAttributesRequestPayloadRT = rt.intersection([
rt.type({
name: nonEmptyStringRt,
}),
rt.UnknownRecord,
rt.exact(rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined })),
metricsExplorerViewAttributesRT,
rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined }),
]);

export type UpdateMetricsExplorerViewAttributesRequestPayload = rt.TypeOf<
Expand All @@ -23,3 +23,5 @@ export type UpdateMetricsExplorerViewAttributesRequestPayload = rt.TypeOf<
export const updateMetricsExplorerViewRequestPayloadRT = rt.type({
attributes: updateMetricsExplorerViewAttributesRequestPayloadRT,
});

export type UpdateMetricsExplorerViewResponsePayload = rt.TypeOf<typeof metricsExplorerViewRT>;
17 changes: 11 additions & 6 deletions x-pack/plugins/infra/common/metrics_explorer_views/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@

import { i18n } from '@kbn/i18n';
import type { NonEmptyString } from '@kbn/io-ts-utils';
import type { MetricsExplorerViewAttributes } from './types';
import { Color } from '../color_palette';
import {
MetricsExplorerChartType,
MetricsExplorerViewAttributes,
MetricsExplorerYAxisMode,
} from './types';

export const staticMetricsExplorerViewId = '0';

Expand All @@ -23,24 +28,24 @@ export const staticMetricsExplorerViewAttributes: MetricsExplorerViewAttributes
{
aggregation: 'avg',
field: 'system.cpu.total.norm.pct',
color: 'color0',
color: Color.color0,
},
{
aggregation: 'avg',
field: 'kubernetes.pod.cpu.usage.node.pct',
color: 'color1',
color: Color.color1,
},
{
aggregation: 'avg',
field: 'docker.cpu.total.pct',
color: 'color2',
color: Color.color2,
},
],
source: 'default',
},
chartOptions: {
type: 'line',
yAxisMode: 'fromZero',
type: MetricsExplorerChartType.line,
yAxisMode: MetricsExplorerYAxisMode.fromZero,
stack: false,
},
currentTimerange: {
Expand Down
119 changes: 110 additions & 9 deletions x-pack/plugins/infra/common/metrics_explorer_views/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,101 @@
* 2.0.
*/

import { nonEmptyStringRt } from '@kbn/io-ts-utils';
import { isoToEpochRt, nonEmptyStringRt } from '@kbn/io-ts-utils';
import * as rt from 'io-ts';
import { Color } from '../color_palette';
import {
metricsExplorerAggregationRT,
metricsExplorerMetricRT,
} from '../http_api/metrics_explorer';

export const metricsExplorerViewAttributesRT = rt.intersection([
rt.type({
name: nonEmptyStringRt,
isDefault: rt.boolean,
isStatic: rt.boolean,
export const inventorySortOptionRT = rt.type({
by: rt.keyof({ name: null, value: null }),
direction: rt.keyof({ asc: null, desc: null }),
});

export enum MetricsExplorerChartType {
line = 'line',
area = 'area',
bar = 'bar',
}

export enum MetricsExplorerYAxisMode {
fromZero = 'fromZero',
auto = 'auto',
}

export const metricsExplorerChartOptionsRT = rt.type({
yAxisMode: rt.keyof(
Object.fromEntries(Object.values(MetricsExplorerYAxisMode).map((v) => [v, null])) as Record<
MetricsExplorerYAxisMode,
null
>
),
type: rt.keyof(
Object.fromEntries(Object.values(MetricsExplorerChartType).map((v) => [v, null])) as Record<
MetricsExplorerChartType,
null
>
),
stack: rt.boolean,
});

export const metricsExplorerTimeOptionsRT = rt.type({
from: rt.string,
to: rt.string,
interval: rt.string,
});
const metricsExplorerOptionsMetricRT = rt.intersection([
metricsExplorerMetricRT,
rt.partial({
rate: rt.boolean,
color: rt.keyof(
Object.fromEntries(Object.values(Color).map((c) => [c, null])) as Record<Color, null>
),
label: rt.string,
}),
rt.UnknownRecord,
]);

export type MetricsExplorerViewAttributes = rt.TypeOf<typeof metricsExplorerViewAttributesRT>;
export const metricExplorerOptionsRequiredRT = rt.type({
aggregation: metricsExplorerAggregationRT,
metrics: rt.array(metricsExplorerOptionsMetricRT),
});

export const metricExplorerOptionsOptionalRT = rt.partial({
limit: rt.number,
groupBy: rt.union([rt.string, rt.array(rt.string)]),
filterQuery: rt.string,
source: rt.string,
forceInterval: rt.boolean,
dropLastBucket: rt.boolean,
});
export const metricsExplorerOptionsRT = rt.intersection([
metricExplorerOptionsRequiredRT,
metricExplorerOptionsOptionalRT,
]);

export const metricExplorerViewStateRT = rt.type({
chartOptions: metricsExplorerChartOptionsRT,
currentTimerange: metricsExplorerTimeOptionsRT,
options: metricsExplorerOptionsRT,
});

export const metricsExplorerViewBasicAttributesRT = rt.type({
name: nonEmptyStringRt,
});

const metricsExplorerViewFlagsRT = rt.partial({ isDefault: rt.boolean, isStatic: rt.boolean });

export const metricsExplorerViewAttributesRT = rt.intersection([
metricExplorerViewStateRT,
metricsExplorerViewBasicAttributesRT,
metricsExplorerViewFlagsRT,
]);

const singleMetricsExplorerViewAttributesRT = rt.exact(
rt.intersection([metricsExplorerViewBasicAttributesRT, metricsExplorerViewFlagsRT])
);

export const metricsExplorerViewRT = rt.exact(
rt.intersection([
Expand All @@ -26,10 +108,29 @@ export const metricsExplorerViewRT = rt.exact(
attributes: metricsExplorerViewAttributesRT,
}),
rt.partial({
updatedAt: rt.number,
updatedAt: isoToEpochRt,
version: rt.string,
}),
])
);

export const singleMetricsExplorerViewRT = rt.exact(
rt.intersection([
rt.type({
id: rt.string,
attributes: singleMetricsExplorerViewAttributesRT,
}),
rt.partial({
updatedAt: isoToEpochRt,
version: rt.string,
}),
])
);

export type MetricsExplorerChartOptions = rt.TypeOf<typeof metricsExplorerChartOptionsRT>;
export type MetricsExplorerOptions = rt.TypeOf<typeof metricsExplorerOptionsRT>;
export type MetricsExplorerOptionsMetric = rt.TypeOf<typeof metricsExplorerOptionsMetricRT>;
export type MetricsExplorerViewState = rt.TypeOf<typeof metricExplorerViewStateRT>;
export type MetricsExplorerTimeOptions = rt.TypeOf<typeof metricsExplorerTimeOptionsRT>;
export type MetricsExplorerViewAttributes = rt.TypeOf<typeof metricsExplorerViewAttributesRT>;
export type MetricsExplorerView = rt.TypeOf<typeof metricsExplorerViewRT>;
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { MetricsSourceConfiguration } from '../../../../common/metrics_sources';
import { MetricExpression, TimeRange } from '../types';
import {
MetricsExplorerOptions,
MetricsExplorerTimestampsRT,
MetricsExplorerTimestamp,
} from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data';
import { MetricExplorerCustomMetricAggregations } from '../../../../common/http_api/metrics_explorer';
Expand Down Expand Up @@ -59,7 +59,7 @@ export const useMetricsExplorerChartData = (
groupBy,
]
);
const timestamps: MetricsExplorerTimestampsRT = useMemo(() => {
const timestamps: MetricsExplorerTimestamp = useMemo(() => {
const from = timeRange.from ?? `now-${(timeSize || 1) * 20}${timeUnit}`;
const to = timeRange.to ?? 'now';
const fromTimestamp = DateMath.parse(from)!.valueOf();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import React, { useMemo } from 'react';
import { ThrowReporter } from 'io-ts/lib/ThrowReporter';
import { UrlStateContainer } from '../../utils/url_state';
import {
MetricsExplorerOptions,
type MetricsExplorerOptions,
type MetricsExplorerTimeOptions,
type MetricsExplorerChartOptions,
useMetricsExplorerOptionsContainerContext,
MetricsExplorerTimeOptions,
MetricsExplorerChartOptions,
metricExplorerOptionsRT,
metricsExplorerOptionsRT,
metricsExplorerChartOptionsRT,
metricsExplorerTimeOptionsRT,
} from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options';
Expand Down Expand Up @@ -73,7 +73,7 @@ export const WithMetricsExplorerOptionsUrlState = () => {
};

function isMetricExplorerOptions(subject: any): subject is MetricsExplorerOptions {
const result = metricExplorerOptionsRT.decode(subject);
const result = metricsExplorerOptionsRT.decode(subject);

try {
ThrowReporter.report(result);
Expand Down
7 changes: 5 additions & 2 deletions x-pack/plugins/infra/public/hooks/use_inventory_views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import {
UpdateViewParams,
} from '../../common/saved_views';
import { MetricsSourceConfigurationResponse } from '../../common/metrics_sources';
import { CreateInventoryViewAttributesRequestPayload } from '../../common/http_api/latest';
import {
CreateInventoryViewAttributesRequestPayload,
UpdateInventoryViewAttributesRequestPayload,
} from '../../common/http_api/latest';
import type { InventoryView } from '../../common/inventory_views';
import { useKibanaContextForPlugin } from './use_kibana';
import { useUrlState } from '../utils/use_url_state';
Expand Down Expand Up @@ -133,7 +136,7 @@ export const useInventoryViews = (): UseInventoryViewsResult => {
const { mutateAsync: updateViewById, isLoading: isUpdatingView } = useMutation<
InventoryView,
ServerError,
UpdateViewParams<CreateInventoryViewAttributesRequestPayload>
UpdateViewParams<UpdateInventoryViewAttributesRequestPayload>
>({
mutationFn: ({ id, attributes }) => inventoryViews.client.updateInventoryView(id, attributes),
onError: (error) => {
Expand Down
Loading

0 comments on commit a0a83c1

Please sign in to comment.