Skip to content

Commit eb16c5f

Browse files
authored
Merge branch 'main' into feat/agentic-skills
2 parents 9543633 + 2d02d3f commit eb16c5f

201 files changed

Lines changed: 13871 additions & 382 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitattributes‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
# Images should be stored in Git LFS, and not have their line endings modified.
1010
*.png filter=lfs diff=lfs merge=lfs -text
11+
*.gif filter=lfs diff=lfs merge=lfs -text
1112

1213
# Generated files — collapsed in PR diffs, excluded from language stats.
1314
api/**/zz_generated.deepcopy.go linguist-generated

‎.github/workflows/test-gnmi.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
matrix:
2929
provider:
3030
- openconfig
31+
- cisco-nxos-gnmi
3132
steps:
3233
- uses: actions/checkout@v7
3334
- uses: actions/setup-go@v7

‎AGENTS.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ kubectl --context <context> logs -n network-operator-system deployment/network-o
9696
```
9797

9898
- **Use** `metav1.Condition` for status (not custom string fields)
99+
- **Place enum validation on enum types**: Put `// +kubebuilder:validation:Enum=...` on named enum type definitions (for example, `type T string`), not on fields using those types
99100
- **Use predefined types**: `metav1.Time` instead of `string` for dates
100101
- **Follow K8s API conventions**: Standard field names (`spec`, `status`, `metadata`)
101102

@@ -153,6 +154,13 @@ Using `omitempty` or `omitzero` JSON struct tags on gNMI payload structs is dang
153154
3. Diff compares device state (with defaults) against desired (without) → mismatch
154155
4. Operator performs a Set on every reconcile — breaking idempotency
155156

157+
**`omitempty` guidelines:**
158+
159+
1. **Safe:** The field's Go zero value matches the platform default or "absent" state. Omitting it from the payload is semantically equivalent to the device's default.
160+
2. **Safe:** The field is a pointer or slice representing "not configured" (nil) vs "configured" (non-nil). Mutually exclusive choices (e.g. `accept`/`drop`) fall into this category.
161+
3. **Dangerous:** The platform default is non-zero (e.g. `admin-state` defaults to `"enable"`, `port` defaults to `49`). Omitting the Go zero value would either misrepresent intent or cause a false diff on subsequent GET responses.
162+
4. **Unnecessary:** The field is unconditionally set to a non-zero value by the provider code. The tag never triggers, but removing it documents intent — the field is always present.
163+
156164
**Rule:** After setting configuration once, the provider must produce no gNMI Set calls on subsequent reconciles when no user-facing configuration has changed. Test this explicitly.
157165

158166
## Logging

‎Makefile‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ help: ## Display this help.
4848

4949
.PHONY: manifests
5050
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
51-
$(CONTROLLER_GEN) crd rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
51+
$(CONTROLLER_GEN) crd rbac:roleName=manager-role webhook paths="{./api/...,./internal/...}" output:crd:artifacts:config=config/crd/bases
5252

5353
YEAR ?= $(shell date +%Y)
5454

5555
.PHONY: generate
5656
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
57-
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt",year="$(YEAR)" paths="./..."
58-
$(CONTROLLER_GEN) applyconfiguration:headerFile="hack/boilerplate.go.txt" paths="./..."
57+
$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt",year="$(YEAR)" paths="{./api/...,./internal/...}"
58+
$(CONTROLLER_GEN) applyconfiguration:headerFile="hack/boilerplate.go.txt" paths="{./api/...,./internal/...}"
5959

6060
.PHONY: fmt
6161
fmt: goimports gofumpt ## Run goimports and gofumpt against code.
@@ -106,8 +106,8 @@ PROVIDER ?= openconfig
106106
GINKGO_PROCS ?= $(shell nproc 2>/dev/null || sysctl -n hw.logicalcpu)
107107

108108
.PHONY: test-gnmi
109-
test-gnmi: setup-envtest ## Run gNMI tests.
110-
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" PROVIDER=$(PROVIDER) go test ./test/gnmi/ -v -ginkgo.v
109+
test-gnmi: setup-envtest ginkgo ## Run gNMI tests.
110+
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" PROVIDER=$(PROVIDER) $(GINKGO) --procs=$(GINKGO_PROCS) -v ./test/gnmi/
111111

112112
.PHONY: test-lab
113113
test-lab: ## Run lab tests against a real network device.
@@ -181,6 +181,10 @@ run-docs:
181181
build: manifests generate fmt vet ## Build manager binary.
182182
CGO_ENABLED=0 go build $(LDFLAGS) -o bin/manager cmd/main.go
183183

