feat: add ACL tab in create subnet page (#527)

* feat: add ACL tab in create subnet page

Signed-off-by: Andy Lee <andy.lee@suse.com>

* fix: typo

Signed-off-by: Andy Lee <andy.lee@suse.com>

---------

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2025-09-22 16:13:13 +08:00 committed by GitHub
parent f652ed9d4b
commit 9fdbe9c58f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 233 additions and 3 deletions

View File

@ -0,0 +1,196 @@
<script>
import debounce from 'lodash/debounce';
import { _EDIT, _VIEW } from '@shell/config/query-params';
import { removeAt } from '@shell/utils/array';
import { Banner } from '@components/Banner';
import InfoBox from '@shell/components/InfoBox';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { LabeledInput } from '@components/Form/LabeledInput';
export default {
name: 'AccessControlList',
emits: ['update:value'],
components: {
Banner, InfoBox, LabeledSelect, LabeledInput
},
props: {
value: {
type: Array,
default: null,
},
mode: {
type: String,
default: _EDIT,
},
},
data() {
const rows = (this.value || []).map((row) => {
return {
action: row.action || '',
direction: row.direction || '',
priority: row.priority || 0,
match: row.match || '',
};
});
return { rows };
},
computed: {
isView() {
return this.mode === _VIEW;
},
actionOptions() {
return [
{ label: 'Allow', value: 'allow' },
{ label: 'Drop', value: 'drop' },
{ label: 'Pass', value: 'pass' },
{ label: 'Reject', value: 'reject' },
{ label: 'Allow-related', value: 'allow-related' },
{ label: 'Allow-stateless', value: 'allow-stateless' },
];
},
directionOptions() {
return [
{ label: 'To-lport', value: 'to-lport' },
{ label: 'From-lport', value: 'from-lport' },
];
}
},
created() {
this.queueUpdate = debounce(this.update, 100);
},
methods: {
add() {
this.rows.push({
action: '',
direction: '',
priority: this.rows.length,
match: '',
});
this.queueUpdate();
},
removeRule(idx) {
removeAt(this.rows, idx);
this.queueUpdate();
},
update() {
if (this.isView) {
return;
}
this.$emit('update:value', this.rows);
}
},
};
</script>
<template>
<div>
<Banner
v-if="rows.length > 0"
color="info"
>
<t
k="harvester.subnet.acl.banner"
:raw="true"
/>
</Banner>
<div
v-for="(row, idx) in rows"
:key="idx"
>
<InfoBox class="box">
<button
type="button"
class="role-link btn btn-sm removeBtn"
:disabled="isView"
@click="removeRule(idx)"
>
<i class="icon icon-x" />
</button>
<div class="row">
<div class="col span-4">
<LabeledSelect
v-model:value="row.action"
:mode="mode"
class="mt-5 ml-5"
:options="actionOptions"
:required="true"
:label="t('harvester.subnet.acl.action.label')"
:placeholder="t('harvester.subnet.acl.action.placeholder')"
@update:value="update"
/>
</div>
<div class="col span-4">
<LabeledSelect
v-model:value="row.direction"
:mode="mode"
:options="directionOptions"
class="mt-5"
:required="true"
:label="t('harvester.subnet.acl.direction.label')"
:placeholder="t('harvester.subnet.acl.direction.placeholder')"
@update:value="update"
/>
</div>
<div class="col span-3">
<LabeledInput
v-model:value.number="row.priority"
type="number"
class="mb-20 mt-5"
:max="32767"
:min="0"
:mode="mode"
required
label-key="harvester.subnet.acl.priority.label"
@update:value="update"
/>
</div>
</div>
<div class="row">
<div class="col span-11">
<LabeledInput
v-model:value="row.match"
class="mb-5 ml-5"
:mode="mode"
required
:placeholder="t('harvester.subnet.acl.match.placeholder')"
label-key="harvester.subnet.acl.match.label"
@update:value="update"
/>
</div>
</div>
</InfoBox>
</div>
<button
type="button"
class="btn role-tertiary add"
:disabled="isView"
@click="add()"
>
<t k="harvester.subnet.acl.addRule" />
</button>
</div>
</template>
<style lang="scss" scoped>
.box {
position: relative;
}
.removeBtn {
position: absolute;
top: 10px;
right: 10px;
padding: 0px;
}
</style>

View File

@ -15,6 +15,7 @@ import { allHash } from '@shell/utils/promise';
import { HCI } from '../../types';
import ResourceTabs from '@shell/components/form/ResourceTabs/index';
import { Banner } from '@components/Banner';
import AccessControlList from './AccessControlList';
export default {
name: 'EditSubnet',
@ -32,6 +33,7 @@ export default {
ArrayList,
ResourceTabs,
Loading,
AccessControlList
},
mixins: [CreateEditView],
@ -51,7 +53,8 @@ export default {
gatewayIP: '',
excludeIps: [],
private: false,
enableDHCP
enableDHCP,
acls: []
});
},
@ -143,6 +146,7 @@ export default {
async saveSubnet(buttonCb) {
const errors = [];
const name = this.value?.metadata?.name;
const hasEmptyAcls = this.value?.spec?.acls?.some((acl) => !acl.match || !acl.action || acl.priority === undefined || acl.priority === null);
try {
if (!name) {
@ -153,6 +157,8 @@ export default {
errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true));
} else if (this.value.spec.excludeIps.includes('')) {
errors.push(this.t('harvester.validation.subnet.excludeIps'));
} else if (hasEmptyAcls) {
errors.push(this.t('harvester.validation.subnet.aclEmptyError'));
}
if (errors.length > 0) {
@ -371,6 +377,17 @@ export default {
</template>
</ArrayList>
</Tab>
<Tab
name="ACL"
:label="t('harvester.subnet.acl.label')"
:weight="-2"
class="bordered-table"
>
<AccessControlList
v-model:value="value.spec.acls"
:mode="mode"
/>
</Tab>
</ResourceTabs>
</CruResource>
</template>

View File

@ -404,7 +404,7 @@ harvester:
sha512: 'Invalid SHA512 checksum.'
subnet:
excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.'
aclEmptyError: The fields in subnet access control list rule can not be empty.
dashboard:
label: Dashboard
header: "Harvester Cluster: {cluster}"
@ -1062,7 +1062,24 @@ harvester:
placeholder: e.g. 172.16.0.0/16
excludeIPs:
tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list.
acl:
label: Access Control List
tooltip: The ACL to apply to this Subnet. Must be one of the ACLs in the same namespace.
action:
label: Action
placeholder: Please select an action
direction:
label: Direction
placeholder: Please select a direction
addRule: Add Rule
priority:
label: Priority
placeholder: Please select a priority
match:
label: Match
placeholder: e.g. ip4.dst == 10.10.0.2
banner: The supported field in ACL match can refer to <a href="https://kubeovn.github.io/docs/v1.14.x/en/guide/subnet/#subnet-acl" target="_blank">KubeOvn Subnet ACL document</a>
vpc:
noAddonEnabled:
prefix: The kubeovn-operator add-on is not enabled, click