From 696a1125bada9c69dbbd77e2fa988d0cd18afe6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Wed, 25 Mar 2026 21:41:03 +0100 Subject: [PATCH 1/8] Track *.gif files with Git LFS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felix Kästner --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index a5889bae3..6cd4075e7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,7 @@ # Images should be stored in Git LFS, and not have their line endings modified. *.png filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text # Generated files — collapsed in PR diffs, excluded from language stats. api/**/zz_generated.deepcopy.go linguist-generated From dd148ffaacd960b99809c5f6f0550ee304eb6062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Mon, 24 Aug 2026 16:00:48 +0200 Subject: [PATCH 2/8] Remove redundant Claim short name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Felix Kästner --- api/pool/v1alpha1/claim_types.go | 1 - .../crd/claims.pool.networking.metal.ironcore.dev.yaml | 2 -- config/crd/bases/pool.networking.metal.ironcore.dev_claims.yaml | 2 -- 3 files changed, 5 deletions(-) diff --git a/api/pool/v1alpha1/claim_types.go b/api/pool/v1alpha1/claim_types.go index 5b9220410..3d665a32f 100644 --- a/api/pool/v1alpha1/claim_types.go +++ b/api/pool/v1alpha1/claim_types.go @@ -88,7 +88,6 @@ type ClaimStatus struct { // +kubebuilder:subresource:status // +kubebuilder:resource:path=claims // +kubebuilder:resource:singular=claim -// +kubebuilder:resource:shortName=claim // +kubebuilder:printcolumn:name="Value",type=string,JSONPath=`.status.value` // +kubebuilder:printcolumn:name="Allocated",type=string,JSONPath=`.status.conditions[?(@.type=="Allocated")].status` // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" diff --git a/charts/network-operator/templates/crd/claims.pool.networking.metal.ironcore.dev.yaml b/charts/network-operator/templates/crd/claims.pool.networking.metal.ironcore.dev.yaml index d817cfa3c..6a5b32f8f 100644 --- a/charts/network-operator/templates/crd/claims.pool.networking.metal.ironcore.dev.yaml +++ b/charts/network-operator/templates/crd/claims.pool.networking.metal.ironcore.dev.yaml @@ -14,8 +14,6 @@ spec: kind: Claim listKind: ClaimList plural: claims - shortNames: - - claim singular: claim scope: Namespaced versions: diff --git a/config/crd/bases/pool.networking.metal.ironcore.dev_claims.yaml b/config/crd/bases/pool.networking.metal.ironcore.dev_claims.yaml index ade259994..7b8f780a5 100644 --- a/config/crd/bases/pool.networking.metal.ironcore.dev_claims.yaml +++ b/config/crd/bases/pool.networking.metal.ironcore.dev_claims.yaml @@ -11,8 +11,6 @@ spec: kind: Claim listKind: ClaimList plural: claims - shortNames: - - claim singular: claim scope: Namespaced versions: From 8f631316d1e60a45dba9edde252a722fb48d27c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Wed, 25 Mar 2026 23:40:24 +0100 Subject: [PATCH 3/8] Add kubectl-net plugin for managing network-operator resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a kubectl plugin with get, pause, unpause, and shell completion subcommands. The get command supports server-side table printing and all standard kubectl output formats. Label shorthand flags (--device/-d, --aggregate, --vrf, --routed-vlan, --evi) provide convenient filtering. Resource definitions are code-generated from the API types via kubebuilder markers. Signed-off-by: Felix Kästner --- README.md | 11 + kubectl-net/.gitignore | 24 ++ kubectl-net/README.md | 111 +++++++++ kubectl-net/cmd/kubectl-net.go | 24 ++ kubectl-net/examples/demo.gif | 3 + kubectl-net/examples/demo.tape | 35 +++ kubectl-net/go.mod | 80 +++++++ kubectl-net/go.sum | 208 +++++++++++++++++ kubectl-net/pkg/cmd/completion.go | 36 +++ kubectl-net/pkg/cmd/doc.go | 7 + kubectl-net/pkg/cmd/gen.go | 10 + kubectl-net/pkg/cmd/get.go | 198 ++++++++++++++++ kubectl-net/pkg/cmd/labels.go | 65 ++++++ kubectl-net/pkg/cmd/pause.go | 197 ++++++++++++++++ kubectl-net/pkg/cmd/resources.go | 72 ++++++ kubectl-net/pkg/cmd/root.go | 50 +++++ kubectl-net/tools/generate-resources/main.go | 224 +++++++++++++++++++ 17 files changed, 1355 insertions(+) create mode 100644 kubectl-net/.gitignore create mode 100644 kubectl-net/README.md create mode 100644 kubectl-net/cmd/kubectl-net.go create mode 100644 kubectl-net/examples/demo.gif create mode 100644 kubectl-net/examples/demo.tape create mode 100644 kubectl-net/go.mod create mode 100644 kubectl-net/go.sum create mode 100644 kubectl-net/pkg/cmd/completion.go create mode 100644 kubectl-net/pkg/cmd/doc.go create mode 100644 kubectl-net/pkg/cmd/gen.go create mode 100644 kubectl-net/pkg/cmd/get.go create mode 100644 kubectl-net/pkg/cmd/labels.go create mode 100644 kubectl-net/pkg/cmd/pause.go create mode 100644 kubectl-net/pkg/cmd/resources.go create mode 100644 kubectl-net/pkg/cmd/root.go create mode 100644 kubectl-net/tools/generate-resources/main.go diff --git a/README.md b/README.md index 7f360df88..ff09d614b 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,17 @@ make uninstall make undeploy ``` +## kubectl Plugin + +The `kubectl-net` plugin extends kubectl with shorthand flags and resource lifecycle operations tailored to network-operator. + +```bash +kubectl net get interfaces --device leaf1 +kubectl net pause devices leaf1 --recursive +``` + +See [kubectl-net/README.md](kubectl-net/README.md) for installation and full usage documentation. + ## Project Distribution Following are the steps to build the installer and distribute this project to users. diff --git a/kubectl-net/.gitignore b/kubectl-net/.gitignore new file mode 100644 index 000000000..fb2d8d84b --- /dev/null +++ b/kubectl-net/.gitignore @@ -0,0 +1,24 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work +go.work.sum + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ diff --git a/kubectl-net/README.md b/kubectl-net/README.md new file mode 100644 index 000000000..f6f2ff3c9 --- /dev/null +++ b/kubectl-net/README.md @@ -0,0 +1,111 @@ + + +# kubectl-net + +A [kubectl plugin](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/) for managing [network-operator](https://github.com/ironcore-dev/network-operator) custom resources. + +## Installation + +Install via `go install`: + +```bash +go install github.com/ironcore-dev/network-operator/kubectl-net/cmd@latest +``` + +> Ensure your `$GOBIN` (or default `$GOPATH/bin`) is on your `$PATH`. + +Verify kubectl discovers the plugin: + +```bash +kubectl plugin list | grep kubectl-net +``` + +You can now run the plugin as `kubectl net`. + +
+Build from source + +```bash +go build -o kubectl-net ./cmd/kubectl-net.go +sudo install -m 0755 kubectl-net /usr/local/bin/kubectl-net +kubectl plugin list | grep kubectl-net +``` + +
+ +## Usage + +![demo](examples/demo.gif) + +### Get resources + +The `get` subcommand works like `kubectl get` and supports the same output flags (`-o yaml`, `-o json`, `-o wide`, `-o name`, `-o jsonpath=...`, etc.). + +```bash +# List all devices (server-side table output, same as kubectl get) +kubectl net get devices + +# Get a single device +kubectl net get devices leaf1 + +# Filter interfaces by device +kubectl net get interfaces --device leaf1 + +# Filter interfaces by VRF +kubectl net get interfaces --vrf default + +# Filter interfaces by aggregate +kubectl net get interfaces --aggregate ae0 + +# Filter VLANs by EVPN instance +kubectl net get vlans --evi evi-100 + +# Filter VLANs by routed VLAN interface +kubectl net get vlans --routed-vlan irb100 + +# Output as YAML +kubectl net get devices leaf1 -o yaml +``` + +### Pause and unpause resources + +The `pause` and `unpause` subcommands set or remove the `networking.metal.ironcore.dev/paused` annotation. For devices, `--recursive` also sets `spec.paused` to propagate to child resources. + +```bash +# Pause a single interface +kubectl net pause interfaces lo0 + +# Unpause interface +kubectl net unpause interfaces lo0 + +# Pause a device and all child resources +kubectl net pause devices leaf1 --recursive + +# Unpause a device and all child resources +kubectl net unpause devices leaf1 --recursive + +# Pause all interfaces on a device +kubectl net pause interfaces --device leaf1 +``` + +### Shell completion + +```bash +# Generate and source completion for your shell +source <(kubectl net completion bash) +source <(kubectl net completion zsh) +kubectl net completion fish | source +``` + +## Label shorthand flags + +| Flag | Label | Available for | +| ---------------- | ------------------------------------------- | ------------- | +| `--device`, `-d` | `networking.metal.ironcore.dev/device-name` | All resources | +| `--aggregate` | `networking.metal.ironcore.dev/aggregate` | Interfaces | +| `--vrf` | `networking.metal.ironcore.dev/vrf` | Interfaces | +| `--routed-vlan` | `networking.metal.ironcore.dev/routed-vlan` | VLANs | +| `--evi` | `networking.metal.ironcore.dev/l2vni` | VLANs | diff --git a/kubectl-net/cmd/kubectl-net.go b/kubectl-net/cmd/kubectl-net.go new file mode 100644 index 000000000..053b67601 --- /dev/null +++ b/kubectl-net/cmd/kubectl-net.go @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "os" + + "github.com/spf13/pflag" + "k8s.io/cli-runtime/pkg/genericiooptions" + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "github.com/ironcore-dev/kubectl-net/pkg/cmd" +) + +func main() { + flags := pflag.NewFlagSet("kubectl-net", pflag.ExitOnError) + pflag.CommandLine = flags + + root := cmd.NewCmdNet(genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) + if err := root.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/kubectl-net/examples/demo.gif b/kubectl-net/examples/demo.gif new file mode 100644 index 000000000..17c9d7a1f --- /dev/null +++ b/kubectl-net/examples/demo.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7fc52f631059012d83a23fc9971c11d8ecc9724821c50ed248b40e54b282b8d0 +size 337984 diff --git a/kubectl-net/examples/demo.tape b/kubectl-net/examples/demo.tape new file mode 100644 index 000000000..e9988b1ba --- /dev/null +++ b/kubectl-net/examples/demo.tape @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +# SPDX-License-Identifier: Apache-2.0 + +Require kubectl +Require jq + +Output examples/demo.gif + +Set Shell "bash" +Set Padding 24 + +Type "# List all devices (drop-in replacement for kubectl get)" Enter Sleep 1s +Type "kubectl net get devices" Enter Sleep 3s + +Type "# Filter interfaces by device name" Enter Sleep 1s +Type "kubectl net get interfaces --device leaf1" Enter Sleep 3s + +Type "clear" Sleep 1s Enter Sleep 500ms + +Type "# Pause a single interface" Enter Sleep 1s +Type "kubectl net pause interfaces lo0" Enter Sleep 3s + +Type "# Unpause the interface" Enter Sleep 1s +Type "kubectl net unpause interfaces lo0" Enter Sleep 3s + +Type "clear" Sleep 1s Enter Sleep 500ms + +Type "# Recursively pause a device and children" Enter Sleep 1s +Type "kubectl net pause devices leaf1 --recursive" Enter Sleep 3s + +Type "# Verify the device is paused" Enter Sleep 1s +Type "kubectl net get devices leaf1 -o yaml | yq .spec.paused" Enter Sleep 3s + +Type "# Unpause the device and children" Enter Sleep 1s +Type "kubectl net unpause devices leaf1 --recursive" Enter Sleep 3s diff --git a/kubectl-net/go.mod b/kubectl-net/go.mod new file mode 100644 index 000000000..d41e4991c --- /dev/null +++ b/kubectl-net/go.mod @@ -0,0 +1,80 @@ +module github.com/ironcore-dev/kubectl-net + +go 1.26.0 + +require ( + github.com/ironcore-dev/network-operator v0.0.0 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + k8s.io/apimachinery v0.36.2 + k8s.io/cli-runtime v0.36.2 + k8s.io/client-go v0.36.2 +) + +require ( + github.com/go-logr/logr v1.4.4 // indirect + google.golang.org/protobuf v1.36.12 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/api v0.36.2 // indirect + k8s.io/klog/v2 v2.140.0 // indirect +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-openapi/jsonpointer v1.0.0 // indirect + github.com/go-openapi/jsonreference v1.0.0 // indirect + github.com/go-openapi/swag v0.27.3 // indirect + github.com/go-openapi/swag/cmdutils v0.27.3 // indirect + github.com/go-openapi/swag/conv v0.27.3 // indirect + github.com/go-openapi/swag/fileutils v0.27.3 // indirect + github.com/go-openapi/swag/jsonutils v0.27.3 // indirect + github.com/go-openapi/swag/loading v0.27.3 // indirect + github.com/go-openapi/swag/mangling v0.27.3 // indirect + github.com/go-openapi/swag/netutils v0.27.3 // indirect + github.com/go-openapi/swag/pools v0.27.3 // indirect + github.com/go-openapi/swag/stringutils v0.27.3 // indirect + github.com/go-openapi/swag/typeutils v0.27.3 // indirect + github.com/go-openapi/swag/yamlutils v0.27.3 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.41.0 // indirect + golang.org/x/time v0.15.0 // indirect + k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect + k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect + sigs.k8s.io/controller-runtime v0.24.1 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kustomize/api v0.21.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.21.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace github.com/ironcore-dev/network-operator => .. diff --git a/kubectl-net/go.sum b/kubectl-net/go.sum new file mode 100644 index 000000000..d7162526b --- /dev/null +++ b/kubectl-net/go.sum @@ -0,0 +1,208 @@ +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= +github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= +github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= +github.com/go-openapi/swag v0.27.3 h1:i6oVKkGZeFgETHMiBHGtj9gIQ1aLtWDdJnT/SRZeets= +github.com/go-openapi/swag v0.27.3/go.mod h1:qEXs3GcyyQTDCFQ4ykqnLPDh8qT+zBcjbVWdxCAW0Us= +github.com/go-openapi/swag/cmdutils v0.27.3 h1:sjuL0TvW81i9R9GRMO/fy+c3mOW+7zxRYwy/7fobZt4= +github.com/go-openapi/swag/cmdutils v0.27.3/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU= +github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ= +github.com/go-openapi/swag/fileutils v0.27.3 h1:3UVoZ2RLaIs1lt+2jcKzL8RM3Yk0rmsDE9FLA/HGxFE= +github.com/go-openapi/swag/fileutils v0.27.3/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= +github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc= +github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= +github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0= +github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE= +github.com/go-openapi/swag/mangling v0.27.3 h1:gRzzD1PAUoLTtGMgI3KpBmCSOlTuLTFWnviLxLcTnyg= +github.com/go-openapi/swag/mangling v0.27.3/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= +github.com/go-openapi/swag/netutils v0.27.3 h1:IoBvfCoprsE6E87kAIm9basnISqDDqB79mJ8MN+f5PU= +github.com/go-openapi/swag/netutils v0.27.3/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= +github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk= +github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= +github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI= +github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= +github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4= +github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= +github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs= +github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= +github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= +github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= +github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw= +github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= +github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= +github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= +github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= +k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= +k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= +k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= +k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= +k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= +k8s.io/cli-runtime v0.36.2 h1:CconTvEeV4DJs4ZX3HQKCFbFRGsm6OtuBM9yjmMP2VM= +k8s.io/cli-runtime v0.36.2/go.mod h1:LddcjiMf4YlnHO7c1Y7rEtDqL84FyiYVLco7V679GUU= +k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= +k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM= +sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= +sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= +sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q= +sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/kubectl-net/pkg/cmd/completion.go b/kubectl-net/pkg/cmd/completion.go new file mode 100644 index 000000000..e9c738741 --- /dev/null +++ b/kubectl-net/pkg/cmd/completion.go @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// NewCmdCompletion returns a command that generates shell completion scripts +// for bash, zsh, fish, and powershell. +func NewCmdCompletion(root *cobra.Command) *cobra.Command { + return &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate completion scripts", + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + DisableFlagsInUseLine: true, + RunE: func(cmd *cobra.Command, args []string) error { + switch args[0] { + case "bash": + return root.GenBashCompletion(cmd.OutOrStdout()) + case "zsh": + return root.GenZshCompletion(cmd.OutOrStdout()) + case "fish": + return root.GenFishCompletion(cmd.OutOrStdout(), true) + case "powershell": + return root.GenPowerShellCompletion(cmd.OutOrStdout()) + default: + return fmt.Errorf("unsupported shell type %q", args[0]) + } + }, + } +} diff --git a/kubectl-net/pkg/cmd/doc.go b/kubectl-net/pkg/cmd/doc.go new file mode 100644 index 000000000..1368884f8 --- /dev/null +++ b/kubectl-net/pkg/cmd/doc.go @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +// Package cmd implements the kubectl-net plugin commands for managing +// network-operator resources. It provides subcommands built on top of +// cobra and the Kubernetes CLI runtime. +package cmd diff --git a/kubectl-net/pkg/cmd/gen.go b/kubectl-net/pkg/cmd/gen.go new file mode 100644 index 000000000..ae20d00cf --- /dev/null +++ b/kubectl-net/pkg/cmd/gen.go @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +// generate-resources scans the network-operator API types for kubebuilder +// resource markers (+kubebuilder:resource:path=, singular=, shortName=) and +// produces resources.go with the ResourceDef slice used by all subcommands. +// Run "go generate ./pkg/cmd" to regenerate after API type changes. +//go:generate go run ../../tools/generate-resources --api-root ../../../api --out ./resources.go diff --git a/kubectl-net/pkg/cmd/get.go b/kubectl-net/pkg/cmd/get.go new file mode 100644 index 000000000..3021f8ebd --- /dev/null +++ b/kubectl-net/pkg/cmd/get.go @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/rest" +) + +// GetOptions holds the configuration for the "get" subcommand. +type GetOptions struct { + Root *RootOptions + Resource ResourceDef + Labels LabelFlags + PrintFlags *genericclioptions.PrintFlags + + Namespace string +} + +// NewCmdGet returns the "get" command with a subcommand for each known resource type. +func NewCmdGet(root *RootOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: "Get network-operator resources", + SilenceUsage: true, + } + + for _, res := range resourceDefs { + cmd.AddCommand(newGetResourceCmd(root, res)) + } + + return cmd +} + +// newGetResourceCmd returns a command that fetches and prints a specific resource type. +func newGetResourceCmd(root *RootOptions, res ResourceDef) *cobra.Command { + o := &GetOptions{ + Root: root, + Resource: res, + PrintFlags: genericclioptions.NewPrintFlags(""), + } + + cmd := &cobra.Command{ + Use: fmt.Sprintf("%s [NAME...]", res.Name), + Short: fmt.Sprintf("Get %s", res.Name), + Args: cobra.ArbitraryArgs, + RunE: func(c *cobra.Command, args []string) error { + if err := o.Complete(); err != nil { + return err + } + return o.Run(args) + }, + } + cmd.Aliases = append(cmd.Aliases, res.Aliases...) + + o.Labels.AddCommonFlags(cmd) + switch res.Kind { + case "Interface": + o.Labels.AddInterfaceFlags(cmd) + case "VLAN": + o.Labels.AddVLANFlags(cmd) + } + o.PrintFlags.AddFlags(cmd) + + return cmd +} + +// Complete resolves the target namespace from the kubeconfig. +func (o *GetOptions) Complete() error { + namespace, _, err := o.Root.ConfigFlags.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + o.Namespace = namespace + return nil +} + +// Run fetches the requested resources and prints them to the configured output. +func (o *GetOptions) Run(args []string) error { + f := strings.ToLower(*o.PrintFlags.OutputFormat) + + var transforms []resource.RequestTransform + if f == "" || f == "wide" { + transforms = append(transforms, func(req *rest.Request) { + req.SetHeader("Accept", fmt.Sprintf("application/json;as=Table;v=%s;g=%s,application/json", metav1.SchemeGroupVersion.Version, metav1.GroupName)) + }) + } + + result, err := buildResourceResult(o.Root.ConfigFlags, o.Namespace, o.Root.AllNamespaces, o.Resource.QualifiedName(), args, o.Labels.BuildSelector(), transforms...) + if err != nil { + return err + } + + infos, err := result.Infos() + if err != nil { + return err + } + + // Table output (default or -o wide). + if f == "" || f == "wide" { + if len(infos) == 0 { + fmt.Fprintf(o.Root.IOStreams.ErrOut, "No resources found in %s namespace.\n", o.Namespace) + return nil + } + printer := printers.NewTablePrinter(printers.PrintOptions{ + Wide: f == "wide", + WithNamespace: o.Root.AllNamespaces, + }) + for _, info := range infos { + if info.Object == nil { + continue + } + table := &metav1.Table{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(info.Object.(*unstructured.Unstructured).Object, table); err != nil { + return err + } + if err := printer.PrintObj(table, o.Root.IOStreams.Out); err != nil { + return err + } + } + return nil + } + + for _, info := range infos { + if info.Object != nil && info.Object.GetObjectKind().GroupVersionKind().Empty() { + info.Object.GetObjectKind().SetGroupVersionKind(info.Mapping.GroupVersionKind) + } + } + + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return err + } + + // -o name prints each resource individually. + if f == "name" { + for _, info := range infos { + if info.Object != nil { + if err := printer.PrintObj(info.Object, o.Root.IOStreams.Out); err != nil { + return err + } + } + } + return nil + } + + // Structured output (-o json, -o yaml, etc.). + // A single resource is printed directly; multiple resources are wrapped in a List. + if len(infos) == 1 && infos[0].Object != nil { + return printer.PrintObj(infos[0].Object, o.Root.IOStreams.Out) + } + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(schema.GroupVersionKind{Version: "v1", Kind: "List"}) + list.SetResourceVersion("") + for _, info := range infos { + if info.Object != nil { + list.Items = append(list.Items, *info.Object.(*unstructured.Unstructured)) + } + } + return printer.PrintObj(list, o.Root.IOStreams.Out) +} + +// buildResourceResult constructs a resource.Result for the given resource type, +// optional names, and label selector using the Kubernetes resource builder. +func buildResourceResult(configFlags *genericclioptions.ConfigFlags, namespace string, allNamespaces bool, resourceName string, args []string, labelSelector string, transforms ...resource.RequestTransform) (*resource.Result, error) { + resourceArgs := append([]string{resourceName}, args...) + + builder := resource.NewBuilder(configFlags). + Unstructured(). + NamespaceParam(namespace). + DefaultNamespace(). + AllNamespaces(allNamespaces). + ResourceTypeOrNameArgs(true, resourceArgs...). + LabelSelectorParam(labelSelector). + ContinueOnError(). + Latest(). + Flatten(). + TransformRequests(transforms...) + + result := builder.Do() + if err := result.Err(); err != nil { + return nil, err + } + + return result, nil +} diff --git a/kubectl-net/pkg/cmd/labels.go b/kubectl-net/pkg/cmd/labels.go new file mode 100644 index 000000000..b09c2767c --- /dev/null +++ b/kubectl-net/pkg/cmd/labels.go @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" +) + +// LabelFlags holds label-based filter flags used by get and pause commands. +type LabelFlags struct { + Device string + + Aggregate string + VRF string + + RoutedVLAN string + EVI string +} + +// AddCommonFlags registers the --device flag available for all resource types. +func (l *LabelFlags) AddCommonFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&l.Device, "device", "d", "", fmt.Sprintf("Filter by %s label", v1alpha1.DeviceLabel)) +} + +// AddInterfaceFlags registers the --aggregate and --vrf flags for Interface resources. +func (l *LabelFlags) AddInterfaceFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&l.Aggregate, "aggregate", "", fmt.Sprintf("Filter by %s label", v1alpha1.AggregateLabel)) + cmd.Flags().StringVar(&l.VRF, "vrf", "", fmt.Sprintf("Filter by %s label", v1alpha1.VRFLabel)) +} + +// AddVLANFlags registers the --routed-vlan and --evi flags for VLAN resources. +func (l *LabelFlags) AddVLANFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&l.RoutedVLAN, "routed-vlan", "", fmt.Sprintf("Filter by %s label", v1alpha1.RoutedVLANLabel)) + cmd.Flags().StringVar(&l.EVI, "evi", "", fmt.Sprintf("Filter by %s label", v1alpha1.L2VNILabel)) +} + +// BuildSelector returns a comma-separated Kubernetes label selector string +// from the populated flag values. +func (l *LabelFlags) BuildSelector() string { + parts := []string{} + + if l.Device != "" { + parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.DeviceLabel, l.Device)) + } + if l.Aggregate != "" { + parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.AggregateLabel, l.Aggregate)) + } + if l.VRF != "" { + parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.VRFLabel, l.VRF)) + } + if l.RoutedVLAN != "" { + parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.RoutedVLANLabel, l.RoutedVLAN)) + } + if l.EVI != "" { + parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.L2VNILabel, l.EVI)) + } + + return strings.Join(parts, ",") +} diff --git a/kubectl-net/pkg/cmd/pause.go b/kubectl-net/pkg/cmd/pause.go new file mode 100644 index 000000000..4869cc135 --- /dev/null +++ b/kubectl-net/pkg/cmd/pause.go @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/dynamic" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" +) + +// PauseOptions holds the configuration for the "pause" and "unpause" subcommands. +type PauseOptions struct { + Root *RootOptions + Resource ResourceDef + Labels LabelFlags + PrintFlags *genericclioptions.PrintFlags + + Pause bool + Recursive bool + Namespace string +} + +// NewCmdPause returns a "pause" or "unpause" command depending on the pause flag, +// with a subcommand for each known resource type. +func NewCmdPause(root *RootOptions, pause bool) *cobra.Command { + use := "pause" + action := "paused" + short := "Pause network-operator resources" + if !pause { + use = "unpause" + action = "unpaused" + short = "Unpause network-operator resources" + } + + cmd := &cobra.Command{ + Use: use, + Short: short, + SilenceUsage: true, + } + + for _, res := range resourceDefs { + cmd.AddCommand(newPauseResourceCmd(root, res, pause, action)) + } + + return cmd +} + +// newPauseResourceCmd returns a command that pauses or unpauses a specific resource type. +func newPauseResourceCmd(root *RootOptions, res ResourceDef, pause bool, action string) *cobra.Command { + o := &PauseOptions{ + Root: root, + Resource: res, + Pause: pause, + PrintFlags: genericclioptions.NewPrintFlags(action), + } + + title := action + if len(title) > 0 { + title = strings.ToUpper(title[:1]) + title[1:] + } + + cmd := &cobra.Command{ + Use: fmt.Sprintf("%s [NAME...]", res.Name), + Short: fmt.Sprintf("%s %s", title, res.Name), + Args: cobra.ArbitraryArgs, + RunE: func(c *cobra.Command, args []string) error { + if err := o.Complete(); err != nil { + return err + } + return o.Run(args) + }, + } + cmd.Aliases = append(cmd.Aliases, res.Aliases...) + + o.Labels.AddCommonFlags(cmd) + switch res.Kind { + case "Interface": + o.Labels.AddInterfaceFlags(cmd) + case "VLAN": + o.Labels.AddVLANFlags(cmd) + } + if res.Kind == "Device" { + cmd.Flags().BoolVar(&o.Recursive, "recursive", false, "Also set spec.paused on the Device to pause or unpause child resources") + } + o.PrintFlags.AddFlags(cmd) + + return cmd +} + +// Complete resolves the target namespace from the kubeconfig. +func (o *PauseOptions) Complete() error { + namespace, _, err := o.Root.ConfigFlags.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + o.Namespace = namespace + return nil +} + +// Run patches the matched resources to set or remove the paused annotation. +func (o *PauseOptions) Run(args []string) error { + result, err := buildResourceResult(o.Root.ConfigFlags, o.Namespace, o.Root.AllNamespaces, o.Resource.QualifiedName(), args, o.Labels.BuildSelector()) + if err != nil { + return err + } + + infos, err := result.Infos() + if err != nil { + return err + } + if len(infos) == 0 { + return nil + } + + restConfig, err := o.Root.ConfigFlags.ToRESTConfig() + if err != nil { + return err + } + dyn, err := dynamic.NewForConfig(restConfig) + if err != nil { + return err + } + + patchBytes, err := o.buildPatch() + if err != nil { + return err + } + + patched := make([]*unstructured.Unstructured, 0, len(infos)) + for _, info := range infos { + resourceClient := dyn.Resource(info.Mapping.Resource) + var client dynamic.ResourceInterface = resourceClient + if info.Mapping.Scope.Name() == meta.RESTScopeNameNamespace && info.Namespace != "" { + client = resourceClient.Namespace(info.Namespace) + } + obj, err := client.Patch(context.Background(), info.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}) + if err != nil { + return err + } + patched = append(patched, obj) + } + + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return err + } + + if len(patched) == 1 { + return printer.PrintObj(patched[0], o.Root.IOStreams.Out) + } + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(schema.GroupVersionKind{Version: "v1", Kind: "List"}) + for _, obj := range patched { + list.Items = append(list.Items, *obj) + } + + return printer.PrintObj(list, o.Root.IOStreams.Out) +} + +// buildPatch returns the JSON merge-patch payload for setting or removing the +// paused annotation, optionally including spec.paused for recursive Device operations. +func (o *PauseOptions) buildPatch() ([]byte, error) { + annotations := map[string]any{} + if o.Pause { + annotations[v1alpha1.PausedAnnotation] = "true" + } else { + annotations[v1alpha1.PausedAnnotation] = nil + } + + patch := map[string]any{ + "metadata": map[string]any{ + "annotations": annotations, + }, + } + + if o.Recursive && o.Resource.Kind == "Device" { + patch["spec"] = map[string]any{ + "paused": o.Pause, + } + } + + return json.Marshal(patch) +} diff --git a/kubectl-net/pkg/cmd/resources.go b/kubectl-net/pkg/cmd/resources.go new file mode 100644 index 000000000..e6077cc34 --- /dev/null +++ b/kubectl-net/pkg/cmd/resources.go @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by generate-resources; DO NOT EDIT. + +package cmd + +type ResourceDef struct { + Name string + Aliases []string + Kind string + Group string + Version string +} + +var resourceDefs = []ResourceDef{ + {Name: "aaa", Aliases: []string{}, Kind: "AAA", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "aaaconfigs", Aliases: []string{"aaaconfig", "nxaaa"}, Kind: "AAAConfig", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "accesscontrollists", Aliases: []string{"accesscontrollist", "acl"}, Kind: "AccessControlList", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "banners", Aliases: []string{"banner"}, Kind: "Banner", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "bgp", Aliases: []string{}, Kind: "BGP", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "bgpconfigs", Aliases: []string{"bgpconfig", "nxbgp"}, Kind: "BGPConfig", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "bgppeers", Aliases: []string{"bgppeer", "peer", "bgpneighbor"}, Kind: "BGPPeer", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "bordergateways", Aliases: []string{"bordergateway", "bgw"}, Kind: "BorderGateway", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "certificates", Aliases: []string{"certificate", "cert", "netcert"}, Kind: "Certificate", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "claims", Aliases: []string{"claim"}, Kind: "Claim", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "configbackups", Aliases: []string{"configbackup"}, Kind: "ConfigBackup", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "devices", Aliases: []string{"device", "dev"}, Kind: "Device", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "dhcprelays", Aliases: []string{"dhcprelay"}, Kind: "DHCPRelay", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "dns", Aliases: []string{}, Kind: "DNS", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "ethernetsegments", Aliases: []string{"ethernetsegment", "es"}, Kind: "EthernetSegment", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "evpninstances", Aliases: []string{"evpninstance", "evi", "vni"}, Kind: "EVPNInstance", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "fabrics", Aliases: []string{"fabric"}, Kind: "Fabric", Group: "evpn.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "indexpools", Aliases: []string{"indexpool", "idxpool"}, Kind: "IndexPool", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "indices,singular=index,shortName=idx", Aliases: []string{}, Kind: "Index", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "interfaceconfigs", Aliases: []string{"interfaceconfig", "nxint"}, Kind: "InterfaceConfig", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "interfaces", Aliases: []string{"interface", "int"}, Kind: "Interface", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "ipaddresses,singular=ipaddress,shortName=ipa", Aliases: []string{}, Kind: "IPAddress", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "ipaddresspools", Aliases: []string{"ipaddresspool", "ippool"}, Kind: "IPAddressPool", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "ipprefixes,singular=ipprefix,shortName=pfx", Aliases: []string{}, Kind: "IPPrefix", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "ipprefixpools", Aliases: []string{"ipprefixpool", "pfxpool"}, Kind: "IPPrefixPool", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "isis", Aliases: []string{}, Kind: "ISIS", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "lldpconfigs", Aliases: []string{"lldpconfig"}, Kind: "LLDPConfig", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "lldps", Aliases: []string{"lldp"}, Kind: "LLDP", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "managementaccessconfigs", Aliases: []string{"managementaccessconfig", "nxmgmt", "nxmgmtaccess"}, Kind: "ManagementAccessConfig", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "managementaccesses", Aliases: []string{"managementaccess", "mgmt", "mgmtaccess"}, Kind: "ManagementAccess", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "networkvirtualizationedgeconfigs", Aliases: []string{"networkvirtualizationedgeconfig", "nveconfig"}, Kind: "NetworkVirtualizationEdgeConfig", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "networkvirtualizationedges", Aliases: []string{"networkvirtualizationedge", "nve", "vtep"}, Kind: "NetworkVirtualizationEdge", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "ntp", Aliases: []string{}, Kind: "NTP", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "ospf", Aliases: []string{}, Kind: "OSPF", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "pim", Aliases: []string{}, Kind: "PIM", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "prefixsets", Aliases: []string{"prefixset"}, Kind: "PrefixSet", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "routingpolicies", Aliases: []string{"routingpolicy", "routemap"}, Kind: "RoutingPolicy", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "snmp", Aliases: []string{}, Kind: "SNMP", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "syslogs", Aliases: []string{"syslog"}, Kind: "Syslog", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "systems", Aliases: []string{"system", "nxsystem"}, Kind: "System", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "users", Aliases: []string{"user"}, Kind: "User", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "vlans", Aliases: []string{"vlan"}, Kind: "VLAN", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "vpcdomains", Aliases: []string{"vpcdomain", "vpcdomain"}, Kind: "VPCDomain", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "vrfs", Aliases: []string{"vrf"}, Kind: "VRF", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, +} + +// QualifiedName returns the resource name in "name.group" form understood by +// the Kubernetes resource builder, pinning the lookup to this resource's API +// group and avoiding clashes with same-named types from other controllers +// (e.g. Calico BGPPeer, cert-manager Certificate). +func (r ResourceDef) QualifiedName() string { + if r.Group == "" { + return r.Name + } + return r.Name + "." + r.Group +} diff --git a/kubectl-net/pkg/cmd/root.go b/kubectl-net/pkg/cmd/root.go new file mode 100644 index 000000000..d4f47b94b --- /dev/null +++ b/kubectl-net/pkg/cmd/root.go @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/genericiooptions" +) + +// RootOptions holds the global configuration shared across all subcommands. +type RootOptions struct { + ConfigFlags *genericclioptions.ConfigFlags + IOStreams genericiooptions.IOStreams + AllNamespaces bool +} + +// NewCmdNet returns the root "kubectl net" command with all subcommands registered. +func NewCmdNet(streams genericiooptions.IOStreams) *cobra.Command { + o := &RootOptions{ + ConfigFlags: genericclioptions.NewConfigFlags(true), + IOStreams: streams, + } + + cmd := &cobra.Command{ + Use: "net", + Short: "Manage network-operator resources", + SilenceUsage: true, + Annotations: map[string]string{ + cobra.CommandDisplayNameAnnotation: "kubectl net", + }, + } + + cmd.SetOut(streams.Out) + cmd.SetErr(streams.ErrOut) + + o.ConfigFlags.AddFlags(cmd.PersistentFlags()) + cmd.PersistentFlags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", false, "If present, list requested objects across all namespaces") + + cmd.CompletionOptions.DisableDefaultCmd = true + cmd.AddCommand( + NewCmdGet(o), + NewCmdPause(o, true), + NewCmdPause(o, false), + NewCmdCompletion(cmd), + ) + + return cmd +} diff --git a/kubectl-net/tools/generate-resources/main.go b/kubectl-net/tools/generate-resources/main.go new file mode 100644 index 000000000..5545f7fba --- /dev/null +++ b/kubectl-net/tools/generate-resources/main.go @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +// generate-resources walks the network-operator API directory for *_types.go +// files and extracts resource definitions from kubebuilder markers: +// +// - +kubebuilder:resource:path= +// - +kubebuilder:resource:singular= +// - +kubebuilder:resource:shortName=[;...] +// +// It outputs a Go source file containing a ResourceDef slice with the plural +// name, aliases (singular + short names), and kind for each resource. This +// keeps the kubectl plugin's resource list in sync with the API types +// automatically. +// +// Usage: +// +// go run ./tools/generate-resources --api-root --out +package main + +import ( + "bytes" + "flag" + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "time" +) + +type resourceDef struct { + Name string + Aliases []string + Kind string + Group string + Version string +} + +func main() { + var apiRoot string + var outPath string + flag.StringVar(&apiRoot, "api-root", "", "Path to api directory") + flag.StringVar(&outPath, "out", "", "Output file path") + flag.Parse() + + if apiRoot == "" || outPath == "" { + fmt.Fprintln(os.Stderr, "usage: generate-resources --api-root --out ") + os.Exit(2) + } + + defs, err := loadResources(apiRoot) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + if err := writeOutput(outPath, defs); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +// packageMeta looks for a +groupName marker in groupversion.go and doc.go +// (in that order) and returns (group, version). version is derived from the +// last path segment of dir (e.g. "v1alpha1"). Returns empty group if no +// marker is found in either file. +func packageMeta(dir string) (group, version string) { + version = filepath.Base(dir) + for _, name := range []string{"groupversion.go", "doc.go"} { + data, err := os.ReadFile(filepath.Join(dir, name)) + if err != nil { + continue + } + for line := range strings.SplitSeq(string(data), "\n") { + if matches := groupNameRe.FindStringSubmatch(line); matches != nil { + return matches[1], version + } + } + } + return "", version +} + +func loadResources(apiRoot string) ([]resourceDef, error) { + var defs []resourceDef + cache := map[string][2]string{} + + err := filepath.WalkDir(apiRoot, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() || !strings.HasSuffix(entry.Name(), "_types.go") { + return nil + } + dir := filepath.Dir(path) + meta, ok := cache[dir] + if !ok { + g, v := packageMeta(dir) + meta = [2]string{g, v} + cache[dir] = meta + } + content, err := os.ReadFile(path) + if err != nil { + return err + } + defs = append(defs, extractResources(string(content), meta[0], meta[1])...) + return nil + }) + if err != nil { + return nil, err + } + slices.SortFunc(defs, func(i, j resourceDef) int { + return strings.Compare(i.Name, j.Name) + }) + return defs, nil +} + +var ( + groupNameRe = regexp.MustCompile(`^\s*//\s*\+groupName=(\S+)`) + resourcePathRe = regexp.MustCompile(`^//\s*\+kubebuilder:resource:path=([^\s]+)`) + resourceSingRe = regexp.MustCompile(`^//\s*\+kubebuilder:resource:singular=([^\s]+)`) + resourceShortRe = regexp.MustCompile(`^//\s*\+kubebuilder:resource:shortName=([^\s]+)`) + typeDefRe = regexp.MustCompile(`^type\s+(\w+)\s+struct\b`) +) + +func extractResources(content, group, version string) []resourceDef { + var defs []resourceDef + lines := strings.Split(content, "\n") + for i := range lines { + line := strings.TrimSpace(lines[i]) + m := resourcePathRe.FindStringSubmatch(line) + if m == nil { + continue + } + + var ( + path = strings.TrimSpace(m[1]) + kind string + singular string + shortNames []string + ) + + for j := i + 1; j < len(lines); j++ { + l := strings.TrimSpace(lines[j]) + if match := resourceSingRe.FindStringSubmatch(l); match != nil { + singular = strings.TrimSpace(match[1]) + continue + } + if match := resourceShortRe.FindStringSubmatch(l); match != nil { + for part := range strings.SplitSeq(match[1], ";") { + if name := strings.TrimSpace(part); name != "" { + shortNames = append(shortNames, name) + } + } + continue + } + if match := typeDefRe.FindStringSubmatch(l); match != nil { + kind = strings.TrimSpace(match[1]) + break + } + } + + if kind == "" { + continue + } + + aliases := make([]string, 0, 1+len(shortNames)) + if singular != "" && singular != path { + aliases = append(aliases, singular) + } + aliases = append(aliases, shortNames...) + + defs = append(defs, resourceDef{ + Name: path, + Aliases: aliases, + Kind: kind, + Group: group, + Version: version, + }) + } + return defs +} + +func writeOutput(outPath string, defs []resourceDef) error { + var buf bytes.Buffer + fmt.Fprintf(&buf, "// SPDX-"+"FileCopyrightText: %d SAP SE or an SAP affiliate company and IronCore contributors\n", time.Now().UTC().Year()) + fmt.Fprintln(&buf, "// SPDX-"+"License-Identifier: Apache-2.0") + fmt.Fprintln(&buf, "") + fmt.Fprintln(&buf, "// Code generated by generate-resources; DO NOT EDIT.") + fmt.Fprintln(&buf, "") + fmt.Fprintln(&buf, "package cmd") + fmt.Fprintln(&buf, "") + fmt.Fprintln(&buf, "type ResourceDef struct {") + fmt.Fprintln(&buf, "\tName string") + fmt.Fprintln(&buf, "\tAliases []string") + fmt.Fprintln(&buf, "\tKind string") + fmt.Fprintln(&buf, "\tGroup string") + fmt.Fprintln(&buf, "\tVersion string") + fmt.Fprintln(&buf, "}") + fmt.Fprintln(&buf, "") + fmt.Fprintln(&buf, "var resourceDefs = []ResourceDef{") + for _, def := range defs { + fmt.Fprintf(&buf, "\t{Name: %q, Aliases: %#v, Kind: %q, Group: %q, Version: %q},\n", def.Name, def.Aliases, def.Kind, def.Group, def.Version) + } + fmt.Fprintln(&buf, "}") + fmt.Fprintln(&buf, "") + fmt.Fprintln(&buf, "// QualifiedName returns the resource name in \"name.group\" form understood by") + fmt.Fprintln(&buf, "// the Kubernetes resource builder, pinning the lookup to this resource's API") + fmt.Fprintln(&buf, "// group and avoiding clashes with same-named types from other controllers") + fmt.Fprintln(&buf, "// (e.g. Calico BGPPeer, cert-manager Certificate).") + fmt.Fprintln(&buf, "func (r ResourceDef) QualifiedName() string {") + fmt.Fprintln(&buf, "\tif r.Group == \"\" {") + fmt.Fprintln(&buf, "\t\treturn r.Name") + fmt.Fprintln(&buf, "\t}") + fmt.Fprintln(&buf, "\treturn r.Name + \".\" + r.Group") + fmt.Fprintln(&buf, "}") + + if err := os.MkdirAll(filepath.Dir(outPath), fs.ModePerm); err != nil { + return err + } + return os.WriteFile(outPath, buf.Bytes(), 0o644) +} From 5a5dd03fe09adc1bbd62890bbf99cf64c53409b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Wed, 8 Jul 2026 18:52:14 +0200 Subject: [PATCH 4/8] Add --serial and --neighbor label filter flags to kubectl-net MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add --serial flag for Device resources to filter by DeviceSerialLabel, and --neighbor flag for Interface resources to filter by PhysicalInterfaceNeighborLabel. Both flags are available on get and pause/unpause commands. Signed-off-by: Felix Kästner --- kubectl-net/pkg/cmd/get.go | 2 ++ kubectl-net/pkg/cmd/labels.go | 16 +++++++++++++++- kubectl-net/pkg/cmd/pause.go | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/kubectl-net/pkg/cmd/get.go b/kubectl-net/pkg/cmd/get.go index 3021f8ebd..36a6cbfa7 100644 --- a/kubectl-net/pkg/cmd/get.go +++ b/kubectl-net/pkg/cmd/get.go @@ -66,6 +66,8 @@ func newGetResourceCmd(root *RootOptions, res ResourceDef) *cobra.Command { o.Labels.AddCommonFlags(cmd) switch res.Kind { + case "Device": + o.Labels.AddDeviceFlags(cmd) case "Interface": o.Labels.AddInterfaceFlags(cmd) case "VLAN": diff --git a/kubectl-net/pkg/cmd/labels.go b/kubectl-net/pkg/cmd/labels.go index b09c2767c..93a6a8ef7 100644 --- a/kubectl-net/pkg/cmd/labels.go +++ b/kubectl-net/pkg/cmd/labels.go @@ -15,9 +15,11 @@ import ( // LabelFlags holds label-based filter flags used by get and pause commands. type LabelFlags struct { Device string + Serial string Aggregate string VRF string + Neighbor string RoutedVLAN string EVI string @@ -28,10 +30,16 @@ func (l *LabelFlags) AddCommonFlags(cmd *cobra.Command) { cmd.Flags().StringVarP(&l.Device, "device", "d", "", fmt.Sprintf("Filter by %s label", v1alpha1.DeviceLabel)) } -// AddInterfaceFlags registers the --aggregate and --vrf flags for Interface resources. +// AddDeviceFlags registers flags specific to Device resources. +func (l *LabelFlags) AddDeviceFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&l.Serial, "serial", "", fmt.Sprintf("Filter by %s label", v1alpha1.DeviceSerialLabel)) +} + +// AddInterfaceFlags registers the --aggregate, --vrf, and --neighbor flags for Interface resources. func (l *LabelFlags) AddInterfaceFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&l.Aggregate, "aggregate", "", fmt.Sprintf("Filter by %s label", v1alpha1.AggregateLabel)) cmd.Flags().StringVar(&l.VRF, "vrf", "", fmt.Sprintf("Filter by %s label", v1alpha1.VRFLabel)) + cmd.Flags().StringVar(&l.Neighbor, "neighbor", "", fmt.Sprintf("Filter by %s label", v1alpha1.PhysicalInterfaceNeighborLabel)) } // AddVLANFlags registers the --routed-vlan and --evi flags for VLAN resources. @@ -48,12 +56,18 @@ func (l *LabelFlags) BuildSelector() string { if l.Device != "" { parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.DeviceLabel, l.Device)) } + if l.Serial != "" { + parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.DeviceSerialLabel, l.Serial)) + } if l.Aggregate != "" { parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.AggregateLabel, l.Aggregate)) } if l.VRF != "" { parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.VRFLabel, l.VRF)) } + if l.Neighbor != "" { + parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.PhysicalInterfaceNeighborLabel, l.Neighbor)) + } if l.RoutedVLAN != "" { parts = append(parts, fmt.Sprintf("%s=%s", v1alpha1.RoutedVLANLabel, l.RoutedVLAN)) } diff --git a/kubectl-net/pkg/cmd/pause.go b/kubectl-net/pkg/cmd/pause.go index 4869cc135..d78ac0544 100644 --- a/kubectl-net/pkg/cmd/pause.go +++ b/kubectl-net/pkg/cmd/pause.go @@ -87,6 +87,8 @@ func newPauseResourceCmd(root *RootOptions, res ResourceDef, pause bool, action o.Labels.AddCommonFlags(cmd) switch res.Kind { + case "Device": + o.Labels.AddDeviceFlags(cmd) case "Interface": o.Labels.AddInterfaceFlags(cmd) case "VLAN": From 6d5a5a44c239c52688fd6e5fe0542a9959882042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Tue, 18 Aug 2026 14:40:47 +0200 Subject: [PATCH 5/8] Fix resource name generation for single-line markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generator was capturing kubebuilder marker suffixes (singular=, shortName=) as part of the resource name when all attributes appeared on one line (e.g. path=indices,singular=index). Parse inline attributes from the same line as path= so the Name field contains only the plural resource name. Signed-off-by: Felix Kästner --- kubectl-net/pkg/cmd/resources.go | 6 ++--- kubectl-net/tools/generate-resources/main.go | 28 +++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/kubectl-net/pkg/cmd/resources.go b/kubectl-net/pkg/cmd/resources.go index e6077cc34..0288fe10c 100644 --- a/kubectl-net/pkg/cmd/resources.go +++ b/kubectl-net/pkg/cmd/resources.go @@ -32,12 +32,12 @@ var resourceDefs = []ResourceDef{ {Name: "evpninstances", Aliases: []string{"evpninstance", "evi", "vni"}, Kind: "EVPNInstance", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, {Name: "fabrics", Aliases: []string{"fabric"}, Kind: "Fabric", Group: "evpn.networking.metal.ironcore.dev", Version: "v1alpha1"}, {Name: "indexpools", Aliases: []string{"indexpool", "idxpool"}, Kind: "IndexPool", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, - {Name: "indices,singular=index,shortName=idx", Aliases: []string{}, Kind: "Index", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "indices", Aliases: []string{"index", "idx"}, Kind: "Index", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, {Name: "interfaceconfigs", Aliases: []string{"interfaceconfig", "nxint"}, Kind: "InterfaceConfig", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, {Name: "interfaces", Aliases: []string{"interface", "int"}, Kind: "Interface", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, - {Name: "ipaddresses,singular=ipaddress,shortName=ipa", Aliases: []string{}, Kind: "IPAddress", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "ipaddresses", Aliases: []string{"ipaddress", "ipa"}, Kind: "IPAddress", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, {Name: "ipaddresspools", Aliases: []string{"ipaddresspool", "ippool"}, Kind: "IPAddressPool", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, - {Name: "ipprefixes,singular=ipprefix,shortName=pfx", Aliases: []string{}, Kind: "IPPrefix", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, + {Name: "ipprefixes", Aliases: []string{"ipprefix", "pfx"}, Kind: "IPPrefix", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, {Name: "ipprefixpools", Aliases: []string{"ipprefixpool", "pfxpool"}, Kind: "IPPrefixPool", Group: "pool.networking.metal.ironcore.dev", Version: "v1alpha1"}, {Name: "isis", Aliases: []string{}, Kind: "ISIS", Group: "networking.metal.ironcore.dev", Version: "v1alpha1"}, {Name: "lldpconfigs", Aliases: []string{"lldpconfig"}, Kind: "LLDPConfig", Group: "nx.cisco.networking.metal.ironcore.dev", Version: "v1alpha1"}, diff --git a/kubectl-net/tools/generate-resources/main.go b/kubectl-net/tools/generate-resources/main.go index 5545f7fba..8e18bd503 100644 --- a/kubectl-net/tools/generate-resources/main.go +++ b/kubectl-net/tools/generate-resources/main.go @@ -119,7 +119,7 @@ func loadResources(apiRoot string) ([]resourceDef, error) { var ( groupNameRe = regexp.MustCompile(`^\s*//\s*\+groupName=(\S+)`) - resourcePathRe = regexp.MustCompile(`^//\s*\+kubebuilder:resource:path=([^\s]+)`) + resourcePathRe = regexp.MustCompile(`^//\s*\+kubebuilder:resource:path=([^,\s]+)`) resourceSingRe = regexp.MustCompile(`^//\s*\+kubebuilder:resource:singular=([^\s]+)`) resourceShortRe = regexp.MustCompile(`^//\s*\+kubebuilder:resource:shortName=([^\s]+)`) typeDefRe = regexp.MustCompile(`^type\s+(\w+)\s+struct\b`) @@ -142,6 +142,9 @@ func extractResources(content, group, version string) []resourceDef { shortNames []string ) + // Parse inline singular/shortName from the same line as path. + parseInlineAttrs(line, &singular, &shortNames) + for j := i + 1; j < len(lines); j++ { l := strings.TrimSpace(lines[j]) if match := resourceSingRe.FindStringSubmatch(l); match != nil { @@ -183,6 +186,29 @@ func extractResources(content, group, version string) []resourceDef { return defs } +// parseInlineAttrs extracts singular and shortName from a line like: +// +kubebuilder:resource:path=indices,singular=index,shortName=idx +func parseInlineAttrs(line string, singular *string, shortNames *[]string) { + idx := strings.Index(line, ":path=") + if idx < 0 { + return + } + attrs := line[idx+1:] // "path=indices,singular=index,shortName=idx" + for kv := range strings.SplitSeq(attrs, ",") { + k, v, _ := strings.Cut(strings.TrimSpace(kv), "=") + switch k { + case "singular": + *singular = v + case "shortName": + for sn := range strings.SplitSeq(v, ";") { + if name := strings.TrimSpace(sn); name != "" { + *shortNames = append(*shortNames, name) + } + } + } + } +} + func writeOutput(outPath string, defs []resourceDef) error { var buf bytes.Buffer fmt.Fprintf(&buf, "// SPDX-"+"FileCopyrightText: %d SAP SE or an SAP affiliate company and IronCore contributors\n", time.Now().UTC().Year()) From dd022ebd69db032a9b5f04a592e898f215e318a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Wed, 19 Aug 2026 16:12:31 +0200 Subject: [PATCH 6/8] Add move command to kubectl-net MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a 'kubectl net move' subcommand that migrates all network-operator resources from a source cluster namespace to a target cluster. Inspired by clusterctl move from cluster-api. The move algorithm: - Verify CRDs exist on the target cluster - Ensure the target namespace exists - Discover all network-operator resources in the source namespace - Pause all Devices on the source (spec.paused) - Create resources on the target (stripped of cluster-specific metadata and owner references) - Delete resources from the source (stripping finalizers) - Unpause Devices on the target Owner references are not rewritten; controllers re-establish them on first reconcile after unpause. The operation is idempotent and can be re-run on partial failure. Signed-off-by: Felix Kästner --- kubectl-net/pkg/cmd/move.go | 364 ++++++++++++++++++++++++++++++++++++ kubectl-net/pkg/cmd/must.go | 12 ++ kubectl-net/pkg/cmd/root.go | 1 + 3 files changed, 377 insertions(+) create mode 100644 kubectl-net/pkg/cmd/move.go create mode 100644 kubectl-net/pkg/cmd/must.go diff --git a/kubectl-net/pkg/cmd/move.go b/kubectl-net/pkg/cmd/move.go new file mode 100644 index 000000000..a1dee8fbf --- /dev/null +++ b/kubectl-net/pkg/cmd/move.go @@ -0,0 +1,364 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/dynamic" +) + +// MoveOptions holds the configuration for the "move" subcommand. +type MoveOptions struct { + Root *RootOptions + + ToKubeconfig string + ToContext string + DryRun bool + Namespace string + ScaleDownDeployment string + DeploymentNamespace string +} + +// NewCmdMove returns the "move" command for migrating resources between clusters. +func NewCmdMove(root *RootOptions) *cobra.Command { + o := &MoveOptions{Root: root} + + cmd := &cobra.Command{ + Use: "move", + Short: "Move network-operator resources to a target cluster", + Long: `Move network-operator resources from the source cluster to a target cluster. + +All network-operator resources in the namespace are moved. The target cluster +must already have network-operator CRDs installed.`, + SilenceUsage: true, + RunE: func(c *cobra.Command, args []string) error { + if err := o.Complete(); err != nil { + return err + } + return o.Run(c.Context()) + }, + } + + cmd.Flags().StringVar(&o.ToKubeconfig, "to-kubeconfig", "", "Path to the target cluster kubeconfig (required)") + cmd.Flags().StringVar(&o.ToContext, "to-context", "", "Context in the target kubeconfig") + cmd.Flags().BoolVar(&o.DryRun, "dry-run", false, "Print what would be moved without executing") + cmd.Flags().StringVar(&o.ScaleDownDeployment, "scale-down-deployment", "", "Deployment to scale to 0 on source before moving") + cmd.Flags().StringVar(&o.DeploymentNamespace, "deployment-namespace", "", "Namespace of the deployment (defaults to resource namespace)") + + _ = cmd.MarkFlagRequired("to-kubeconfig") + + return cmd +} + +func (o *MoveOptions) Complete() error { + namespace, _, err := o.Root.ConfigFlags.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + o.Namespace = namespace + return nil +} + +// Run executes the move operation: +// preflight → discover → pause → create on target → delete from source → unpause. +func (o *MoveOptions) Run(ctx context.Context) error { + srcConfig, err := o.Root.ConfigFlags.ToRESTConfig() + if err != nil { + return fmt.Errorf("source cluster: %w", err) + } + srcDyn, err := dynamic.NewForConfig(srcConfig) + if err != nil { + return fmt.Errorf("source cluster: %w", err) + } + + tgtFlags := genericclioptions.NewConfigFlags(true) + tgtFlags.KubeConfig = &o.ToKubeconfig + if o.ToContext != "" { + tgtFlags.Context = &o.ToContext + } + tgtConfig, err := tgtFlags.ToRESTConfig() + if err != nil { + return fmt.Errorf("target cluster: %w", err) + } + tgtDyn, err := dynamic.NewForConfig(tgtConfig) + if err != nil { + return fmt.Errorf("target cluster: %w", err) + } + + // Pre-flight: verify CRDs exist on target. + fmt.Fprintf(o.Root.IOStreams.Out, "Checking target cluster CRDs...\n") + crd := schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"} + for _, def := range resourceDefs { + name := def.Name + "." + def.Group + _, err := tgtDyn.Resource(crd).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("target cluster missing CRD: %s", name) + } + } + + // Ensure namespace exists on target. + ns := schema.GroupVersionResource{Version: "v1", Resource: "namespaces"} + _, err = tgtDyn.Resource(ns).Get(ctx, o.Namespace, metav1.GetOptions{}) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("checking namespace on target: %w", err) + } + n := &unstructured.Unstructured{} + n.SetGroupVersionKind(schema.GroupVersionKind{Version: "v1", Kind: "Namespace"}) + n.SetName(o.Namespace) + _, err = tgtDyn.Resource(ns).Create(ctx, n, metav1.CreateOptions{}) + if err != nil && !errors.IsAlreadyExists(err) { + return fmt.Errorf("creating namespace on target: %w", err) + } + } + + // Discover all network-operator resources in the source namespace. + fmt.Fprintf(o.Root.IOStreams.Out, "Discovering resources in namespace %q...\n", o.Namespace) + objects, err := o.DiscoverResources(ctx, srcDyn) + if err != nil { + return err + } + if len(objects) == 0 { + fmt.Fprintf(o.Root.IOStreams.Out, "No resources found.\n") + return nil + } + + // Check nothing is already paused. + for _, obj := range objects { + if obj.GetKind() == "Device" { + paused, _, _ := unstructured.NestedBool(obj.Object, "spec", "paused") + if paused { + return fmt.Errorf("device %q is already paused; unpause before moving", obj.GetName()) + } + } + } + + if o.DryRun { + for _, obj := range objects { + fmt.Fprintf(o.Root.IOStreams.Out, " %s/%s\n", obj.GetKind(), obj.GetName()) + } + fmt.Fprintf(o.Root.IOStreams.Out, "\nTotal: %d resources would be moved.\n", len(objects)) + return nil + } + + // Scale down operator deployment on source (if requested). + if o.ScaleDownDeployment != "" { + ns := o.DeploymentNamespace + if ns == "" { + ns = o.Namespace + } + fmt.Fprintf(o.Root.IOStreams.Out, "Scaling down %s/%s on source...\n", ns, o.ScaleDownDeployment) + deploy := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"} + patch := must(json.Marshal(map[string]any{ + "spec": map[string]any{"replicas": 0}, + })) + _, err := srcDyn.Resource(deploy).Namespace(ns).Patch(ctx, o.ScaleDownDeployment, types.MergePatchType, patch, metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("scaling down deployment %q: %w", o.ScaleDownDeployment, err) + } + } + + // Pause devices on source. + fmt.Fprintf(o.Root.IOStreams.Out, "Pausing Devices on source...\n") + device := schema.GroupVersionResource{Group: "networking.metal.ironcore.dev", Version: "v1alpha1", Resource: "devices"} + patch := must(json.Marshal(map[string]any{ + "spec": map[string]any{"paused": true}, + })) + for _, obj := range objects { + if obj.GetKind() == "Device" { + _, err := srcDyn.Resource(device).Namespace(o.Namespace).Patch(ctx, obj.GetName(), types.MergePatchType, patch, metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("pausing device %q: %w", obj.GetName(), err) + } + } + } + + // Create on target. + fmt.Fprintf(o.Root.IOStreams.Out, "Creating resources on target...\n") + var errs []error + for _, obj := range objects { + if err := o.CreateOnTarget(ctx, tgtDyn, obj); err != nil { + errs = append(errs, fmt.Errorf("create %s/%s: %w", obj.GetKind(), obj.GetName(), err)) + } + } + if len(errs) > 0 { + for _, err := range errs { + fmt.Fprintf(o.Root.IOStreams.ErrOut, " error: %v\n", err) + } + return fmt.Errorf("%d errors creating resources on target", len(errs)) + } + + // Delete from source. + fmt.Fprintf(o.Root.IOStreams.Out, "Deleting resources from source...\n") + for _, obj := range objects { + if err := o.DeleteFromSource(ctx, srcDyn, obj); err != nil { + errs = append(errs, fmt.Errorf("delete %s/%s: %w", obj.GetKind(), obj.GetName(), err)) + } + } + if len(errs) > 0 { + for _, err := range errs { + fmt.Fprintf(o.Root.IOStreams.ErrOut, " error: %v\n", err) + } + return fmt.Errorf("%d errors deleting resources from source", len(errs)) + } + + // Unpause devices on target. + fmt.Fprintf(o.Root.IOStreams.Out, "Unpausing Devices on target...\n") + patch = must(json.Marshal(map[string]any{ + "spec": map[string]any{"paused": false}, + })) + for _, obj := range objects { + if obj.GetKind() == "Device" { + _, err := tgtDyn.Resource(device).Namespace(o.Namespace).Patch(ctx, obj.GetName(), types.MergePatchType, patch, metav1.PatchOptions{}) + if err != nil { + return fmt.Errorf("unpausing device %q: %w", obj.GetName(), err) + } + } + } + + fmt.Fprintf(o.Root.IOStreams.Out, "Move complete.\n") + return nil +} + +// DiscoverResources lists all network-operator resources plus Secrets and +// ConfigMaps in the source namespace. +func (o *MoveOptions) DiscoverResources(ctx context.Context, dyn dynamic.Interface) ([]*unstructured.Unstructured, error) { + var all []*unstructured.Unstructured + for _, def := range resourceDefs { + gvr := schema.GroupVersionResource{Group: def.Group, Version: def.Version, Resource: def.Name} + list, err := dyn.Resource(gvr).Namespace(o.Namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + if errors.IsNotFound(err) || meta.IsNoMatchError(err) { + continue + } + return nil, fmt.Errorf("listing %s: %w", def.Name, err) + } + for i := range list.Items { + item := &list.Items[i] + item.SetGroupVersionKind(schema.GroupVersionKind{ + Group: def.Group, + Version: def.Version, + Kind: def.Kind, + }) + all = append(all, item) + } + } + + // Also move Secrets and ConfigMaps referenced by controllers. + for _, extra := range []schema.GroupVersionResource{ + {Version: "v1", Resource: "secrets"}, + {Version: "v1", Resource: "configmaps"}, + } { + list, err := dyn.Resource(extra).Namespace(o.Namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("listing %s: %w", extra.Resource, err) + } + for i := range list.Items { + item := &list.Items[i] + if item.GetName() == "kube-root-ca.crt" { + continue + } + item.SetGroupVersionKind(schema.GroupVersionKind{ + Version: "v1", + Kind: list.Items[i].GetKind(), + }) + all = append(all, item) + } + } + + return all, nil +} + +// CreateOnTarget creates a single object on the target cluster. +// Owner references are stripped — controllers re-establish them on first reconcile. +func (o *MoveOptions) CreateOnTarget(ctx context.Context, dyn dynamic.Interface, obj *unstructured.Unstructured) error { + tgt := obj.DeepCopy() + + tgt.SetResourceVersion("") + tgt.SetUID("") + tgt.SetCreationTimestamp(metav1.Time{}) + tgt.SetGeneration(0) + tgt.SetManagedFields(nil) + tgt.SetOwnerReferences(nil) + delete(tgt.Object, "status") + + gvr := gvrFromObject(obj) + client := dyn.Resource(gvr).Namespace(o.Namespace) + _, err := client.Create(ctx, tgt, metav1.CreateOptions{}) + if err != nil { + if !errors.IsAlreadyExists(err) { + return err + } + existing, err := client.Get(ctx, tgt.GetName(), metav1.GetOptions{}) + if err != nil { + return err + } + tgt.SetResourceVersion(existing.GetResourceVersion()) + _, err = client.Update(ctx, tgt, metav1.UpdateOptions{}) + return err + } + return nil +} + +// DeleteFromSource deletes a single object from the source cluster. +func (o *MoveOptions) DeleteFromSource(ctx context.Context, dyn dynamic.Interface, obj *unstructured.Unstructured) error { + gvr := gvrFromObject(obj) + client := dyn.Resource(gvr).Namespace(o.Namespace) + err := client.Delete(ctx, obj.GetName(), metav1.DeleteOptions{}) + if errors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + + // Strip finalizers to allow immediate GC (controllers are paused). + if len(obj.GetFinalizers()) > 0 { + patch := must(json.Marshal(map[string]any{ + "metadata": map[string]any{"finalizers": []string{}}, + })) + _, err = client.Patch(ctx, obj.GetName(), types.MergePatchType, patch, metav1.PatchOptions{}) + if err != nil && !errors.IsNotFound(err) { + return err + } + } + + return nil +} + +func gvrFromObject(obj *unstructured.Unstructured) schema.GroupVersionResource { + gvk := obj.GroupVersionKind() + for _, def := range resourceDefs { + if def.Kind == gvk.Kind && def.Group == gvk.Group { + return schema.GroupVersionResource{ + Group: def.Group, + Version: def.Version, + Resource: def.Name, + } + } + } + // Fallback for core types (Secret, ConfigMap, etc.). + switch gvk.Kind { + case "Secret": + return schema.GroupVersionResource{Version: "v1", Resource: "secrets"} + case "ConfigMap": + return schema.GroupVersionResource{Version: "v1", Resource: "configmaps"} + } + return schema.GroupVersionResource{ + Group: gvk.Group, + Version: gvk.Version, + Resource: fmt.Sprintf("%ss", gvk.Kind), + } +} diff --git a/kubectl-net/pkg/cmd/must.go b/kubectl-net/pkg/cmd/must.go new file mode 100644 index 000000000..427632694 --- /dev/null +++ b/kubectl-net/pkg/cmd/must.go @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +// must panics if err is non-nil, otherwise returns v. +func must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} diff --git a/kubectl-net/pkg/cmd/root.go b/kubectl-net/pkg/cmd/root.go index d4f47b94b..a408db20c 100644 --- a/kubectl-net/pkg/cmd/root.go +++ b/kubectl-net/pkg/cmd/root.go @@ -43,6 +43,7 @@ func NewCmdNet(streams genericiooptions.IOStreams) *cobra.Command { NewCmdGet(o), NewCmdPause(o, true), NewCmdPause(o, false), + NewCmdMove(o), NewCmdCompletion(cmd), ) From 3661472b463dcede76afa576948a1508f5d4b591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Thu, 20 Aug 2026 13:16:15 +0200 Subject: [PATCH 7/8] Exclude kubectl-net from controller-gen paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit controller-gen with paths="./..." recurses into the kubectl-net sub-module, fails to resolve its dependencies in CI, and aborts. Narrow the paths to ./api/ and ./internal/ where the actual types and controllers live. Signed-off-by: Felix Kästner --- Makefile | 6 +++--- kubectl-net/pkg/cmd/move.go | 22 +++++++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 534afc3a6..6c6007cda 100644 --- a/Makefile +++ b/Makefile @@ -48,14 +48,14 @@ help: ## Display this help. .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) crd rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd rbac:roleName=manager-role webhook paths="{./api/...,./internal/...}" output:crd:artifacts:config=config/crd/bases YEAR ?= $(shell date +%Y) .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt",year="$(YEAR)" paths="./..." - $(CONTROLLER_GEN) applyconfiguration:headerFile="hack/boilerplate.go.txt" paths="./..." + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt",year="$(YEAR)" paths="{./api/...,./internal/...}" + $(CONTROLLER_GEN) applyconfiguration:headerFile="hack/boilerplate.go.txt" paths="{./api/...,./internal/...}" .PHONY: fmt fmt: goimports gofumpt ## Run goimports and gofumpt against code. diff --git a/kubectl-net/pkg/cmd/move.go b/kubectl-net/pkg/cmd/move.go index a1dee8fbf..64d486740 100644 --- a/kubectl-net/pkg/cmd/move.go +++ b/kubectl-net/pkg/cmd/move.go @@ -4,9 +4,11 @@ package cmd import ( + "cmp" "context" "encoding/json" "fmt" + "slices" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" @@ -185,7 +187,10 @@ func (o *MoveOptions) Run(ctx context.Context) error { } } - // Create on target. + // Create on target (sorted by priority: devices, pools, allocations, claims, rest). + slices.SortFunc(objects, func(a, b *unstructured.Unstructured) int { + return cmp.Compare(movePriority(a), movePriority(b)) + }) fmt.Fprintf(o.Root.IOStreams.Out, "Creating resources on target...\n") var errs []error for _, obj := range objects { @@ -362,3 +367,18 @@ func gvrFromObject(obj *unstructured.Unstructured) schema.GroupVersionResource { Resource: fmt.Sprintf("%ss", gvk.Kind), } } + +func movePriority(obj *unstructured.Unstructured) int { + switch obj.GetKind() { + case "Device": + return 0 + case "IndexPool", "IPAddressPool", "IPPrefixPool": + return 1 + case "Index", "IPAddress", "IPPrefix": + return 2 + case "Claim": + return 3 + default: + return 4 + } +} From ee7091ba8a725f54dd77ebb7c11f78dbb4867a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Mon, 24 Aug 2026 15:30:03 +0200 Subject: [PATCH 8/8] Add version command and Makefile target for kubectl-net MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a 'kubectl net version' subcommand that prints the version injected at build time via ldflags. Add a 'make kubectl-net' target that builds the plugin binary into bin/ with stripped symbols and version info from git describe. Signed-off-by: Felix Kästner --- Makefile | 4 ++++ kubectl-net/cmd/kubectl-net.go | 4 +++- kubectl-net/pkg/cmd/root.go | 3 ++- kubectl-net/pkg/cmd/version.go | 21 +++++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 kubectl-net/pkg/cmd/version.go diff --git a/Makefile b/Makefile index 6c6007cda..fa2862630 100644 --- a/Makefile +++ b/Makefile @@ -181,6 +181,10 @@ run-docs: build: manifests generate fmt vet ## Build manager binary. CGO_ENABLED=0 go build $(LDFLAGS) -o bin/manager cmd/main.go +.PHONY: kubectl-net +kubectl-net: ## Build kubectl-net plugin binary. + cd kubectl-net && CGO_ENABLED=0 go build -ldflags="-s -w -X 'main.version=$(VERSION)'" -o ../bin/kubectl-net ./cmd/kubectl-net.go + .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. go run ./cmd/main.go diff --git a/kubectl-net/cmd/kubectl-net.go b/kubectl-net/cmd/kubectl-net.go index 053b67601..7faf9ea3e 100644 --- a/kubectl-net/cmd/kubectl-net.go +++ b/kubectl-net/cmd/kubectl-net.go @@ -13,11 +13,13 @@ import ( "github.com/ironcore-dev/kubectl-net/pkg/cmd" ) +var version = "dev" + func main() { flags := pflag.NewFlagSet("kubectl-net", pflag.ExitOnError) pflag.CommandLine = flags - root := cmd.NewCmdNet(genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) + root := cmd.NewCmdNet(genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}, version) if err := root.Execute(); err != nil { os.Exit(1) } diff --git a/kubectl-net/pkg/cmd/root.go b/kubectl-net/pkg/cmd/root.go index a408db20c..ae0a2b0c0 100644 --- a/kubectl-net/pkg/cmd/root.go +++ b/kubectl-net/pkg/cmd/root.go @@ -17,7 +17,7 @@ type RootOptions struct { } // NewCmdNet returns the root "kubectl net" command with all subcommands registered. -func NewCmdNet(streams genericiooptions.IOStreams) *cobra.Command { +func NewCmdNet(streams genericiooptions.IOStreams, version string) *cobra.Command { o := &RootOptions{ ConfigFlags: genericclioptions.NewConfigFlags(true), IOStreams: streams, @@ -44,6 +44,7 @@ func NewCmdNet(streams genericiooptions.IOStreams) *cobra.Command { NewCmdPause(o, true), NewCmdPause(o, false), NewCmdMove(o), + NewCmdVersion(o, version), NewCmdCompletion(cmd), ) diff --git a/kubectl-net/pkg/cmd/version.go b/kubectl-net/pkg/cmd/version.go new file mode 100644 index 000000000..b7c0671f2 --- /dev/null +++ b/kubectl-net/pkg/cmd/version.go @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func NewCmdVersion(root *RootOptions, version string) *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version", + RunE: func(cmd *cobra.Command, args []string) error { + _, err := fmt.Fprintln(root.IOStreams.Out, version) + return err + }, + } +}