From a211da55782b90c8d958e7c356e78a2851bf267e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20K=C3=A4stner?= Date: Mon, 4 Aug 2025 13:05:56 +0200 Subject: [PATCH] Add `netop-provider` cli to directly invoke provider implementation This cli is meant to be used for testing and development of the provider implementation. It can be used to directly invoke the provider for a given k8s resource, without the need to deploy the full operator, leading to short development cycles. --- .editorconfig | 1 + .github/workflows/checks.yaml | 2 +- .github/workflows/ci.yaml | 4 +- .github/workflows/goreleaser.yaml | 2 +- .goreleaser.yaml | 2 +- .license-scan-overrides.jsonl | 2 + Makefile | 26 ++-- Makefile.maker.yaml | 6 + hack/provider/main.go | 235 ++++++++++++++++++++++++++++++ 9 files changed, 265 insertions(+), 15 deletions(-) create mode 100644 hack/provider/main.go diff --git a/.editorconfig b/.editorconfig index d2131327..46ace684 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,6 +12,7 @@ indent_size = 2 [{Makefile,go.mod,go.sum,*.go}] indent_style = tab +indent_size = unset [*.md] trim_trailing_whitespace = false diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 244e1a82..5f91f90c 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -29,7 +29,7 @@ jobs: uses: actions/setup-go@v5 with: check-latest: true - go-version: 1.24.4 + go-version: 1.24.5 - name: Run prepare make target run: make generate - name: Run golangci-lint diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6309367a..fdd97294 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -32,7 +32,7 @@ jobs: uses: actions/setup-go@v5 with: check-latest: true - go-version: 1.24.4 + go-version: 1.24.5 - name: Run prepare make target run: make generate - name: Build all binaries @@ -49,7 +49,7 @@ jobs: uses: actions/setup-go@v5 with: check-latest: true - go-version: 1.24.4 + go-version: 1.24.5 - name: Run prepare make target run: make generate - name: Run tests and generate coverage report diff --git a/.github/workflows/goreleaser.yaml b/.github/workflows/goreleaser.yaml index 3c4527c7..79d37ed0 100644 --- a/.github/workflows/goreleaser.yaml +++ b/.github/workflows/goreleaser.yaml @@ -27,7 +27,7 @@ jobs: uses: actions/setup-go@v5 with: check-latest: true - go-version: 1.24.4 + go-version: 1.24.5 - name: Run prepare make target run: make generate - name: Generate release info diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 0f96bdb8..971f4f87 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -7,7 +7,7 @@ archives: - name_template: '{{ .ProjectName }}-{{ replace .Version "v" "" }}-{{ .Os }}-{{ .Arch }}' format_overrides: - goos: windows - format: zip + formats: [ zip ] files: - CHANGELOG.md - LICENSE diff --git a/.license-scan-overrides.jsonl b/.license-scan-overrides.jsonl index c0b0fee9..0a8feb2e 100644 --- a/.license-scan-overrides.jsonl +++ b/.license-scan-overrides.jsonl @@ -1,8 +1,10 @@ {"name": "github.com/chzyer/logex", "licenceType": "MIT"} {"name": "github.com/hashicorp/vault/api/auth/approle", "licenceType": "MPL-2.0"} {"name": "github.com/jpillora/longestcommon", "licenceType": "MIT"} +{"name": "github.com/logrusorgru/aurora", "licenceType": "Unlicense"} {"name": "github.com/mattn/go-localereader", "licenceType": "MIT"} {"name": "github.com/miekg/dns", "licenceType": "BSD-3-Clause"} +{"name": "github.com/pashagolub/pgxmock/v4", "licenceType": "BSD-3-Clause"} {"name": "github.com/spdx/tools-golang", "licenceTextOverrideFile": "vendor/github.com/spdx/tools-golang/LICENSE.code"} {"name": "github.com/xeipuuv/gojsonpointer", "licenceType": "Apache-2.0"} {"name": "github.com/xeipuuv/gojsonreference", "licenceType": "Apache-2.0"} diff --git a/Makefile b/Makefile index 783b475f..d669e6be 100644 --- a/Makefile +++ b/Makefile @@ -126,6 +126,12 @@ charts: FORCE generate @kubebuilder edit --plugins=helm/v1-alpha @rm -rf charts/network-operator && mv dist/chart charts/network-operator && rm -rf dist +netop-provider: + @printf "\e[1;36m>> go build -o build/netop-provider ./hack/provider\e[0m\n" + @go build -o build/netop-provider ./hack/provider + @printf "\e[1;36m>> ./build/netop-provider --help\e[0m\n" + @./build/netop-provider --help + install-goimports: FORCE @if ! hash goimports 2>/dev/null; then printf "\e[1;36m>> Installing goimports (this may take a while)...\e[0m\n"; go install golang.org/x/tools/cmd/goimports@latest; fi @@ -136,10 +142,7 @@ install-modernize: FORCE @if ! hash modernize 2>/dev/null; then printf "\e[1;36m>> Installing modernize (this may take a while)...\e[0m\n"; go install golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest; fi install-shellcheck: FORCE - @if ! hash shellcheck 2>/dev/null; then printf "\e[1;36m>> Installing shellcheck...\e[0m\n"; SHELLCHECK_ARCH=$(shell uname -m); SHELLCHECK_OS=$(shell uname -s | tr '[:upper:]' '[:lower:]'); if [[ "$$SHELLCHECK_OS" == "darwin" ]]; then SHELLCHECK_OS=macos; fi; SHELLCHECK_VERSION="stable"; curl -sLo- "https://github.com/koalaman/shellcheck/releases/download/$$SHELLCHECK_VERSION/shellcheck-$$SHELLCHECK_VERSION.$$SHELLCHECK_OS.$$SHELLCHECK_ARCH.tar.xz" | tar -Jxf -; BIN=$$(go env GOBIN); if [[ -z $$BIN ]]; then BIN=$$(go env GOPATH)/bin; fi; install -Dm755 shellcheck-$$SHELLCHECK_VERSION/shellcheck -t "$$BIN"; rm -rf shellcheck-$$SHELLCHECK_VERSION; fi - -install-ginkgo: FORCE - @if ! hash ginkgo 2>/dev/null; then printf "\e[1;36m>> Installing ginkgo (this may take a while)...\e[0m\n"; go install github.com/onsi/ginkgo/v2/ginkgo@latest; fi + @if ! hash shellcheck 2>/dev/null; then printf "\e[1;36m>> Installing shellcheck...\e[0m\n"; SHELLCHECK_ARCH=$(shell uname -m); SHELLCHECK_OS=$(shell uname -s | tr '[:upper:]' '[:lower:]'); if [[ "$$SHELLCHECK_OS" == "darwin" ]]; then SHELLCHECK_OS=macos; fi; SHELLCHECK_VERSION="stable"; if command -v curl >/dev/null 2>&1; then GET="curl -sLo-"; elif command -v wget >/dev/null 2>&1; then GET="wget -O-"; else echo "Didn't find curl or wget to download shellcheck"; exit 2; fi; $$GET "https://github.com/koalaman/shellcheck/releases/download/$$SHELLCHECK_VERSION/shellcheck-$$SHELLCHECK_VERSION.$$SHELLCHECK_OS.$$SHELLCHECK_ARCH.tar.xz" | tar -Jxf -; BIN=$$(go env GOBIN); if [[ -z $$BIN ]]; then BIN=$$(go env GOPATH)/bin; fi; install -Dm755 shellcheck-$$SHELLCHECK_VERSION/shellcheck -t "$$BIN"; rm -rf shellcheck-$$SHELLCHECK_VERSION; fi install-go-licence-detector: FORCE @if ! hash go-licence-detector 2>/dev/null; then printf "\e[1;36m>> Installing go-licence-detector (this may take a while)...\e[0m\n"; go install go.elastic.co/go-licence-detector@latest; fi @@ -147,7 +150,10 @@ install-go-licence-detector: FORCE install-addlicense: FORCE @if ! hash addlicense 2>/dev/null; then printf "\e[1;36m>> Installing addlicense (this may take a while)...\e[0m\n"; go install github.com/google/addlicense@latest; fi -prepare-static-check: FORCE install-golangci-lint install-modernize install-shellcheck install-ginkgo install-go-licence-detector install-addlicense +install-reuse: FORCE + @if ! hash reuse 2>/dev/null; then if ! hash pip3 2>/dev/null; then printf "\e[1;31m>> Cannot install reuse because no pip3 was found. Either install it using your package manager or install pip3\e[0m\n"; else printf "\e[1;36m>> Installing reuse...\e[0m\n"; pip3 install --user reuse; fi; fi + +prepare-static-check: FORCE install-golangci-lint install-modernize install-shellcheck install-go-licence-detector install-addlicense install-reuse install-controller-gen: FORCE @if ! hash controller-gen 2>/dev/null; then printf "\e[1;36m>> Installing controller-gen (this may take a while)...\e[0m\n"; go install sigs.k8s.io/controller-tools/cmd/controller-gen@latest; fi @@ -215,9 +221,9 @@ run-shellcheck: FORCE install-shellcheck @printf "\e[1;36m>> shellcheck\e[0m\n" @find . -type f \( -name '*.bash' -o -name '*.ksh' -o -name '*.zsh' -o -name '*.sh' -o -name '*.shlib' \) -exec shellcheck {} + -build/cover.out: FORCE install-ginkgo generate install-setup-envtest | build +build/cover.out: FORCE generate install-setup-envtest | build @printf "\e[1;36m>> Running tests\e[0m\n" - KUBEBUILDER_ASSETS=$$(setup-envtest use 1.32 -p path) ginkgo run --randomize-all -output-dir=build $(GO_BUILDFLAGS) -ldflags '-s -w -X github.com/sapcc/go-api-declarations/bininfo.binName=network-operator -X github.com/sapcc/go-api-declarations/bininfo.version=$(BININFO_VERSION) -X github.com/sapcc/go-api-declarations/bininfo.commit=$(BININFO_COMMIT_HASH) -X github.com/sapcc/go-api-declarations/bininfo.buildDate=$(BININFO_BUILD_DATE) $(GO_LDFLAGS)' -covermode=count -coverpkg=$(subst $(space),$(comma),$(GO_COVERPKGS)) $(GO_TESTPKGS) + KUBEBUILDER_ASSETS=$$(setup-envtest use 1.32 -p path) go run github.com/onsi/ginkgo/v2/ginkgo run --randomize-all -output-dir=build $(GO_BUILDFLAGS) -ldflags '-s -w -X github.com/sapcc/go-api-declarations/bininfo.binName=network-operator -X github.com/sapcc/go-api-declarations/bininfo.version=$(BININFO_VERSION) -X github.com/sapcc/go-api-declarations/bininfo.commit=$(BININFO_COMMIT_HASH) -X github.com/sapcc/go-api-declarations/bininfo.buildDate=$(BININFO_BUILD_DATE) $(GO_LDFLAGS)' -covermode=count -coverpkg=$(subst $(space),$(comma),$(GO_COVERPKGS)) $(GO_TESTPKGS) @mv build/coverprofile.out build/cover.out build/cover.html: build/cover.out @@ -228,7 +234,7 @@ check-addlicense: FORCE install-addlicense @printf "\e[1;36m>> addlicense --check\e[0m\n" @addlicense --check -- $(patsubst $(shell awk '$$1 == "module" {print $$2}' go.mod)%,.%/*.go,$(shell go list ./...)) -check-reuse: FORCE +check-reuse: FORCE install-reuse @printf "\e[1;36m>> reuse lint\e[0m\n" @if ! reuse lint -q; then reuse lint; fi @@ -246,7 +252,7 @@ tidy-deps: FORCE go mod tidy go mod verify -license-headers: FORCE install-addlicense +license-headers: FORCE install-addlicense install-reuse @printf "\e[1;36m>> addlicense (for license headers on source code files)\e[0m\n" @printf "%s\0" $(patsubst $(shell awk '$$1 == "module" {print $$2}' go.mod)%,.%/*.go,$(shell go list ./...)) | $(XARGS) -0 -I{} bash -c 'year="$$(grep 'Copyright' {} | head -n1 | grep -E -o '"'"'[0-9]{4}(-[0-9]{4})?'"'"')"; if [[ -z "$$year" ]]; then year=$$(date +%Y); fi; gawk -i inplace '"'"'{if (display) {print} else {!/^\/\*/ && !/^\*/}}; {if (!display && $$0 ~ /^(package |$$)/) {display=1} else { }}'"'"' {}; addlicense -c "SAP SE or an SAP affiliate company" -s=only -y "$$year" -- {}; $(SED) -i '"'"'1s+// Copyright +// SPDX-FileCopyrightText: +'"'"' {}; ' @printf "\e[1;36m>> reuse annotate (for license headers on other files)\e[0m\n" @@ -299,9 +305,9 @@ help: FORCE @printf " \e[36minstall-golangci-lint\e[0m Install golangci-lint required by run-golangci-lint/static-check\n" @printf " \e[36minstall-modernize\e[0m Install modernize required by run-modernize/static-check\n" @printf " \e[36minstall-shellcheck\e[0m Install shellcheck required by run-shellcheck/static-check\n" - @printf " \e[36minstall-ginkgo\e[0m Install ginkgo required when using it as test runner. This is used in CI before dropping privileges, you should probably install all the tools using your package manager\n" @printf " \e[36minstall-go-licence-detector\e[0m Install-go-licence-detector required by check-dependency-licenses/static-check\n" @printf " \e[36minstall-addlicense\e[0m Install addlicense required by check-license-headers/license-headers/static-check\n" + @printf " \e[36minstall-reuse\e[0m Install reuse required by license-headers/check-reuse\n" @printf " \e[36mprepare-static-check\e[0m Install any tools required by static-check. This is used in CI before dropping privileges, you should probably install all the tools using your package manager\n" @printf " \e[36minstall-controller-gen\e[0m Install controller-gen required by static-check and build-all. This is used in CI before dropping privileges, you should probably install all the tools using your package manager\n" @printf " \e[36minstall-setup-envtest\e[0m Install setup-envtest required by check. This is used in CI before dropping privileges, you should probably install all the tools using your package manager\n" diff --git a/Makefile.maker.yaml b/Makefile.maker.yaml index a4d6976f..d1cabb48 100644 --- a/Makefile.maker.yaml +++ b/Makefile.maker.yaml @@ -164,3 +164,9 @@ verbatim: | @printf "\e[1;36m>> kubebuilder edit --plugins=helm/v1-alpha\e[0m\n" @kubebuilder edit --plugins=helm/v1-alpha @rm -rf charts/network-operator && mv dist/chart charts/network-operator && rm -rf dist + + netop-provider: + @printf "\e[1;36m>> go build -o build/netop-provider ./hack/provider\e[0m\n" + @go build -o build/netop-provider ./hack/provider + @printf "\e[1;36m>> ./build/netop-provider --help\e[0m\n" + @./build/netop-provider --help diff --git a/hack/provider/main.go b/hack/provider/main.go new file mode 100644 index 00000000..6254a3d4 --- /dev/null +++ b/hack/provider/main.go @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/yaml" + + // Import all supported provider implementations. + _ "github.com/ironcore-dev/network-operator/internal/provider/openconfig" + + "github.com/ironcore-dev/network-operator/api/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/clientutil" + "github.com/ironcore-dev/network-operator/internal/provider" +) + +var ( + address = flag.String("address", "", "API endpoint address (required)") + username = flag.String("username", "", "Username for authentication (required)") + password = flag.String("password", "", "Password for authentication (required)") + file = flag.String("file", "", "Path to Kubernetes resource manifest file (required)") + providerName = flag.String("provider", "openconfig", "Provider implementation to use") +) + +func usage() { + fmt.Fprintf(os.Stderr, "Usage: %s [flags] \n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "A debug tool for testing provider implementations.\n\n") + fmt.Fprintf(os.Stderr, "Arguments:\n") + fmt.Fprintf(os.Stderr, " create|delete Operation to perform on the resource\n\n") + fmt.Fprintf(os.Stderr, "Flags:\n") + flag.PrintDefaults() + fmt.Fprintf(os.Stderr, "\nExample:\n") + fmt.Fprintf(os.Stderr, " %s -address=192.168.1.1:9339 -username=admin -password=secret -file=config/samples/v1alpha1_device.yaml create\n", os.Args[0]) +} + +func validateFlags() error { + if *address == "" { + return errors.New("address flag is required") + } + if *username == "" { + return errors.New("username flag is required") + } + if *password == "" { + return errors.New("password flag is required") + } + if *file == "" { + return errors.New("file flag is required") + } + return nil +} + +func validatePositionalArgs() (string, error) { + if len(flag.Args()) != 1 { + return "", errors.New("exactly one positional argument (create|delete) is required") + } + + operation := flag.Args()[0] + if operation != "create" && operation != "delete" { + return "", fmt.Errorf("positional argument must be either 'create' or 'delete', got: %s", operation) + } + + return operation, nil +} + +func loadAndUnmarshalResource(filePath string) (runtime.Object, error) { + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return nil, fmt.Errorf("file does not exist: %s", filePath) + } + + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %w", filePath, err) + } + + if err = v1alpha1.AddToScheme(scheme.Scheme); err != nil { + return nil, fmt.Errorf("failed to add scheme: %w", err) + } + + decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDeserializer() + + json, err := yaml.YAMLToJSON(data) + if err != nil { + return nil, fmt.Errorf("failed to convert YAML to JSON: %w", err) + } + + obj, _, err := decoder.Decode(json, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to decode resource: %w", err) + } + + return obj, nil +} + +func printResourceInfo(obj runtime.Object) { + switch resource := obj.(type) { + case *v1alpha1.Interface: + fmt.Printf("Loaded Interface: %s\n", resource.Name) + fmt.Printf(" Namespace: %s\n", resource.Namespace) + fmt.Printf(" Interface Name: %s\n", resource.Spec.Name) + fmt.Printf(" Admin State: %s\n", resource.Spec.AdminState) + default: + fmt.Printf("Loaded resource of unknown type: %T\n", resource) + } +} + +func main() { + flag.Usage = usage + + for _, arg := range os.Args[1:] { + if arg == "-h" || arg == "--help" { + flag.Usage() + os.Exit(0) + } + } + + flag.Parse() + + if err := validateFlags(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n\n", err) + flag.Usage() + os.Exit(1) + } + + operation, err := validatePositionalArgs() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n\n", err) + flag.Usage() + os.Exit(1) + } + + resource, err := loadAndUnmarshalResource(*file) + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading resource: %v\n", err) + os.Exit(1) + } + + obj, ok := resource.(client.Object) + if !ok { + fmt.Fprintf(os.Stderr, "Error: resource is not a client.Object\n") + os.Exit(1) + } + if obj.GetNamespace() == "" { + obj.SetNamespace(metav1.NamespaceDefault) + } + + fmt.Printf("=== Debug Tool Configuration ===\n") + fmt.Printf("Address: %s\n", *address) + fmt.Printf("Username: %s\n", *username) + fmt.Printf("Password: %s\n", "[REDACTED]") + fmt.Printf("Resource File: %s\n", *file) + fmt.Printf("Provider: %s\n", *providerName) + fmt.Printf("Operation: %s\n", operation) + fmt.Printf("\n=== Resource Information ===\n") + printResourceInfo(resource) + + prov, err := provider.Get(*providerName) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting provider: %v\n", err) + os.Exit(1) + } + + device := &v1alpha1.Device{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-device", + Namespace: obj.GetNamespace(), + }, + Spec: v1alpha1.DeviceSpec{ + Endpoint: &v1alpha1.Endpoint{ + Address: *address, + SecretRef: &corev1.SecretReference{ + Name: "test-secret", + Namespace: obj.GetNamespace(), + }, + }, + }, + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: obj.GetNamespace(), + }, + Data: map[string][]byte{ + "username": []byte(*username), + "password": []byte(*password), + }, + Type: corev1.SecretTypeBasicAuth, + } + + obj.SetLabels(map[string]string{v1alpha1.DeviceLabel: device.Name}) + + c := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). + WithObjects(device, secret). + Build() + + ctx := clientutil.IntoContext(context.Background(), c, obj.GetNamespace()) + + fmt.Printf("\n=== Operation Status ===\n") + switch operation { + case "create": + switch resource := obj.(type) { + case *v1alpha1.Interface: + err = prov.CreateInterface(ctx, resource) + default: + fmt.Printf("Loaded resource of unknown type: %T\n", resource) + } + case "delete": + switch resource := obj.(type) { + case *v1alpha1.Interface: + err = prov.DeleteInterface(ctx, resource) + default: + fmt.Printf("Loaded resource of unknown type: %T\n", resource) + } + } + + if err != nil { + fmt.Fprintf(os.Stderr, "Error performing operation: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Provider tool completed successfully.\n") +}