Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ indent_size = 2

[{Makefile,go.mod,go.sum,*.go}]
indent_style = tab
indent_size = unset

[*.md]
trim_trailing_whitespace = false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .license-scan-overrides.jsonl
Original file line number Diff line number Diff line change
@@ -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"}
Expand Down
26 changes: 16 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -136,18 +142,18 @@ 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

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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions Makefile.maker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
235 changes: 235 additions & 0 deletions hack/provider/main.go
Original file line number Diff line number Diff line change
@@ -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] <create|delete>\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")
}
Loading