mirror of https://github.com/rancher/dashboard.git
Adding the route alertmanager route type
- Extracted code that could be shared between route and receiver into an alertmanagerconfig util - Added some more support code to allow users to dynamically add/remove uncreatableTypes rancher/dashboard#1239
This commit is contained in:
parent
6cfecbfafb
commit
0118e0c984
|
|
@ -817,6 +817,13 @@ monitoring:
|
||||||
block: Block
|
block: Block
|
||||||
file: Filesystem
|
file: Filesystem
|
||||||
v1Warning: Monitoring is currently deployed from Cluster Manager. If you are migrating from an older version of Rancher with monitoring enabled, please disable monitoring in Cluster Manager before attempting to install the new Rancher Monitoring chart in Cluster Explorer.
|
v1Warning: Monitoring is currently deployed from Cluster Manager. If you are migrating from an older version of Rancher with monitoring enabled, please disable monitoring in Cluster Manager before attempting to install the new Rancher Monitoring chart in Cluster Explorer.
|
||||||
|
route:
|
||||||
|
fields:
|
||||||
|
receiver: Receiver
|
||||||
|
groupBy: Group By
|
||||||
|
groupWait: Group Wait
|
||||||
|
groupInterval: Group Interval
|
||||||
|
repeatInterval: Repeat Interval
|
||||||
|
|
||||||
nameNsDescription:
|
nameNsDescription:
|
||||||
name:
|
name:
|
||||||
|
|
@ -1343,6 +1350,10 @@ validation:
|
||||||
min: '"{key}" should be at least {count} {count, plural, =1 {character} other {characters}}'
|
min: '"{key}" should be at least {count} {count, plural, =1 {character} other {characters}}'
|
||||||
targets:
|
targets:
|
||||||
missingProjectId: A target must have a project selected.
|
missingProjectId: A target must have a project selected.
|
||||||
|
monitoring:
|
||||||
|
route:
|
||||||
|
match: At least one Match or Match Regex must be selected
|
||||||
|
interval: '"{key}" must be of a format with digits followed by a unit i.e. 1h, 2m, 30s'
|
||||||
|
|
||||||
wizard:
|
wizard:
|
||||||
back: Back
|
back: Back
|
||||||
|
|
@ -1901,3 +1912,8 @@ typeLabel:
|
||||||
one { Receiver }
|
one { Receiver }
|
||||||
other { Receivers }
|
other { Receivers }
|
||||||
}
|
}
|
||||||
|
monitoring.coreos.com.route: |-
|
||||||
|
{count, plural,
|
||||||
|
one { Route }
|
||||||
|
other { Routes }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { DSL } from '@/store/type-map';
|
import { DSL } from '@/store/type-map';
|
||||||
import { MONITORING, SECRET } from '@/config/types';
|
import { MONITORING } from '@/config/types';
|
||||||
import { STATE, NAME as NAME_COL, AGE } from '@/config/table-headers';
|
import { STATE, NAME as NAME_COL, AGE } from '@/config/table-headers';
|
||||||
import { base64Decode } from '@/utils/crypto';
|
import { getAllReceivers, getAllRoutes } from '@/utils/alertmanagerconfig';
|
||||||
import jsyaml from 'js-yaml';
|
|
||||||
import { FILENAME, getSecretId } from '@/models/monitoring.coreos.com.receiver';
|
|
||||||
|
|
||||||
export const NAME = 'monitoring';
|
export const NAME = 'monitoring';
|
||||||
export const CHART_NAME = 'rancher-monitoring';
|
export const CHART_NAME = 'rancher-monitoring';
|
||||||
|
|
@ -25,39 +23,11 @@ export function init(store) {
|
||||||
PROMETHEUSRULE,
|
PROMETHEUSRULE,
|
||||||
PROMETHEUS,
|
PROMETHEUS,
|
||||||
SPOOFED: {
|
SPOOFED: {
|
||||||
RECEIVER, RECEIVER_SPEC, RECEIVER_EMAIL, RECEIVER_SLACK, RECEIVER_WEBHOOK, RECEIVER_HTTP_CONFIG
|
RECEIVER, RECEIVER_SPEC, RECEIVER_EMAIL, RECEIVER_SLACK, RECEIVER_WEBHOOK, RECEIVER_HTTP_CONFIG,
|
||||||
|
ROUTE, ROUTE_SPEC
|
||||||
}
|
}
|
||||||
} = MONITORING;
|
} = MONITORING;
|
||||||
|
|
||||||
async function getSecretFile() {
|
|
||||||
const secretId = await getSecretId(store.dispatch);
|
|
||||||
const fileName = FILENAME;
|
|
||||||
const secret = await store.dispatch('cluster/find', {
|
|
||||||
type: SECRET, id: secretId, opt: { force: true }
|
|
||||||
});
|
|
||||||
const file = secret?.data?.[fileName];
|
|
||||||
const decodedFile = file ? base64Decode(file) : '{receivers: []}';
|
|
||||||
|
|
||||||
return jsyaml.safeLoad(decodedFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAllReceivers() {
|
|
||||||
try {
|
|
||||||
const file = await getSecretFile();
|
|
||||||
const receivers = file.receivers || [];
|
|
||||||
const receiversWithName = receivers.filter(receiver => receiver.name);
|
|
||||||
const mapped = receiversWithName.map(receiver => store.dispatch('cluster/create', {
|
|
||||||
id: receiver.name,
|
|
||||||
spec: receiver,
|
|
||||||
type: 'monitoring.coreos.com.receiver'
|
|
||||||
}));
|
|
||||||
|
|
||||||
return Promise.all(mapped);
|
|
||||||
} catch (ex) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
product({
|
product({
|
||||||
ifHaveType: PODMONITOR, // possible RBAC issue here if mon turned on but user doesn't have view/read roles on pod monitors
|
ifHaveType: PODMONITOR, // possible RBAC issue here if mon turned on but user doesn't have view/read roles on pod monitors
|
||||||
icon: 'monitoring'
|
icon: 'monitoring'
|
||||||
|
|
@ -73,72 +43,99 @@ export function init(store) {
|
||||||
exact: true
|
exact: true
|
||||||
});
|
});
|
||||||
|
|
||||||
spoofedType(
|
spoofedType({
|
||||||
{
|
label: 'Receivers',
|
||||||
label: 'Receivers',
|
type: RECEIVER,
|
||||||
type: RECEIVER,
|
schemas: [
|
||||||
schemas: [
|
{
|
||||||
{
|
id: RECEIVER,
|
||||||
id: RECEIVER,
|
type: 'schema',
|
||||||
type: 'schema',
|
collectionMethods: ['POST'],
|
||||||
collectionMethods: ['POST'],
|
resourceFields: { spec: { type: RECEIVER_SPEC } }
|
||||||
resourceFields: { spec: { type: RECEIVER_SPEC } }
|
},
|
||||||
},
|
{
|
||||||
{
|
id: RECEIVER_SPEC,
|
||||||
id: RECEIVER_SPEC,
|
type: 'schema',
|
||||||
type: 'schema',
|
resourceFields: {
|
||||||
resourceFields: {
|
name: { type: 'string' },
|
||||||
name: { type: 'string' },
|
email_configs: { type: `array[${ RECEIVER_EMAIL }]` },
|
||||||
email_configs: { type: `array[${ RECEIVER_EMAIL }]` },
|
slack_configs: { type: `array[${ RECEIVER_SLACK }]` },
|
||||||
slack_configs: { type: `array[${ RECEIVER_SLACK }]` },
|
webhook_configs: { type: `array[${ RECEIVER_WEBHOOK }]` }
|
||||||
webhook_configs: { type: `array[${ RECEIVER_WEBHOOK }]` }
|
}
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
id: RECEIVER_EMAIL,
|
||||||
id: RECEIVER_EMAIL,
|
type: 'schema',
|
||||||
type: 'schema',
|
resourceFields: {
|
||||||
resourceFields: {
|
to: { type: 'string' },
|
||||||
to: { type: 'string' },
|
send_resolved: { type: 'boolean' },
|
||||||
send_resolved: { type: 'boolean' },
|
from: { type: 'string' },
|
||||||
from: { type: 'string' },
|
smarthost: { type: 'string' },
|
||||||
smarthost: { type: 'string' },
|
require_tls: { type: 'boolean' },
|
||||||
require_tls: { type: 'boolean' },
|
auth_username: { type: 'string' },
|
||||||
auth_username: { type: 'string' },
|
auth_password: { type: 'string' }
|
||||||
auth_password: { type: 'string' }
|
}
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
id: RECEIVER_SLACK,
|
||||||
id: RECEIVER_SLACK,
|
type: 'schema',
|
||||||
type: 'schema',
|
resourceFields: {
|
||||||
resourceFields: {
|
api_url: { type: 'string' },
|
||||||
api_url: { type: 'string' },
|
channel: { type: 'string' },
|
||||||
channel: { type: 'string' },
|
http_config: { type: RECEIVER_HTTP_CONFIG },
|
||||||
http_config: { type: RECEIVER_HTTP_CONFIG },
|
send_resolved: { type: 'boolean' }
|
||||||
send_resolved: { type: 'boolean' }
|
}
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
id: RECEIVER_WEBHOOK,
|
||||||
id: RECEIVER_WEBHOOK,
|
type: 'schema',
|
||||||
type: 'schema',
|
resourceFields: {
|
||||||
resourceFields: {
|
url: { type: 'string' },
|
||||||
url: { type: 'string' },
|
http_config: { type: RECEIVER_HTTP_CONFIG },
|
||||||
http_config: { type: RECEIVER_HTTP_CONFIG },
|
send_resolved: { type: 'boolean' }
|
||||||
send_resolved: { type: 'boolean' }
|
}
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
id: RECEIVER_HTTP_CONFIG,
|
||||||
id: RECEIVER_HTTP_CONFIG,
|
type: 'schema',
|
||||||
type: 'schema',
|
resourceFields: { proxy_url: { type: 'string' } }
|
||||||
resourceFields: { proxy_url: { type: 'string' } }
|
},
|
||||||
},
|
|
||||||
|
|
||||||
],
|
],
|
||||||
getInstances: getAllReceivers
|
getInstances: () => getAllReceivers(store.dispatch)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
spoofedType({
|
||||||
|
label: 'Routes',
|
||||||
|
type: ROUTE,
|
||||||
|
schemas: [
|
||||||
|
{
|
||||||
|
id: ROUTE,
|
||||||
|
type: 'schema',
|
||||||
|
collectionMethods: ['POST'],
|
||||||
|
resourceFields: { spec: { type: ROUTE_SPEC } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ROUTE_SPEC,
|
||||||
|
type: 'schema',
|
||||||
|
resourceFields: {
|
||||||
|
receiver: { type: 'string' },
|
||||||
|
group_by: { type: 'array[string]' },
|
||||||
|
group_wait: { type: 'string' },
|
||||||
|
group_interval: { type: 'string' },
|
||||||
|
repeat_interval: { type: 'string' },
|
||||||
|
match: { type: 'map[string]' },
|
||||||
|
match_re: { type: 'map[string]' },
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
getInstances: () => getAllRoutes(store.dispatch)
|
||||||
|
});
|
||||||
|
|
||||||
basicType([
|
basicType([
|
||||||
'monitoring-overview',
|
'monitoring-overview',
|
||||||
RECEIVER,
|
RECEIVER,
|
||||||
|
ROUTE,
|
||||||
SERVICEMONITOR,
|
SERVICEMONITOR,
|
||||||
PODMONITOR,
|
PODMONITOR,
|
||||||
PROMETHEUSRULE,
|
PROMETHEUSRULE,
|
||||||
|
|
@ -151,6 +148,7 @@ export function init(store) {
|
||||||
mapType(PROMETHEUSRULE, store.getters['i18n/t'](`typeLabel.${ PROMETHEUSRULE }`, { count: 2 }));
|
mapType(PROMETHEUSRULE, store.getters['i18n/t'](`typeLabel.${ PROMETHEUSRULE }`, { count: 2 }));
|
||||||
mapType(ALERTMANAGER, store.getters['i18n/t'](`typeLabel.${ ALERTMANAGER }`, { count: 2 }));
|
mapType(ALERTMANAGER, store.getters['i18n/t'](`typeLabel.${ ALERTMANAGER }`, { count: 2 }));
|
||||||
mapType(RECEIVER, store.getters['i18n/t'](`typeLabel.${ RECEIVER }`, { count: 2 }));
|
mapType(RECEIVER, store.getters['i18n/t'](`typeLabel.${ RECEIVER }`, { count: 2 }));
|
||||||
|
mapType(ROUTE, store.getters['i18n/t'](`typeLabel.${ ROUTE }`, { count: 2 }));
|
||||||
|
|
||||||
weightType(SERVICEMONITOR, 104, true);
|
weightType(SERVICEMONITOR, 104, true);
|
||||||
weightType(PODMONITOR, 103, true);
|
weightType(PODMONITOR, 103, true);
|
||||||
|
|
@ -168,6 +166,17 @@ export function init(store) {
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
headers(ROUTE, [
|
||||||
|
NAME_COL,
|
||||||
|
{
|
||||||
|
name: 'receiver',
|
||||||
|
label: 'Configured Receiver',
|
||||||
|
value: 'spec.receiver',
|
||||||
|
sort: 'spec.receiver',
|
||||||
|
width: '85%'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
headers(ALERTMANAGER, [
|
headers(ALERTMANAGER, [
|
||||||
STATE,
|
STATE,
|
||||||
NAME_COL,
|
NAME_COL,
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,9 @@ export const MONITORING = {
|
||||||
RECEIVER_EMAIL: 'monitoring.coreos.com.receiver.email',
|
RECEIVER_EMAIL: 'monitoring.coreos.com.receiver.email',
|
||||||
RECEIVER_SLACK: 'monitoring.coreos.com.receiver.slack',
|
RECEIVER_SLACK: 'monitoring.coreos.com.receiver.slack',
|
||||||
RECEIVER_WEBHOOK: 'monitoring.coreos.com.receiver.webhook',
|
RECEIVER_WEBHOOK: 'monitoring.coreos.com.receiver.webhook',
|
||||||
RECEIVER_HTTP_CONFIG: 'monitoring.coreos.com.receiver.httpconfig'
|
RECEIVER_HTTP_CONFIG: 'monitoring.coreos.com.receiver.httpconfig',
|
||||||
|
ROUTE: 'monitoring.coreos.com.route',
|
||||||
|
ROUTE_SPEC: 'monitoring.coreos.com.route.spec',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
<script>
|
||||||
|
import CruResource from '@/components/CruResource';
|
||||||
|
import ArrayList from '@/components/form/ArrayList';
|
||||||
|
import KeyValue from '@/components/form/KeyValue';
|
||||||
|
import LabeledInput from '@/components/form/LabeledInput';
|
||||||
|
import LabeledSelect from '@/components/form/LabeledSelect';
|
||||||
|
import CreateEditView from '@/mixins/create-edit-view';
|
||||||
|
import { defaultAsyncData } from '@/components/ResourceDetail';
|
||||||
|
import Tabbed from '@/components/Tabbed';
|
||||||
|
import Tab from '@/components/Tabbed/Tab';
|
||||||
|
import { MONITORING } from '@/config/types';
|
||||||
|
import Banner from '@/components/Banner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ArrayList, Banner, CruResource, KeyValue, LabeledInput, LabeledSelect, Tab, Tabbed
|
||||||
|
},
|
||||||
|
mixins: [CreateEditView],
|
||||||
|
async fetch() {
|
||||||
|
const receivers = await this.$store.dispatch('cluster/findAll', { type: MONITORING.SPOOFED.RECEIVER });
|
||||||
|
|
||||||
|
this.receiverOptions = receivers.map(receiver => receiver.spec.name);
|
||||||
|
},
|
||||||
|
asyncData(ctx) {
|
||||||
|
function yamlSave(value, originalValue) {
|
||||||
|
Object.assign(originalValue, value);
|
||||||
|
originalValue.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultAsyncData(ctx, null, {
|
||||||
|
hideBanner: true, hideAge: true, hideBadgeState: true, yamlSave
|
||||||
|
});
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { receiverOptions: [] };
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
watch: {},
|
||||||
|
methods: {}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CruResource
|
||||||
|
class="route"
|
||||||
|
:done-route="doneRoute"
|
||||||
|
:errors="errors"
|
||||||
|
:mode="mode"
|
||||||
|
:resource="value"
|
||||||
|
:subtypes="[]"
|
||||||
|
@error="e=>errors = e"
|
||||||
|
@finish="save"
|
||||||
|
@cancel="done"
|
||||||
|
>
|
||||||
|
<div v-if="!isView" class="row mb-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="value.spec.name" :disabled="!isCreate" :label="t('generic.name')" :mode="mode" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Banner v-if="value.isRoot" color="info">
|
||||||
|
This is the top-level Route used by Alertmanager as the default destination for any Alerts that do not match any other Routes. This Route must exist and cannot be deleted.
|
||||||
|
</Banner>
|
||||||
|
<Tabbed ref="tabbed" :side-tabs="true" default-tab="overview">
|
||||||
|
<Tab label="Receiver" :weight="2" name="receiver">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledSelect v-model="value.spec.receiver" :options="receiverOptions" label="Receiver" :mode="mode" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab label="Grouping" :weight="1" name="groups">
|
||||||
|
<div class="row mb-20">
|
||||||
|
<div class="col span-6">
|
||||||
|
<span class="label">
|
||||||
|
Group By:
|
||||||
|
</span>
|
||||||
|
<ArrayList v-if="!isView || value.spec.group_by.length > 0" v-model="value.spec.group_by" label="Group By" :mode="mode" :initial-empty-row="true" />
|
||||||
|
<div v-else>
|
||||||
|
{{ t('generic.none') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="divider" />
|
||||||
|
<div class="row mb-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="value.spec.group_wait" label="Group Wait" :mode="mode" />
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="value.spec.group_interval" label="Group Interval" :mode="mode" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-10">
|
||||||
|
<div class="col span-6">
|
||||||
|
<LabeledInput v-model="value.spec.repeat_interval" label="Repeat Interval" :mode="mode" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
<Tab label="Matching" :weight="1" name="matching">
|
||||||
|
<Banner v-if="value.isRoot" color="info">
|
||||||
|
The root route has to match everything so matching can't be configured.
|
||||||
|
</Banner>
|
||||||
|
<div v-else class="row">
|
||||||
|
<div class="col span-6">
|
||||||
|
<span class="label">
|
||||||
|
Match:
|
||||||
|
</span>
|
||||||
|
<KeyValue
|
||||||
|
v-if="!isView || Object.keys(value.spec.match || {}).length > 0"
|
||||||
|
v-model="value.spec.match"
|
||||||
|
:disabled="value.isRoot"
|
||||||
|
:options="receiverOptions"
|
||||||
|
label="Receiver"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<div v-else>
|
||||||
|
{{ t('generic.none') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col span-6">
|
||||||
|
<span class="label">
|
||||||
|
Match Regex:
|
||||||
|
</span>
|
||||||
|
<KeyValue
|
||||||
|
v-if="!isView || Object.keys(value.spec.match_re || {}).length > 0"
|
||||||
|
v-model="value.spec.match_re"
|
||||||
|
:disabled="value.isRoot"
|
||||||
|
:options="receiverOptions"
|
||||||
|
label="Receiver"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
<div v-else>
|
||||||
|
{{ t('generic.none') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tab>
|
||||||
|
</Tabbed>
|
||||||
|
</CruResource>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.route {
|
||||||
|
&[real-mode=view] .label {
|
||||||
|
color: var(--input-label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<script>
|
||||||
|
import ResourceTable from '@/components/ResourceTable';
|
||||||
|
import Loading from '@/components/Loading';
|
||||||
|
import { MONITORING } from '@/config/types';
|
||||||
|
import { areRoutesSupportedFormat, getSecret } from '@/utils/alertmanagerconfig';
|
||||||
|
import { MODE, _EDIT } from '@/config/query-params';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ListRoute',
|
||||||
|
components: { Loading, ResourceTable },
|
||||||
|
|
||||||
|
props: {
|
||||||
|
schema: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const routes = this.$store.dispatch('cluster/findAll', { type: MONITORING.SPOOFED.ROUTE });
|
||||||
|
const secret = await getSecret(this.$store.dispatch);
|
||||||
|
const areSomeRoutesInvalidFormat = areRoutesSupportedFormat(secret);
|
||||||
|
|
||||||
|
if (areSomeRoutesInvalidFormat) {
|
||||||
|
this.$store.dispatch('type-map/removeUncreatableType', { match: MONITORING.SPOOFED.ROUTE });
|
||||||
|
this.rows = await routes;
|
||||||
|
} else {
|
||||||
|
this.$store.dispatch('type-map/uncreatableType', { match: MONITORING.SPOOFED.ROUTE });
|
||||||
|
this.secretTo = { ...secret.detailLocation };
|
||||||
|
this.secretTo.query = { [MODE]: _EDIT };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { rows: null, secretTo: null };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Loading v-if="$fetchState.pending" />
|
||||||
|
<div v-else-if="secretTo">
|
||||||
|
We don't support the current route format stored in alertmanager.yaml. Click
|
||||||
|
<nuxt-link :to="secretTo">
|
||||||
|
here
|
||||||
|
</nuxt-link> to update manually.
|
||||||
|
</div>
|
||||||
|
<ResourceTable v-else :schema="schema" :rows="rows" />
|
||||||
|
</template>
|
||||||
|
|
@ -1,11 +1,4 @@
|
||||||
import jsyaml from 'js-yaml';
|
import { canCreate, updateConfig } from '@/utils/alertmanagerconfig';
|
||||||
import { base64Decode, base64Encode } from '@/utils/crypto';
|
|
||||||
import { MONITORING, SECRET } from '@/config/types';
|
|
||||||
|
|
||||||
const DEFAULT_SECRET_ID = 'cattle-monitoring-system/alertmanager-rancher-monitoring-alertmanager';
|
|
||||||
const ALERTMANAGER_ID = 'cattle-monitoring-system/rancher-monitoring-alertmanager';
|
|
||||||
|
|
||||||
export const FILENAME = 'alertmanager.yaml';
|
|
||||||
|
|
||||||
export const RECEIVERS_TYPES = [
|
export const RECEIVERS_TYPES = [
|
||||||
{
|
{
|
||||||
|
|
@ -38,21 +31,14 @@ export const RECEIVERS_TYPES = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function getSecretId(dispatch, useCluster = true) {
|
|
||||||
const action = useCluster ? 'cluster/find' : 'find';
|
|
||||||
const alertManager = await dispatch(action, { type: MONITORING.ALERTMANAGER, id: ALERTMANAGER_ID });
|
|
||||||
|
|
||||||
return alertManager?.spec?.configSecret || DEFAULT_SECRET_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
removeSerially() {
|
removeSerially() {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
return async() => {
|
return () => {
|
||||||
await this.updateReceivers((currentReceivers) => {
|
return this.updateReceivers((currentReceivers) => {
|
||||||
return currentReceivers.filter(r => r.name !== this.spec?.name);
|
return currentReceivers.filter(r => r.name !== this.spec?.name);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -77,7 +63,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
canUpdate() {
|
canUpdate() {
|
||||||
return this.$rootGetters['cluster/byId'](SECRET, DEFAULT_SECRET_ID)?.canUpdate;
|
return this.secret.canUpdate;
|
||||||
},
|
},
|
||||||
|
|
||||||
canCustomEdit() {
|
canCustomEdit() {
|
||||||
|
|
@ -85,11 +71,11 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
canCreate() {
|
canCreate() {
|
||||||
return this.$rootGetters['type-map/isCreatable'](SECRET);
|
return canCreate(this.$rootGetters);
|
||||||
},
|
},
|
||||||
|
|
||||||
canDelete() {
|
canDelete() {
|
||||||
return this.$rootGetters['cluster/byId'](SECRET, DEFAULT_SECRET_ID)?.canDelete;
|
return this.secret.canDelete;
|
||||||
},
|
},
|
||||||
|
|
||||||
canViewInApi() {
|
canViewInApi() {
|
||||||
|
|
@ -121,46 +107,7 @@ export default {
|
||||||
return types;
|
return types;
|
||||||
},
|
},
|
||||||
|
|
||||||
async secret() {
|
|
||||||
const secretId = await getSecretId(this.$dispatch, false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this.$dispatch('find', { type: SECRET, id: secretId });
|
|
||||||
} catch (ex) {
|
|
||||||
const [namespace, name] = secretId.split('/');
|
|
||||||
const secret = await this.$dispatch('create', { type: SECRET });
|
|
||||||
|
|
||||||
secret.metadata = {
|
|
||||||
namespace,
|
|
||||||
name
|
|
||||||
};
|
|
||||||
|
|
||||||
return secret;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateReceivers() {
|
updateReceivers() {
|
||||||
return async(fn) => {
|
return fn => updateConfig(this.$dispatch, 'receivers', this.type, fn);
|
||||||
const secret = await this.secret;
|
|
||||||
|
|
||||||
secret.data = secret.data || {};
|
|
||||||
const file = secret.data[FILENAME];
|
|
||||||
const decodedFile = file ? base64Decode(file) : null;
|
|
||||||
const loadedFile = decodedFile ? jsyaml.safeLoad(decodedFile) : {};
|
|
||||||
|
|
||||||
loadedFile.receivers = loadedFile.receivers || [];
|
|
||||||
|
|
||||||
const newReceivers = fn(loadedFile.receivers);
|
|
||||||
|
|
||||||
loadedFile.receivers = newReceivers;
|
|
||||||
|
|
||||||
const newFile = jsyaml.safeDump(loadedFile);
|
|
||||||
const encodedFile = base64Encode(newFile);
|
|
||||||
|
|
||||||
secret.data[FILENAME] = encodedFile;
|
|
||||||
await secret.save();
|
|
||||||
// Force a store update
|
|
||||||
await this.$dispatch('findAll', { type: this.type, opt: { force: true } });
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { isEmpty, set } from '@/utils/object';
|
||||||
|
import { areRoutesSupportedFormat, canCreate, updateConfig } from '@/utils/alertmanagerconfig';
|
||||||
|
|
||||||
|
export const ROOT_NAME = 'root';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
applyDefaults() {
|
||||||
|
return () => {
|
||||||
|
const spec = this.spec || {};
|
||||||
|
|
||||||
|
spec.group_by = spec.group_by || [];
|
||||||
|
spec.group_wait = spec.group_wait || '30s';
|
||||||
|
spec.group_interval = spec.group_interval || '5m';
|
||||||
|
spec.repeat_interval = spec.repeat_interval || '4h';
|
||||||
|
spec.match = spec.match || {};
|
||||||
|
spec.match_re = spec.match || {};
|
||||||
|
|
||||||
|
set(this, 'spec', spec);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
removeSerially() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
return () => {
|
||||||
|
return this.updateRoutes((currentRoutes) => {
|
||||||
|
return currentRoutes.filter(r => r.name !== this.spec?.name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
return async() => {
|
||||||
|
const errors = await this.validationErrors(this);
|
||||||
|
|
||||||
|
if (!isEmpty(errors)) {
|
||||||
|
return Promise.reject(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.updateRoutes((currentRoutes) => {
|
||||||
|
const existingRoute = currentRoutes.find(r => r.name === this.spec?.name);
|
||||||
|
|
||||||
|
if (existingRoute) {
|
||||||
|
Object.assign(existingRoute, this.spec);
|
||||||
|
} else {
|
||||||
|
currentRoutes.push(this.spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentRoutes;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
canUpdate() {
|
||||||
|
return this.secret.canUpdate;
|
||||||
|
},
|
||||||
|
|
||||||
|
canCustomEdit() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
canCreate() {
|
||||||
|
return canCreate(this.$rootGetters) && areRoutesSupportedFormat(this.secret);
|
||||||
|
},
|
||||||
|
|
||||||
|
canDelete() {
|
||||||
|
return !this.isRoot && this.secret.canDelete;
|
||||||
|
},
|
||||||
|
|
||||||
|
canViewInApi() {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
canYaml() {
|
||||||
|
return areRoutesSupportedFormat(this.secret);
|
||||||
|
},
|
||||||
|
|
||||||
|
customValidationRules() {
|
||||||
|
const rules = [
|
||||||
|
{
|
||||||
|
nullable: false,
|
||||||
|
path: 'spec.name',
|
||||||
|
required: true,
|
||||||
|
translationKey: 'generic.name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nullable: false,
|
||||||
|
path: 'spec.receiver',
|
||||||
|
required: true,
|
||||||
|
translationKey: 'monitoring.route.fields.receiver'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'spec.group_wait',
|
||||||
|
validators: ['interval'],
|
||||||
|
translationKey: 'monitoring.route.fields.groupWait'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'spec.group_interval',
|
||||||
|
validators: ['interval'],
|
||||||
|
translationKey: 'monitoring.route.fields.groupInterval'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'spec.repeat_interval',
|
||||||
|
validators: ['interval'],
|
||||||
|
translationKey: 'monitoring.route.fields.repeatInterval'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!this.isRoot) {
|
||||||
|
rules.push({
|
||||||
|
path: 'spec',
|
||||||
|
validators: ['matching']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRoutes() {
|
||||||
|
return fn => updateConfig(this.$dispatch, 'route.routes', this.type, fn);
|
||||||
|
},
|
||||||
|
|
||||||
|
isRoot() {
|
||||||
|
return this.id === ROOT_NAME;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1236,7 +1236,7 @@ export default {
|
||||||
const validatorExists = Object.prototype.hasOwnProperty.call(CustomValidators, validatorName);
|
const validatorExists = Object.prototype.hasOwnProperty.call(CustomValidators, validatorName);
|
||||||
|
|
||||||
if (!isEmpty(validatorName) && validatorExists) {
|
if (!isEmpty(validatorName) && validatorExists) {
|
||||||
CustomValidators[validatorName](pathValue, this.$rootGetters, errors, validatorArgs);
|
CustomValidators[validatorName](pathValue, this.$rootGetters, errors, validatorArgs, displayKey);
|
||||||
} else if (!isEmpty(validatorName) && !validatorExists) {
|
} else if (!isEmpty(validatorName) && !validatorExists) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.warn(this.t('validation.custom.missing', { validatorName }));
|
console.warn(this.t('validation.custom.missing', { validatorName }));
|
||||||
|
|
|
||||||
|
|
@ -1214,6 +1214,15 @@ export const mutations = {
|
||||||
state.uncreatable.push(match);
|
state.uncreatable.push(match);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeUncreatableType(state, {match}) {
|
||||||
|
match = ensureRegex(match);
|
||||||
|
match = regexToString(match);
|
||||||
|
const matchingIndex = state.uncreatable.findIndex((regex) => regex === match);
|
||||||
|
if (matchingIndex >= 0) {
|
||||||
|
state.uncreatable.splice(matchingIndex, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
immutableType(state, { match }) {
|
immutableType(state, { match }) {
|
||||||
match = ensureRegex(match);
|
match = ensureRegex(match);
|
||||||
match = regexToString(match);
|
match = regexToString(match);
|
||||||
|
|
@ -1261,6 +1270,12 @@ export const actions = {
|
||||||
|
|
||||||
dispatch('prefs/set', { key: EXPANDED_GROUPS, value: groups }, { root: true });
|
dispatch('prefs/set', { key: EXPANDED_GROUPS, value: groups }, { root: true });
|
||||||
},
|
},
|
||||||
|
uncreatableType({ commit }, match) {
|
||||||
|
commit(`uncreatableType`, match);
|
||||||
|
},
|
||||||
|
removeUncreatableType({ commit }, match) {
|
||||||
|
commit(`removeUncreatableType`, match);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function _sortGroup(tree, mode) {
|
function _sortGroup(tree, mode) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
import jsyaml from 'js-yaml';
|
||||||
|
import { base64Decode, base64Encode } from '@/utils/crypto';
|
||||||
|
import { MONITORING, SECRET } from '@/config/types';
|
||||||
|
import { get, set } from '@/utils/object';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import { ROOT_NAME } from '@/models/monitoring.coreos.com.route';
|
||||||
|
|
||||||
|
const DEFAULT_SECRET_ID = 'cattle-monitoring-system/alertmanager-rancher-monitoring-alertmanager';
|
||||||
|
const ALERTMANAGER_ID = 'cattle-monitoring-system/rancher-monitoring-alertmanager';
|
||||||
|
|
||||||
|
export const FILENAME = 'alertmanager.yaml';
|
||||||
|
|
||||||
|
export async function getSecretId(dispatch) {
|
||||||
|
const isLocal = dispatch.name === 'boundDispatch';
|
||||||
|
const action = isLocal ? 'cluster/find' : 'find';
|
||||||
|
const alertManager = await dispatch(action, { type: MONITORING.ALERTMANAGER, id: ALERTMANAGER_ID });
|
||||||
|
|
||||||
|
return alertManager?.spec?.configSecret || DEFAULT_SECRET_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSecret(dispatch) {
|
||||||
|
const secretId = await getSecretId(dispatch, false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isLocal = dispatch.name === 'boundDispatch';
|
||||||
|
const action = isLocal ? 'cluster/find' : 'find';
|
||||||
|
|
||||||
|
return await dispatch(action, { type: SECRET, id: secretId });
|
||||||
|
} catch (ex) {
|
||||||
|
const [namespace, name] = secretId.split('/');
|
||||||
|
const secret = await dispatch('create', { type: SECRET });
|
||||||
|
|
||||||
|
secret.metadata = {
|
||||||
|
namespace,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractConfig(secret) {
|
||||||
|
secret.data = secret.data || {};
|
||||||
|
const file = secret.data[FILENAME];
|
||||||
|
const decodedFile = file ? base64Decode(file) : '{}';
|
||||||
|
const config = jsyaml.safeLoad(decodedFile);
|
||||||
|
|
||||||
|
config.receivers = config.receivers || [];
|
||||||
|
config.route = config.route || {};
|
||||||
|
config.route.routes = config.route.routes || [];
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadConfig(dispatch) {
|
||||||
|
const secret = await getSecret(dispatch);
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: extractConfig(secret),
|
||||||
|
secret
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateConfig(dispatch, path, type, updateFn) {
|
||||||
|
const { config, secret } = await loadConfig(dispatch);
|
||||||
|
|
||||||
|
set(config, path, get(config, path) || []);
|
||||||
|
|
||||||
|
const newValue = updateFn(get(config, path));
|
||||||
|
|
||||||
|
set(config, path, newValue);
|
||||||
|
|
||||||
|
const routes = config.route.routes;
|
||||||
|
const rootIndex = routes.findIndex(route => route.name === ROOT_NAME);
|
||||||
|
|
||||||
|
if (rootIndex >= 0) {
|
||||||
|
const rootRoute = routes.splice(rootIndex, 1)[0];
|
||||||
|
|
||||||
|
rootRoute.routes = routes;
|
||||||
|
config.route = rootRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFile = jsyaml.safeDump(config);
|
||||||
|
const encodedFile = base64Encode(newFile);
|
||||||
|
|
||||||
|
secret.data[FILENAME] = encodedFile;
|
||||||
|
await secret.save();
|
||||||
|
// Force a store update
|
||||||
|
await dispatch('findAll', { type, opt: { force: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllReceivers(dispatch) {
|
||||||
|
try {
|
||||||
|
const { config, secret } = await loadConfig(dispatch);
|
||||||
|
const receivers = config.receivers || [];
|
||||||
|
const receiversWithName = receivers.filter(receiver => receiver.name);
|
||||||
|
const mapped = receiversWithName.map(receiver => dispatch('cluster/create', {
|
||||||
|
id: receiver.name,
|
||||||
|
spec: receiver,
|
||||||
|
type: MONITORING.SPOOFED.RECEIVER,
|
||||||
|
secret
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Promise.all(mapped);
|
||||||
|
} catch (ex) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllRoutes(dispatch) {
|
||||||
|
try {
|
||||||
|
const { config, secret } = await loadConfig(dispatch);
|
||||||
|
|
||||||
|
config.route = config.route || {};
|
||||||
|
config.route.name = ROOT_NAME;
|
||||||
|
const routes = config.route?.routes || [];
|
||||||
|
const routesWithName = routes.filter(route => route.name);
|
||||||
|
|
||||||
|
routesWithName.push(config.route);
|
||||||
|
|
||||||
|
const mapped = routesWithName.map(route => dispatch('cluster/create', {
|
||||||
|
id: route.name,
|
||||||
|
spec: route,
|
||||||
|
type: MONITORING.SPOOFED.ROUTE,
|
||||||
|
secret
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Promise.all(mapped);
|
||||||
|
} catch (ex) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function areRoutesSupportedFormat(secret) {
|
||||||
|
try {
|
||||||
|
const config = extractConfig(secret);
|
||||||
|
const routes = config.route?.routes || [];
|
||||||
|
|
||||||
|
return !routes.some(isEmpty);
|
||||||
|
} catch (ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canCreate(rootGetters) {
|
||||||
|
return rootGetters['type-map/isCreatable'](SECRET);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { flowOutput } from '@/utils/validators/flow-output';
|
import { flowOutput } from '@/utils/validators/flow-output';
|
||||||
import { clusterIp, externalName, servicePort } from '@/utils/validators/service';
|
import { clusterIp, externalName, servicePort } from '@/utils/validators/service';
|
||||||
import { ruleGroups, groupsAreValid } from '@/utils/validators/prometheusrule';
|
import { ruleGroups, groupsAreValid } from '@/utils/validators/prometheusrule';
|
||||||
|
import { interval, matching } from '@/utils/validators/monitoring-route';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom validation functions beyond normal scalr types
|
* Custom validation functions beyond normal scalr types
|
||||||
|
|
@ -13,5 +14,7 @@ export default {
|
||||||
flowOutput,
|
flowOutput,
|
||||||
groupsAreValid,
|
groupsAreValid,
|
||||||
ruleGroups,
|
ruleGroups,
|
||||||
|
interval,
|
||||||
servicePort,
|
servicePort,
|
||||||
|
matching
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
|
||||||
|
export function matching(spec, getters, errors, validatorArgs) {
|
||||||
|
if (isEmpty(spec?.match) && isEmpty(spec?.['match_re'])) {
|
||||||
|
errors.push(getters['i18n/t']('validation.monitoring.route.match'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function interval(value, getters, errors, validatorArgs, displayKey) {
|
||||||
|
if (!/^\d+[hms]$/.test(value)) {
|
||||||
|
errors.push(getters['i18n/t']('validation.monitoring.route.interval', { key: displayKey }));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue