mirror of https://github.com/rancher/dashboard.git
397 lines
12 KiB
Vue
397 lines
12 KiB
Vue
<script>
|
|
|
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
import { _EDIT } from '@shell/config/query-params';
|
|
import MatchExpressions from '@shell/components/form/MatchExpressions';
|
|
import { convert, simplify } from '@shell/utils/selector';
|
|
import { NAMESPACE, POD } from '@shell/config/types';
|
|
import ArrayList from '@shell/components/form/ArrayList';
|
|
import { Banner } from '@components/Banner';
|
|
import debounce from 'lodash/debounce';
|
|
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
|
import { matching } from '@shell/utils/selector-typed';
|
|
|
|
const TARGET_OPTIONS = {
|
|
IP_BLOCK: 'ipBlock',
|
|
NAMESPACE_SELECTOR: 'namespaceSelector',
|
|
POD_SELECTOR: 'podSelector',
|
|
NAMESPACE_AND_POD_SELECTOR: 'namespaceAndPodSelector',
|
|
};
|
|
|
|
// Components shown for Network Policy --> Ingress/Egress Rules --> Rule Type are...
|
|
// Edit Network Policy --> `PolicyRules` 1 --> M `PolicyRule` 1 --> M `PolicyRuleTarget`
|
|
|
|
export default {
|
|
components: {
|
|
ArrayList, Banner, LabeledInput, LabeledSelect, MatchExpressions
|
|
},
|
|
props: {
|
|
value: {
|
|
type: Object,
|
|
default: () => {
|
|
return {};
|
|
},
|
|
},
|
|
mode: {
|
|
type: String,
|
|
default: _EDIT,
|
|
},
|
|
type: {
|
|
type: String,
|
|
default: 'ingress'
|
|
},
|
|
namespace: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
},
|
|
data() {
|
|
if (!this.value[TARGET_OPTIONS.IP_BLOCK] &&
|
|
!this.value[TARGET_OPTIONS.POD_SELECTOR] &&
|
|
!this.value[TARGET_OPTIONS.NAMESPACE_SELECTOR] &&
|
|
!this.value[TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR]
|
|
) {
|
|
this.$nextTick(() => {
|
|
this.value[TARGET_OPTIONS.IP_BLOCK] = {};
|
|
});
|
|
}
|
|
|
|
return {
|
|
portOptions: ['TCP', 'UDP'],
|
|
matchingPods: {
|
|
matches: [], matched: 0, total: 0
|
|
},
|
|
matchingNamespaces: {
|
|
matches: [], matched: 0, total: 0
|
|
},
|
|
invalidCidr: null,
|
|
invalidCidrs: [],
|
|
POD,
|
|
TARGET_OPTIONS,
|
|
targetOptions: Object.values(TARGET_OPTIONS),
|
|
inStore: this.$store.getters['currentProduct'].inStore,
|
|
debouncedUpdateMatches: debounce(this.updateMatches, 500)
|
|
};
|
|
},
|
|
computed: {
|
|
/**
|
|
* of type matchExpression aka `KubeLabelSelectorExpression[]`
|
|
*/
|
|
podSelectorExpressions: {
|
|
get() {
|
|
return convert(
|
|
this.value[TARGET_OPTIONS.POD_SELECTOR]?.matchLabels || {},
|
|
this.value[TARGET_OPTIONS.POD_SELECTOR]?.matchExpressions || []
|
|
);
|
|
},
|
|
set(podSelectorExpressions) {
|
|
this.value[TARGET_OPTIONS.POD_SELECTOR] = simplify(podSelectorExpressions);
|
|
}
|
|
},
|
|
/**
|
|
* of type matchExpression aka `KubeLabelSelectorExpression[]`
|
|
*/
|
|
namespaceSelectorExpressions: {
|
|
get() {
|
|
return convert(
|
|
this.value[TARGET_OPTIONS.NAMESPACE_SELECTOR]?.matchLabels || {},
|
|
this.value[TARGET_OPTIONS.NAMESPACE_SELECTOR]?.matchExpressions || []
|
|
);
|
|
},
|
|
set(namespaceSelectorExpressions) {
|
|
this.value[TARGET_OPTIONS.NAMESPACE_SELECTOR] = simplify(namespaceSelectorExpressions);
|
|
}
|
|
},
|
|
selectTargetOptions() {
|
|
const selectTargetOptions = [];
|
|
|
|
for (const option of this.targetOptions) {
|
|
selectTargetOptions.push({
|
|
label: this.t(`networkpolicy.rules.${ option }.label`),
|
|
value: option,
|
|
});
|
|
}
|
|
|
|
return selectTargetOptions;
|
|
},
|
|
targetType: {
|
|
get() {
|
|
for (const option of this.targetOptions) {
|
|
if (this.value[TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR] || (this.value[TARGET_OPTIONS.NAMESPACE_SELECTOR] && this.value[TARGET_OPTIONS.POD_SELECTOR])) {
|
|
return TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR;
|
|
}
|
|
if (this.value[option]) {
|
|
return option;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
set(targetType) {
|
|
delete this.value[TARGET_OPTIONS.IP_BLOCK];
|
|
delete this.value[TARGET_OPTIONS.NAMESPACE_SELECTOR];
|
|
delete this.value[TARGET_OPTIONS.POD_SELECTOR];
|
|
delete this.value[TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR];
|
|
this.$nextTick(() => {
|
|
this.value[targetType] = {};
|
|
});
|
|
}
|
|
},
|
|
matchingNamespacesAndPods() {
|
|
return {
|
|
policyNamespace: this.namespace,
|
|
...Object.keys(this.matchingNamespaces).reduce((acc, k) => ({ ...acc, [`${ k }Namespaces`]: this.matchingNamespaces[k] }), {}),
|
|
...Object.keys(this.matchingPods).reduce((acc, k) => ({ ...acc, [`${ k }Pods`]: this.matchingPods[k] }), {}),
|
|
};
|
|
}
|
|
},
|
|
watch: {
|
|
namespace: {
|
|
handler: 'debouncedUpdateMatches',
|
|
immediate: true
|
|
},
|
|
'value.podSelector': {
|
|
handler: 'debouncedUpdateMatches',
|
|
immediate: true
|
|
},
|
|
'value.namespaceSelector': {
|
|
handler: 'debouncedUpdateMatches',
|
|
immediate: true
|
|
},
|
|
'value.ipBlock.cidr': 'validateCIDR',
|
|
'value.ipBlock.except': 'validateCIDR',
|
|
podSelectorExpressions: {
|
|
handler: 'debouncedUpdateMatches',
|
|
immediate: true
|
|
},
|
|
namespaceSelectorExpressions: {
|
|
handler: 'debouncedUpdateMatches',
|
|
immediate: true
|
|
}
|
|
},
|
|
|
|
fetch() {
|
|
this.debouncedUpdateMatches();
|
|
},
|
|
|
|
methods: {
|
|
async updateMatches() {
|
|
// Note - needs to be sequential as getMatchingPods requires matchingNamespaces to be up-to-date
|
|
this.matchingNamespaces = await this.getMatchingNamespaces();
|
|
this.matchingPods = await this.getMatchingPods();
|
|
},
|
|
|
|
validateCIDR() {
|
|
const exceptCidrs = this.value[TARGET_OPTIONS.IP_BLOCK]?.except || [];
|
|
|
|
this.invalidCidrs = exceptCidrs
|
|
.filter((cidr) => !isValidCIDR(cidr))
|
|
.map((invalidCidr) => invalidCidr || '<blank>');
|
|
|
|
if (this.value[TARGET_OPTIONS.IP_BLOCK]?.cidr && !isValidCIDR(this.value[TARGET_OPTIONS.IP_BLOCK].cidr)) {
|
|
this.invalidCidr = this.value[TARGET_OPTIONS.IP_BLOCK].cidr;
|
|
} else {
|
|
this.invalidCidr = null;
|
|
}
|
|
},
|
|
|
|
async getMatchingPods() {
|
|
return await matching({
|
|
labelSelector: { matchExpressions: this.podSelectorExpressions },
|
|
type: POD,
|
|
$store: this.$store,
|
|
inStore: this.inStore,
|
|
namespace: this.targetType === TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR ? this.matchingNamespaces.matches.map((ns) => ns.id) : this.namespace,
|
|
transient: true,
|
|
});
|
|
},
|
|
async getMatchingNamespaces() {
|
|
return await matching({
|
|
labelSelector: { matchExpressions: this.namespaceSelectorExpressions },
|
|
type: NAMESPACE,
|
|
$store: this.$store,
|
|
inStore: this.inStore,
|
|
transient: true,
|
|
});
|
|
},
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rule">
|
|
<div class="row mb-20">
|
|
<div class="col span-6">
|
|
<LabeledSelect
|
|
v-model:value="targetType"
|
|
data-testid="policy-rule-target-type-labeled-select"
|
|
:mode="mode"
|
|
:tooltip="targetType === TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR ? t('networkpolicy.selectors.matchingNamespacesAndPods.tooltip') : null"
|
|
:options="selectTargetOptions"
|
|
:multiple="false"
|
|
:label="t('networkpolicy.rules.type')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div v-if="targetType === TARGET_OPTIONS.IP_BLOCK">
|
|
<div class="row">
|
|
<div class="col span-6">
|
|
<LabeledInput
|
|
v-model:value="value[TARGET_OPTIONS.IP_BLOCK].cidr"
|
|
data-testid="labeled-input-ip-block-selector"
|
|
:mode="mode"
|
|
:placeholder="t('networkpolicy.rules.ipBlock.cidr.placeholder')"
|
|
:label="t('networkpolicy.rules.ipBlock.cidr.label')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="invalidCidr"
|
|
class="row"
|
|
>
|
|
<div class="col span-12">
|
|
<Banner color="error">
|
|
<t k="networkpolicy.rules.ipBlock.invalidCidr" />
|
|
</Banner>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-20">
|
|
<div class="col span-12">
|
|
<ArrayList
|
|
v-model:value="value[TARGET_OPTIONS.IP_BLOCK].except"
|
|
:add-label="t('networkpolicy.rules.ipBlock.addExcept')"
|
|
:mode="mode"
|
|
:show-header="true"
|
|
:value-label="t('networkpolicy.rules.ipBlock.exceptions')"
|
|
:value-placeholder="t('networkpolicy.rules.ipBlock.cidr.placeholder')"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="invalidCidrs.length"
|
|
class="row mb-10"
|
|
>
|
|
<div class="col span-12">
|
|
<Banner color="error">
|
|
<t k="networkpolicy.rules.ipBlock.invalidExceptionCidrs" />{{ invalidCidrs.join(', ') }}
|
|
</Banner>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="targetType === TARGET_OPTIONS.POD_SELECTOR">
|
|
<div class="row">
|
|
<div class="col span-12">
|
|
<Banner color="success">
|
|
<span v-clean-html="t('networkpolicy.selectors.matchingPods.matchesSome', matchingPods)" />
|
|
</Banner>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-0">
|
|
<div class="col span-12">
|
|
<MatchExpressions
|
|
v-model:value="podSelectorExpressions"
|
|
data-testid="match-expression-pod-selector"
|
|
:mode="mode"
|
|
:show-remove="false"
|
|
:initial-empty-row="true"
|
|
:type="POD"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="targetType === TARGET_OPTIONS.NAMESPACE_SELECTOR">
|
|
<div class="row">
|
|
<div class="col span-12">
|
|
<Banner color="success">
|
|
<span
|
|
v-clean-html="t('networkpolicy.selectors.matchingNamespaces.matchesSome', matchingNamespaces)"
|
|
data-testid="matching-namespaces-message"
|
|
/>
|
|
</Banner>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-0">
|
|
<div class="col span-12">
|
|
<MatchExpressions
|
|
v-model:value="namespaceSelectorExpressions"
|
|
data-testid="match-expression-namespace-selector"
|
|
:mode="mode"
|
|
:show-remove="false"
|
|
:initial-empty-row="true"
|
|
:type="POD"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="targetType === TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR">
|
|
<div class="row">
|
|
<div class="col span-12">
|
|
<Banner color="success">
|
|
<span
|
|
v-if="!namespaceSelectorExpressions.length"
|
|
v-clean-html="t('networkpolicy.selectors.matchingPods.matchesSome', matchingPods)"
|
|
/>
|
|
<span
|
|
v-else
|
|
v-clean-html="t('networkpolicy.selectors.matchingNamespacesAndPods.matchesSome', matchingNamespacesAndPods)"
|
|
/>
|
|
</Banner>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-0">
|
|
<div class="col span-1 namespace-pod-rule">
|
|
<span class="label">
|
|
{{ t('networkpolicy.rules.namespace') }}
|
|
</span>
|
|
</div>
|
|
<div class="col span-11">
|
|
<MatchExpressions
|
|
v-model:value="namespaceSelectorExpressions"
|
|
data-testid="match-expression-namespace-and-pod-selector-ns-rule"
|
|
:mode="mode"
|
|
:show-add-button="false"
|
|
:show-remove-button="false"
|
|
:show-remove="false"
|
|
:initial-empty-row="true"
|
|
:type="POD"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-0">
|
|
<div class="col span-1 namespace-pod-rule">
|
|
<span class="label">
|
|
{{ t('networkpolicy.rules.pod') }}
|
|
</span>
|
|
</div>
|
|
<div class="col span-11">
|
|
<MatchExpressions
|
|
v-model:value="podSelectorExpressions"
|
|
data-testid="match-expression-namespace-and-pod-selector-pod-rule"
|
|
:mode="mode"
|
|
:show-add-button="false"
|
|
:show-remove-button="false"
|
|
:show-remove="false"
|
|
:initial-empty-row="true"
|
|
:type="POD"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang='scss' scoped>
|
|
.namespace-pod-rule {
|
|
display: table;
|
|
width: 100px;
|
|
padding: 0, 10px, 0, 0;
|
|
text-align: center;
|
|
|
|
.label {
|
|
display:table-cell;
|
|
vertical-align:middle;
|
|
}
|
|
}
|
|
</style>
|