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") +}