Skip to content

Commit 6b0fcca

Browse files
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.
1 parent b1495bc commit 6b0fcca

File tree

9 files changed

+265
-15
lines changed

9 files changed

+265
-15
lines changed

.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ indent_size = 2
1212

1313
[{Makefile,go.mod,go.sum,*.go}]
1414
indent_style = tab
15+
indent_size = unset
1516

1617
[*.md]
1718
trim_trailing_whitespace = false

.github/workflows/checks.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
uses: actions/setup-go@v5
3030
with:
3131
check-latest: true
32-
go-version: 1.24.4
32+
go-version: 1.24.5
3333
- name: Run prepare make target
3434
run: make generate
3535
- name: Run golangci-lint

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
uses: actions/setup-go@v5
3333
with:
3434
check-latest: true
35-
go-version: 1.24.4
35+
go-version: 1.24.5
3636
- name: Run prepare make target
3737
run: make generate
3838
- name: Build all binaries
@@ -49,7 +49,7 @@ jobs:
4949
uses: actions/setup-go@v5
5050
with:
5151
check-latest: true
52-
go-version: 1.24.4
52+
go-version: 1.24.5
5353
- name: Run prepare make target
5454
run: make generate
5555
- name: Run tests and generate coverage report

.github/workflows/goreleaser.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
uses: actions/setup-go@v5
2828
with:
2929
check-latest: true
30-
go-version: 1.24.4
30+
go-version: 1.24.5
3131
- name: Run prepare make target
3232
run: make generate
3333
- name: Generate release info

.goreleaser.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ archives:
77
- name_template: '{{ .ProjectName }}-{{ replace .Version "v" "" }}-{{ .Os }}-{{ .Arch }}'
88
format_overrides:
99
- goos: windows
10-
format: zip
10+
formats: [ zip ]
1111
files:
1212
- CHANGELOG.md
1313
- LICENSE

.license-scan-overrides.jsonl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
{"name": "github.com/chzyer/logex", "licenceType": "MIT"}
22
{"name": "github.com/hashicorp/vault/api/auth/approle", "licenceType": "MPL-2.0"}
33
{"name": "github.com/jpillora/longestcommon", "licenceType": "MIT"}
4+
{"name": "github.com/logrusorgru/aurora", "licenceType": "Unlicense"}
45
{"name": "github.com/mattn/go-localereader", "licenceType": "MIT"}
56
{"name": "github.com/miekg/dns", "licenceType": "BSD-3-Clause"}
7+
{"name": "github.com/pashagolub/pgxmock/v4", "licenceType": "BSD-3-Clause"}
68
{"name": "github.com/spdx/tools-golang", "licenceTextOverrideFile": "vendor/github.com/spdx/tools-golang/LICENSE.code"}
79
{"name": "github.com/xeipuuv/gojsonpointer", "licenceType": "Apache-2.0"}
810
{"name": "github.com/xeipuuv/gojsonreference", "licenceType": "Apache-2.0"}

Makefile

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ charts: FORCE generate
126126
@kubebuilder edit --plugins=helm/v1-alpha
127127
@rm -rf charts/network-operator && mv dist/chart charts/network-operator && rm -rf dist
128128

129+
netop-provider:
130+
@printf "\e[1;36m>> go build -o build/netop-provider ./hack/provider\e[0m\n"
131+
@go build -o build/netop-provider ./hack/provider
132+
@printf "\e[1;36m>> ./build/netop-provider --help\e[0m\n"
133+
@./build/netop-provider --help
134+
129135
install-goimports: FORCE
130136
@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
131137

@@ -136,18 +142,18 @@ install-modernize: FORCE
136142
@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
137143

138144
install-shellcheck: FORCE
139-
@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
140-
141-
install-ginkgo: FORCE
142-
@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
145+
@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
143146

144147
install-go-licence-detector: FORCE
145148
@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
146149

147150
install-addlicense: FORCE
148151
@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
149152

