diff --git a/.gitattributes b/.gitattributes index 06b6f9bb..b6152028 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,9 @@ # and editors capable of LF line endings. *.go text eol=lf diff=golang +# Images should be stored in Git LFS, and not have their line endings modified. +*.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 charts/network-operator/templates/** linguist-generated diff --git a/README.md b/README.md index ef21b7da..a5057719 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,17 @@ make undeploy-crds 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 00000000..fb2d8d84 --- /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 00000000..f6f2ff3c --- /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 00000000..e51c00b9 --- /dev/null +++ b/kubectl-net/cmd/kubectl-net.go @@ -0,0 +1,23 @@ +// 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" + + "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 00000000..17c9d7a1 --- /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 00000000..e9988b1b --- /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 00000000..66ef3679 --- /dev/null +++ b/kubectl-net/go.mod @@ -0,0 +1,75 @@ +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.0 + k8s.io/cli-runtime v0.35.2 + k8s.io/client-go v0.36.0 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // 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/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // 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.0 // 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.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/api v0.36.0 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.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 00000000..c9b4bf74 --- /dev/null +++ b/kubectl-net/go.sum @@ -0,0 +1,170 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +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/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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +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/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.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +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/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/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.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +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.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +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/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +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.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/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.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= +k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= +k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= +k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= +k8s.io/cli-runtime v0.35.2 h1:3DNctzpPNXavqyrm/FFiT60TLk4UjUxuUMYbKOE970E= +k8s.io/cli-runtime v0.35.2/go.mod h1:G2Ieu0JidLm5m1z9b0OkFhnykvJ1w+vjbz1tR5OFKL0= +k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= +k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= +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-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +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.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +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.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.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 00000000..e9c73874 --- /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 00000000..1368884f --- /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 00000000..ae20d00c --- /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 00000000..df643273 --- /dev/null +++ b/kubectl-net/pkg/cmd/get.go @@ -0,0 +1,205 @@ +// 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 := "" + if o.PrintFlags.OutputFormat != nil { + 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 + } + u, ok := info.Object.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("unexpected object type %T", info.Object) + } + table := &metav1.Table{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.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 00000000..b09c2767 --- /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 00000000..2499b300 --- /dev/null +++ b/kubectl-net/pkg/cmd/pause.go @@ -0,0 +1,200 @@ +// 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 + } + if obj.GetObjectKind().GroupVersionKind().Empty() { + obj.GetObjectKind().SetGroupVersionKind(info.Mapping.GroupVersionKind) + } + 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 00000000..bc6e245f --- /dev/null +++ b/kubectl-net/pkg/cmd/resources.go @@ -0,0 +1,68 @@ +// 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: "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: "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: "evpninstances", Aliases: []string{"evpninstance", "evi", "vni"}, Kind: "EVPNInstance", Group: "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: "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 +} + +func allResourceNames() []string { + names := make([]string, 0, len(resourceDefs)) + for _, def := range resourceDefs { + names = append(names, def.Name) + } + return names +} diff --git a/kubectl-net/pkg/cmd/root.go b/kubectl-net/pkg/cmd/root.go new file mode 100644 index 00000000..d4f47b94 --- /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 00000000..0e53923b --- /dev/null +++ b/kubectl-net/tools/generate-resources/main.go @@ -0,0 +1,232 @@ +// 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, "}") + fmt.Fprintln(&buf, "") + fmt.Fprintln(&buf, "func allResourceNames() []string {") + fmt.Fprintln(&buf, "\tnames := make([]string, 0, len(resourceDefs))") + fmt.Fprintln(&buf, "\tfor _, def := range resourceDefs {") + fmt.Fprintln(&buf, "\t\tnames = append(names, def.Name)") + fmt.Fprintln(&buf, "\t}") + fmt.Fprintln(&buf, "\treturn names") + fmt.Fprintln(&buf, "}") + + if err := os.MkdirAll(filepath.Dir(outPath), fs.ModePerm); err != nil { + return err + } + return os.WriteFile(outPath, buf.Bytes(), 0o644) +}