184+
.PHONY: kubectl-net
185+
kubectl-net: ## Build kubectl-net plugin binary.
186+
cd kubectl-net && CGO_ENABLED=0 go build -ldflags="-s -w -X 'main.version=$(VERSION)'" -o ../bin/kubectl-net .
187+
184188
.PHONY: run
185189
run: manifests generate fmt vet ## Run a controller from your host.
186190
go run ./cmd/main.go
@@ -270,6 +274,7 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
270274
GOLANGCI_LINT_CUSTOM = $(LOCALBIN)/golangci-lint-custom
271275
GOIMPORTS ?= $(LOCALBIN)/goimports
272276
GOFUMPT ?= $(LOCALBIN)/gofumpt
277+
GINKGO ?= $(LOCALBIN)/ginkgo
273278
ADDLICENSE ?= $(LOCALBIN)/addlicense
274279
GO_LICENSES ?= $(LOCALBIN)/go-licenses
275280
TYPOS ?= $(LOCALBIN)/typos
@@ -286,8 +291,9 @@ ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -
286291
KUBEBUILDER_VERSION ?= v4.15.0
287292
CRD_REF_DOCS_VERSION ?= v0.3.0
288293
GOLANGCI_LINT_VERSION ?= v2.12.2
289-
GOIMPORTS_VERSION ?= v0.48.0
294+
GOIMPORTS_VERSION ?= $(shell go list -m -f "{{ .Version }}" golang.org/x/tools)
290295
GOFUMPT_VERSION ?= v0.10.0
296+
GINKGO_VERSION ?= $(shell go list -m -f "{{ .Version }}" github.com/onsi/ginkgo/v2)
291297
ADDLICENSE_VERSION ?= v1.2.0
292298
GO_LICENSES_VERSION ?= v2.0.1
293299
TYPOS_VERSION ?= v1.48.0
@@ -347,6 +353,11 @@ gofumpt: $(GOFUMPT) ## Download gofumpt locally if necessary.
347353
$(GOFUMPT): $(LOCALBIN)
348354
$(call go-install-tool,$(GOFUMPT),mvdan.cc/gofumpt,$(GOFUMPT_VERSION))
349355

356+
.PHONY: ginkgo
357+
ginkgo: $(GINKGO) ## Download ginkgo locally if necessary.
358+
$(GINKGO): $(LOCALBIN)
359+
$(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo,$(GINKGO_VERSION))
360+
350361
.PHONY: addlicense
351362
addlicense: $(ADDLICENSE) ## Download addlicense locally if necessary.
352363
$(ADDLICENSE): $(LOCALBIN)
@@ -429,3 +440,7 @@ kind-delete: kind ## Destroys the kind cluster.
429440
.PHONY: tilt-up
430441
tilt-up: $(KUSTOMIZE) kind-create ## Start tilt and create the kind cluster if needed
431442
tilt up --context kind-$(KIND_CLUSTER_NAME)
443+
444+
.PHONY: tilt-debug-up
445+
tilt-debug-up: export DEBUG = true
446+
tilt-debug-up: tilt-up

‎PROJECT‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ resources:
7373
kind: AccessControlList
7474
path: github.com/ironcore-dev/network-operator/api/core/v1alpha1
7575
version: v1alpha1
76+
webhooks:
77+
validation: true
78+
webhookVersion: v1
7679
- api:
7780
crdVersion: v1
7881
namespaced: true
@@ -403,4 +406,12 @@ resources:
403406
kind: Fabric
404407
path: github.com/ironcore-dev/network-operator/api/evpn/v1alpha1
405408
version: v1alpha1
409+
- api:
410+
crdVersion: v1
411+
namespaced: true
412+
controller: true
413+
domain: networking.metal.ironcore.dev
414+
kind: Probe
415+
path: github.com/ironcore-dev/network-operator/api/core/v1alpha1
416+
version: v1alpha1
406417
version: "3"

