Skip to content

Commit 1105368

Browse files
Add netop-provider cli to directly invoke provider implementation (#20)
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 b5d4a50 commit 1105368

9 files changed

Lines changed: 265 additions & 15 deletions

File tree

.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)