From 93a692ff0cc93a93fe4433c9813533b4c7cc539b Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Mon, 22 Jun 2026 14:32:26 +0800 Subject: [PATCH] feat: add NICs tab when creating overlay network (#931) * feat: allow user to select kube-system ns Signed-off-by: Andy Lee * feat: add NICs tab when creating overlay network Signed-off-by: Andy Lee * refactor: only show NIC tab if ns is kube-system Signed-off-by: Andy Lee --------- Signed-off-by: Andy Lee --- ...sterhci.io.networkattachmentdefinition.vue | 115 +++++++++++++++++- pkg/harvester/l10n/en-us.yaml | 4 +- ...sterhci.io.networkattachmentdefinition.vue | 1 + 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue b/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue index fb739588..59f323ad 100644 --- a/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue +++ b/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue @@ -9,6 +9,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect'; import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import CreateEditView from '@shell/mixins/create-edit-view'; import { allHash } from '@shell/utils/promise'; +import { NAMESPACE, NODE } from '@shell/config/types'; import { HCI } from '../types'; import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types'; import { removeObject } from '@shell/utils/array'; @@ -20,6 +21,7 @@ const { ACCESS, TRUNK } = L2VLAN_MODE; const AUTO = 'auto'; const MANUAL = 'manual'; +const KUBE_SYSTEM = 'kube-system'; export default { emits: ['update:value'], @@ -70,7 +72,12 @@ export default { async fetch() { const inStore = this.$store.getters['currentProduct'].inStore; - await allHash({ clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }) }); + await allHash({ + clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }), + namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }), + nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }), + linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }), + }); }, created() { @@ -199,6 +206,80 @@ export default { } return this.type === UNTAGGED; + }, + + showNicsTab() { + return this.isOverlayNetwork && this.value.metadata.namespace === KUBE_SYSTEM; + }, + + namespaceOptions() { + const ns = this.$store.getters['harvester/all'](NAMESPACE) || []; + + // Allow users to select the "kube-system" namespace as the external subnet from Kube-OVN. + // This expects the provider network to be in the "kube-system" namespace for VPC NAT gateway functionality. + return ns + .filter((ns) => !ns.isSystem || ns.id === KUBE_SYSTEM) + .map((ns) => ({ name: ns.id })) + .sort((a, b) => a.name.localeCompare(b.name)); + }, + + nodes() { + const inStore = this.$store.getters['currentProduct'].inStore; + const nodes = this.$store.getters[`${ inStore }/all`](NODE); + + return nodes.filter((n) => n.isEtcd !== 'true'); + }, + + nics() { + const inStore = this.$store.getters['currentProduct'].inStore; + const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {}; + const linkStatus = linkMonitor?.status?.linkStatus || {}; + const nodes = this.nodes.map((n) => n.id); + + const out = []; + + // Collect all nics from all nodes (for overlay, we select from all nodes) + Object.keys(linkStatus).map((nodeName) => { + if (nodes.includes(nodeName)) { + const nics = linkStatus[nodeName] || []; + + nics.map((nic) => { + out.push({ + ...nic, + nodeName, + }); + }); + } + }); + + return out; + }, + + nicOptions() { + const out = []; + const seen = new Set(); + + (this.nics || []).forEach((nic) => { + if (!seen.has(nic.name)) { + seen.add(nic.name); + out.push({ + label: nic.name, + value: nic.name, + }); + } + }); + + return out.sort((a, b) => a.label.localeCompare(b.label)); + }, + + master: { + get() { + return this.config?.master || ''; + }, + + set(value) { + this.config.master = value; + } } }, @@ -214,6 +295,7 @@ export default { this.config.ipam = {}; this.config.bridge = ''; delete this.config.provider; + delete this.config.master; delete this.config.server_socket; } }, @@ -230,6 +312,13 @@ export default { this.config.vlan = ''; } }, + 'value.metadata.namespace'(newNamespace) { + // NIC selection is only valid for overlay in kube-system namespace. + if (newNamespace !== KUBE_SYSTEM) { + delete this.config.master; + this.value.spec.config = JSON.stringify({ ...this.config }); + } + }, }, methods: { @@ -324,6 +413,10 @@ export default { delete this.config.promiscMode; delete this.config.vlan; delete this.config.ipam; + + if (this.value.metadata.namespace !== KUBE_SYSTEM) { + delete this.config.master; + } } if (this.isUntaggedNetwork) { @@ -350,6 +443,7 @@ export default { ref="nd" :value="value" :mode="mode" + :namespace-options="namespaceOptions" @update:value="$emit('update:value', $event)" /> + +
+
+ +
+
+
diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 99be2aed..b1b7d7a2 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -1248,6 +1248,7 @@ harvester: tabs: basics: Basics layer3Network: Route + nic: Network Interface Card clusterNetwork: label: Cluster Network create: Create a new cluster network @@ -1634,8 +1635,9 @@ harvester: schedulingRules: Select node(s) matching rules uplink: nics: - label: NICs + label: NIC addLabel: Add NIC + overlayWarning: The NIC selected here must match the NIC provided in the provider network. placeholder: Select a NIC that is available on all the selected nodes validate: available: NIC "{nic}" is not available on the selected nodes diff --git a/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue b/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue index ae6bc9f3..04841360 100644 --- a/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue +++ b/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue @@ -166,6 +166,7 @@ export default { :schema="schema" :groupable="true" :rows="filterRows" + :ignore-filter="true" key-field="_key" >