‎README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ make uninstall
8383
make undeploy
8484
```
8585

86+
## kubectl Plugin
87+
88+
The `kubectl-net` plugin extends kubectl with shorthand flags and resource lifecycle operations tailored to network-operator.
89+
90+
```bash
91+
kubectl net get interfaces --device leaf1
92+
kubectl net pause devices leaf1 --recursive
93+
```
94+
95+
See [kubectl-net/README.md](kubectl-net/README.md) for installation and full usage documentation.
96+
8697
## Project Distribution
8798

8899
Following are the steps to build the installer and distribute this project to users.

‎Tiltfile‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,16 @@ provider = os.getenv('PROVIDER', 'openconfig')
4646
manager = kustomize('config/develop')
4747
manager = str(manager).replace('--provider=openconfig', '--provider={}'.format(provider))
4848

49+
debug = os.getenv('DEBUG', '') == 'true'
50+
if debug:
51+
# Scale the in-cluster manager to 0 so it can be run locally in a debugger.
52+
manager = str(manager).replace('replicas: 1', 'replicas: 0')
53+
4954
k8s_yaml(blob(manager))
5055
k8s_resource('network-operator-controller-manager', resource_deps=['controller-gen'], labels=['operator'])
5156

57+
k8s_resource('minio', port_forwards=['9001:9001'])
58+
5259
# Sample resources with manual trigger mode
5360
def device_yaml():
5461
decoded = read_yaml_stream('./config/samples/v1alpha1_device.yaml')
@@ -57,7 +64,10 @@ def device_yaml():
5764
decoded[0]['spec']['endpoint']['address'] = ip+':9339'
5865
return encode_yaml_stream(decoded)
5966

60-
k8s_yaml(device_yaml())
67+
if debug:
68+
k8s_yaml('./config/samples/v1alpha1_device.yaml')
69+
else:
70+
k8s_yaml(device_yaml())
6171
k8s_resource(new_name='leaf1', objects=['leaf1:device', 'secret-basic-auth:secret'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
6272

6373
k8s_yaml('./config/samples/v1alpha1_interface.yaml')
@@ -116,6 +126,7 @@ k8s_resource(new_name='bgp-rdst', objects=['bgp-rdst:bgp'], resource_deps=['bgp-
116126
k8s_yaml('./config/samples/cisco/nx/v1alpha1_bgpconfig.yaml')
117127
k8s_resource(new_name='bgpconfig-adv-pip', objects=['bgpconfig-adv-pip:bgpconfig'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
118128
k8s_resource(new_name='bgpconfig-export-gw', objects=['bgpconfig-export-gw:bgpconfig'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
129+
k8s_resource(new_name='bgpconfig-adv-l2vpn-evpn', objects=['bgpconfig-adv-l2vpn-evpn:bgpconfig'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
119130

120131
k8s_yaml('./config/samples/v1alpha1_bgppeer.yaml')
121132
k8s_resource(new_name='peer-spine1', objects=['leaf1-spine1:bgppeer'], resource_deps=['bgp', 'lo0'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
@@ -171,6 +182,7 @@ k8s_resource(new_name='aaa', objects=['aaa-tacacs:aaa', 'tacacs-server-keys:secr
171182
k8s_yaml('./config/samples/v1alpha1_configbackup.yaml')
172183
k8s_resource(new_name='local-backup', objects=['local-backup:configbackup'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
173184
k8s_resource(new_name='startup-backup', objects=['startup-backup:configbackup'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
185+
k8s_resource(new_name='remote-backup', objects=['remote-backup:configbackup', 'minio-credentials:secret', 'backup-encryption-key:secret'], resource_deps=['minio'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
174186

175187
k8s_yaml('./config/samples/v1alpha1_indexpool.yaml')
176188
k8s_resource(new_name='indexpool', objects=['indexpool-sample:indexpool'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
@@ -189,6 +201,12 @@ k8s_resource(new_name='claim-prefix', objects=['claim-prefix:claim'], resource_d
189201
k8s_yaml('./config/samples/v1alpha1_fabric.yaml')
190202
k8s_resource(new_name='fabric', objects=['fabric:fabric', 'loopback-pool:ipaddresspool', 'underlay-p2p-pool:ipprefixpool'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
191203

204+
k8s_yaml('./config/samples/v1alpha1_probe.yaml')
205+
k8s_resource(new_name='ping-peer', objects=['ping-peer:probe'], resource_deps=['lo0'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
206+
k8s_resource(new_name='mac-entry', objects=['mac-entry:probe'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
207+
k8s_resource(new_name='route-prefix', objects=['route-prefix:probe'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
208+
k8s_resource(new_name='vtep-peers', objects=['vtep-peers:probe'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
209+
192210
print('🚀 network-operator development environment')
193211
print('👉 Edit the code inside the api/, cmd/, or internal/ directories')
194212
print('👉 Tilt will automatically rebuild and redeploy when changes are detected')

‎api/cisco/nx/v1alpha1/bgpconfig_types.go‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ type BGPConfigAddressFamilies struct {
3434
Ipv6Unicast *BGPConfigUnicastAddressFamily `json:"ipv6Unicast,omitempty"`
3535
}
3636

37+
// Ipv4UnicastExportGatewayIP reports whether export gateway IP is enabled for the IPv4 unicast address family.
38+
func (af *BGPConfigAddressFamilies) Ipv4UnicastExportGatewayIP() bool {
39+
return af != nil && af.Ipv4Unicast != nil && af.Ipv4Unicast.ExportGatewayIP != nil && *af.Ipv4Unicast.ExportGatewayIP
40+
}
41+
42+
// Ipv6UnicastExportGatewayIP reports whether export gateway IP is enabled for the IPv6 unicast address family.
43+
func (af *BGPConfigAddressFamilies) Ipv6UnicastExportGatewayIP() bool {
44+
return af != nil && af.Ipv6Unicast != nil && af.Ipv6Unicast.ExportGatewayIP != nil && *af.Ipv6Unicast.ExportGatewayIP
45+
}
46+
47+
// Ipv4UnicastAdvertiseL2vpnEvpn reports whether advertise l2vpn evpn is enabled for the IPv4 unicast address family.
48+
func (af *BGPConfigAddressFamilies) Ipv4UnicastAdvertiseL2vpnEvpn() bool {
49+
return af != nil && af.Ipv4Unicast != nil && af.Ipv4Unicast.AdvertiseL2vpnEvpn != nil && *af.Ipv4Unicast.AdvertiseL2vpnEvpn
50+
}
51+
52+
// Ipv6UnicastAdvertiseL2vpnEvpn reports whether advertise l2vpn evpn is enabled for the IPv6 unicast address family.
53+
func (af *BGPConfigAddressFamilies) Ipv6UnicastAdvertiseL2vpnEvpn() bool {
54+
return af != nil && af.Ipv6Unicast != nil && af.Ipv6Unicast.AdvertiseL2vpnEvpn != nil && *af.Ipv6Unicast.AdvertiseL2vpnEvpn
55+
}
56+
3757
// BGPL2vpnEvpn defines the configuration for L2VPN EVPN address family.
3858
type BGPL2vpnEvpn struct {
3959
// AdvertisePIP controls whether the BGP EVPN address-family should advertise the primary IP address (PIP) as the next-hop
@@ -50,6 +70,12 @@ type BGPConfigUnicastAddressFamily struct {
5070
// required for symmetric IRB in VXLAN BGP EVPN topologies.
5171
// +optional
5272
ExportGatewayIP *bool `json:"exportGatewayIP,omitempty"`
73+
74+
// AdvertiseL2vpnEvpn enables advertising EVPN routes within the VRF
75+
// unicast address family. Required on border gateway nodes for cross-site
76+
// EVPN Type-5 IP prefix route advertisement.
77+
// +optional
78+
AdvertiseL2vpnEvpn *bool `json:"advertiseL2vpnEvpn,omitempty"`
5379
}
5480

5581
// +kubebuilder:object:root=true

‎api/cisco/nx/v1alpha1/nveconfig_types.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type NetworkVirtualizationEdgeConfigSpec struct {
2727
HoldDownTime int16 `json:"holdDownTime,omitempty"`
2828

2929
// InfraVLANs specifies VLANs used by all SVI interfaces for uplink and vPC peer-links in VXLAN as infra-VLANs.
30+
// Valid VLAN IDs are 1-4092 because NX-OS always reserves VLANs 4093-4095 for internal use.
31+
// The provider also checks these VLANs against the device's configurable internal reserved VLAN range.
3032
// The total number of VLANs configured must not exceed 512.
3133
// Elements in the list must not overlap with each other.
3234
// +optional
@@ -42,15 +44,15 @@ type NetworkVirtualizationEdgeConfigSpec struct {
4244
type VLANListItem struct {
4345
// +optional
4446
// +kubebuilder:validation:Minimum=1
45-
// +kubebuilder:validation:Maximum=3967
47+
// +kubebuilder:validation:Maximum=4092
4648
ID int16 `json:"id,omitempty"`
4749
// +optional
4850
// +kubebuilder:validation:Minimum=1
49-
// +kubebuilder:validation:Maximum=3967
51+
// +kubebuilder:validation:Maximum=4092
5052
RangeMin int16 `json:"rangeMin,omitempty"`
5153
// +optional
5254
// +kubebuilder:validation:Minimum=1
53-
// +kubebuilder:validation:Maximum=3967
55+
// +kubebuilder:validation:Maximum=4092
5456
RangeMax int16 `json:"rangeMax,omitempty"`
5557
}
5658

‎api/cisco/nx/v1alpha1/zz_generated.deepcopy.go‎

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)