dashboard/shell/edit/networking.k8s.io.networkpo.../PolicyRuleTarget.vue

383 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, matching, simplify } from '@shell/utils/selector';
import { POD } from '@shell/config/types';
import ArrayList from '@shell/components/form/ArrayList';
import { Banner } from '@components/Banner';
import throttle from 'lodash/throttle';
import { isValidCIDR } from '@shell/utils/validators/cidr';
const TARGET_OPTIONS = {
IP_BLOCK: 'ipBlock',
NAMESPACE_SELECTOR: 'namespaceSelector',
POD_SELECTOR: 'podSelector',
NAMESPACE_AND_POD_SELECTOR: 'namespaceAndPodSelector',
};
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: ''
},
allPods: {
type: Array,
default: () => {
return [];
},
},
allNamespaces: {
type: Array,
default: () => {
return [];
},
},
},
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.$set(this.value, TARGET_OPTIONS.IP_BLOCK, {});
});
}
return {
portOptions: ['TCP', 'UDP'],
matchingPods: {},
matchingNamespaces: {},
invalidCidr: null,
invalidCidrs: [],
POD,
TARGET_OPTIONS,
targetOptions: Object.values(TARGET_OPTIONS),
throttleTime: 250,
};
},
computed: {
podSelectorExpressions: {
get() {
return convert(
this.value[TARGET_OPTIONS.POD_SELECTOR]?.matchLabels || {},
this.value[TARGET_OPTIONS.POD_SELECTOR]?.matchExpressions || []
);
},
set(podSelectorExpressions) {
this.$set(this.value, TARGET_OPTIONS.POD_SELECTOR, simplify(podSelectorExpressions));
}
},
namespaceSelectorExpressions: {
get() {
return convert(
this.value[TARGET_OPTIONS.NAMESPACE_SELECTOR]?.matchLabels || {},
this.value[TARGET_OPTIONS.NAMESPACE_SELECTOR]?.matchExpressions || []
);
},
set(namespaceSelectorExpressions) {
this.$set(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) {
this.$delete(this.value, TARGET_OPTIONS.IP_BLOCK);
this.$delete(this.value, TARGET_OPTIONS.NAMESPACE_SELECTOR);
this.$delete(this.value, TARGET_OPTIONS.POD_SELECTOR);
this.$delete(this.value, TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR);
this.$nextTick(() => {
this.$set(this.value, targetType, {});
});
}
},
updateMatches() {
return {
handler: throttle(function() {
this.matchingNamespaces = this.getMatchingNamespaces();
this.matchingPods = this.getMatchingPods();
}, this.throttle, { leading: true }),
immediate: true
};
},
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: 'updateMatches',
'value.podSelector': 'updateMatches',
'value.namespaceSelector': 'updateMatches',
'value.ipBlock.cidr': 'validateCIDR',
'value.ipBlock.except': 'validateCIDR',
podSelectorExpressions: 'updateMatches',
namespaceSelectorExpressions: 'updateMatches',
},
methods: {
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;
}
},
getMatchingPods() {
const namespaces = this.targetType === TARGET_OPTIONS.NAMESPACE_AND_POD_SELECTOR ? this.matchingNamespaces.matches : [{ id: this.namespace }];
const allInNamespace = this.allPods.filter((pod) => namespaces.some((ns) => ns.id === pod.metadata.namespace));
const match = matching(allInNamespace, this.podSelectorExpressions);
const matched = match.length || 0;
const sample = match[0]?.nameDisplay;
return {
matched,
matches: match,
none: matched === 0,
sample,
total: allInNamespace.length,
};
},
getMatchingNamespaces() {
const allNamespaces = this.allNamespaces;
const match = matching(allNamespaces, this.namespaceSelectorExpressions);
const matched = match.length || 0;
const sample = match[0]?.nameDisplay;
return {
matched,
matches: match,
none: matched === 0,
sample,
total: allNamespaces.length,
};
},
}
};
</script>
<template>
<div class="rule">
<div class="row mb-20">
<div class="col span-6">
<LabeledSelect
v-model="targetType"
data-testid="labeled-select-type-selector"
: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[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[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="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)" />
</Banner>
</div>
</div>
<div class="row mb-0">
<div class="col span-12">
<MatchExpressions
v-model="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="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="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>