150-
prepare-static-check: FORCE install-golangci-lint install-modernize install-shellcheck install-ginkgo install-go-licence-detector install-addlicense
153+
install-reuse: FORCE
154+
@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
155+
156+
prepare-static-check: FORCE install-golangci-lint install-modernize install-shellcheck install-go-licence-detector install-addlicense install-reuse
151157

152158
install-controller-gen: FORCE
153159
@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
215221
@printf "\e[1;36m>> shellcheck\e[0m\n"
216222
@find . -type f \( -name '*.bash' -o -name '*.ksh' -o -name '*.zsh' -o -name '*.sh' -o -name '*.shlib' \) -exec shellcheck {} +
217223

218-
build/cover.out: FORCE install-ginkgo generate install-setup-envtest | build
224+
build/cover.out: FORCE generate install-setup-envtest | build
219225
@printf "\e[1;36m>> Running tests\e[0m\n"
220-
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)
226+
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)
221227
@mv build/coverprofile.out build/cover.out
222228

223229
build/cover.html: build/cover.out
@@ -228,7 +234,7 @@ check-addlicense: FORCE install-addlicense
228234
@printf "\e[1;36m>> addlicense --check\e[0m\n"
229235
@addlicense --check -- $(patsubst $(shell awk '$$1 == "module" {print $$2}' go.mod)%,.%/*.go,$(shell go list ./...))
230236

231-
check-reuse: FORCE
237+
check-reuse: FORCE install-reuse
232238
@printf "\e[1;36m>> reuse lint\e[0m\n"
233239
@if ! reuse lint -q; then reuse lint; fi
234240

@@ -246,7 +252,7 @@ tidy-deps: FORCE
246252
go mod tidy
247253
go mod verify
248254

249-
license-headers: FORCE install-addlicense
255+
license-headers: FORCE install-addlicense install-reuse
250256
@printf "\e[1;36m>> addlicense (for license headers on source code files)\e[0m\n"
251257
@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: +'"'"' {}; '
252258
@printf "\e[1;36m>> reuse annotate (for license headers on other files)\e[0m\n"
@@ -299,9 +305,9 @@ help: FORCE
299305
@printf " \e[36minstall-golangci-lint\e[0m Install golangci-lint required by run-golangci-lint/static-check\n"
300306
@printf " \e[36minstall-modernize\e[0m Install modernize required by run-modernize/static-check\n"
301307
@printf " \e[36minstall-shellcheck\e[0m Install shellcheck required by run-shellcheck/static-check\n"
302-
@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"
303308
@printf " \e[36minstall-go-licence-detector\e[0m Install-go-licence-detector required by check-dependency-licenses/static-check\n"
304309
@printf " \e[36minstall-addlicense\e[0m Install addlicense required by check-license-headers/license-headers/static-check\n"
310+
@printf " \e[36minstall-reuse\e[0m Install reuse required by license-headers/check-reuse\n"
305311
@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"
306312
@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"
307313
@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"

Makefile.maker.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,9 @@ verbatim: |
164164
@printf "\e[1;36m>> kubebuilder edit --plugins=helm/v1-alpha\e[0m\n"
165165
@kubebuilder edit --plugins=helm/v1-alpha
166166
@rm -rf charts/network-operator && mv dist/chart charts/network-operator && rm -rf dist
167+
168+
netop-provider:
169+
@printf "\e[1;36m>> go build -o build/netop-provider ./hack/provider\e[0m\n"
170+
@go build -o build/netop-provider ./hack/provider
171+
@printf "\e[1;36m>> ./build/netop-provider --help\e[0m\n"
172+
@./build/netop-provider --help

hack/provider/main.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
package main
4+
5+
import (
6+
"context"
7+
"errors"
8+
"flag"
9+
"fmt"
10+
"os"
11+
12+
corev1 "k8s.io/api/core/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/runtime"
15+
"k8s.io/apimachinery/pkg/runtime/serializer"
16+
"k8s.io/client-go/kubernetes/scheme"
17+
"sigs.k8s.io/controller-runtime/pkg/client"
18+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
19+
"sigs.k8s.io/yaml"
20+
21+
// Import all supported provider implementations.
22+
_ "github.com/ironcore-dev/network-operator/internal/provider/openconfig"
23+
24+
"github.com/ironcore-dev/network-operator/api/v1alpha1"
25+
"github.com/ironcore-dev/network-operator/internal/clientutil"
26+
"github.com/ironcore-dev/network-operator/internal/provider"
27+
)
28+
29+
var (
30+
address = flag.String("address", "", "API endpoint address (required)")
31+
username = flag.String("username", "", "Username for authentication (required)")
32+
password = flag.String("password", "", "Password for authentication (required)")
33+
file = flag.String("file", "", "Path to Kubernetes resource manifest file (required)")
34+
providerName = flag.String("provider", "openconfig", "Provider implementation to use")
35+
)
36+
37+
func usage() {
38+
fmt.Fprintf(os.Stderr, "Usage: %s [flags] <create|delete>\n\n", os.Args[0])
39+
fmt.Fprintf(os.Stderr, "A debug tool for testing provider implementations.\n\n")
40+
fmt.Fprintf(os.Stderr, "Arguments:\n")
41+
fmt.Fprintf(os.Stderr, " create|delete Operation to perform on the resource\n\n")
42+
fmt.Fprintf(os.Stderr, "Flags:\n")
43+
flag.PrintDefaults()
44+
fmt.Fprintf(os.Stderr, "\nExample:\n")
45+
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])
46+
}
47+
48+
func validateFlags() error {
49+
if *address == "" {
50+
return errors.New("address flag is required")
51+
}
52+
if *username == "" {
53+
return errors.New("username flag is required")
54+
}
55+
if *password == "" {
56+
return errors.New("password flag is required")
57+
}
58+
if *file == "" {
59+
return errors.New("file flag is required")
60+
}
61+
return nil
62+
}
63+
64+
func validatePositionalArgs() (string, error) {
65+
if len(flag.Args()) != 1 {
66+
return "", errors.New("exactly one positional argument (create|delete) is required")
67+
}
68+
69+
operation := flag.Args()[0]
70+
if operation != "create" && operation != "delete" {
71+
return "", fmt.Errorf("positional argument must be either 'create' or 'delete', got: %s", operation)
72+
}
73+
74+
return operation, nil
75+
}
76+
77+
func loadAndUnmarshalResource(filePath string) (runtime.Object, error) {
78+
if _, err := os.Stat(filePath); os.IsNotExist(err) {
79+
return nil, fmt.Errorf("file does not exist: %s", filePath)
80+
}
81+
82+
data, err := os.ReadFile(filePath)
83+
if err != nil {
84+
return nil, fmt.Errorf("failed to read file %s: %w", filePath, err)
85+
}
86+
87+
if err = v1alpha1.AddToScheme(scheme.Scheme); err != nil {
88+
return nil, fmt.Errorf("failed to add scheme: %w", err)
89+
}
90+
91+
decoder := serializer.NewCodecFactory(scheme.Scheme).UniversalDeserializer()
92+
93+
json, err := yaml.YAMLToJSON(data)
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to convert YAML to JSON: %w", err)
96+
}
97+
98+
obj, _, err := decoder.Decode(json, nil, nil)
99+
if err != nil {
100+
return nil, fmt.Errorf("failed to decode resource: %w", err)
101+
}
102+
103+
return obj, nil
104+
}
105+
106+
func printResourceInfo(obj runtime.Object) {
107+
switch resource := obj.(type) {
108+
case *v1alpha1.Interface:
109+
fmt.Printf("Loaded Interface: %s\n", resource.Name)
110+
fmt.Printf(" Namespace: %s\n", resource.Namespace)
111+
fmt.Printf(" Interface Name: %s\n", resource.Spec.Name)
112+
fmt.Printf(" Admin State: %s\n", resource.Spec.AdminState)
113+
default:
114+
fmt.Printf("Loaded resource of unknown type: %T\n", resource)
115+
}
116+
}
117+
118+
func main() {
119+
flag.Usage = usage
120+
121+
for _, arg := range os.Args[1:] {
122+
if arg == "-h" || arg == "--help" {
123+
flag.Usage()
124+
os.Exit(0)
125+
}
126+
}
127+
128+
flag.Parse()
129+
130+
if err := validateFlags(); err != nil {
131+
fmt.Fprintf(os.Stderr, "Error: %v\n\n", err)
132+
flag.Usage()
133+
os.Exit(1)
134+
}
135+
136+
operation, err := validatePositionalArgs()
137+
if err != nil {
138+
fmt.Fprintf(os.Stderr, "Error: %v\n\n", err)
139+
flag.Usage()
140+
os.Exit(1)
141+
}
142+
143+
resource, err := loadAndUnmarshalResource(*file)
144+
if err != nil {
145+
fmt.Fprintf(os.Stderr, "Error loading resource: %v\n", err)
146+
os.Exit(1)
147+
}
148+
149+
obj, ok := resource.(client.Object)
150+
if !ok {
151+
fmt.Fprintf(os.Stderr, "Error: resource is not a client.Object\n")
152+
os.Exit(1)
153+
}
154+
if obj.GetNamespace() == "" {
155+
obj.SetNamespace(metav1.NamespaceDefault)
156+
}
157+
158+
fmt.Printf("=== Debug Tool Configuration ===\n")
159+
fmt.Printf("Address: %s\n", *address)
160+
fmt.Printf("Username: %s\n", *username)
161+
fmt.Printf("Password: %s\n", "[REDACTED]")
162+
fmt.Printf("Resource File: %s\n", *file)
163+
fmt.Printf("Provider: %s\n", *providerName)
164+
fmt.Printf("Operation: %s\n", operation)
165+
fmt.Printf("\n=== Resource Information ===\n")
166+
printResourceInfo(resource)
167+
168+
prov, err := provider.Get(*providerName)
169+
if err != nil {
170+
fmt.Fprintf(os.Stderr, "Error getting provider: %v\n", err)
171+
os.Exit(1)
172+
}
173+
174+
device := &v1alpha1.Device{
175+
ObjectMeta: metav1.ObjectMeta{
176+
Name: "test-device",
177+
Namespace: obj.GetNamespace(),
178+
},
179+
Spec: v1alpha1.DeviceSpec{
180+
Endpoint: &v1alpha1.Endpoint{
181+
Address: *address,
182+
SecretRef: &corev1.SecretReference{
183+
Name: "test-secret",
184+
Namespace: obj.GetNamespace(),
185+
},
186+
},
187+
},
188+
}
189+
190+
secret := &corev1.Secret{
191+
ObjectMeta: metav1.ObjectMeta{
192+
Name: "test-secret",
193+
Namespace: obj.GetNamespace(),
194+
},
195+
Data: map[string][]byte{
196+
"username": []byte(*username),
197+
"password": []byte(*password),
198+
},
199+
Type: corev1.SecretTypeBasicAuth,
200+
}
201+
202+
obj.SetLabels(map[string]string{v1alpha1.DeviceLabel: device.Name})
203+
204+
c := fake.NewClientBuilder().
205+
WithScheme(scheme.Scheme).
206+
WithObjects(device, secret).
207+
Build()
208+
209+
ctx := clientutil.IntoContext(context.Background(), c, obj.GetNamespace())
210+
211+
fmt.Printf("\n=== Operation Status ===\n")
212+
switch operation {
213+
case "create":
214+
switch resource := obj.(type) {
215+
case *v1alpha1.Interface:
216+
err = prov.CreateInterface(ctx, resource)
217+
default:
218+
fmt.Printf("Loaded resource of unknown type: %T\n", resource)
219+
}
220+
case "delete":
221+
switch resource := obj.(type) {
222+
case *v1alpha1.Interface:
223+
err = prov.DeleteInterface(ctx, resource)
224+
default:
225+
fmt.Printf("Loaded resource of unknown type: %T\n", resource)
226+
}
227+
}
228+
229+
if err != nil {
230+
fmt.Fprintf(os.Stderr, "Error performing operation: %v\n", err)
231+
os.Exit(1)
232+
}
233+
234+
fmt.Printf("Provider tool completed successfully.\n")
235+
}

0 commit comments

Comments
 (0)