Skip to content

Commit 111c910

Browse files
(chore): Add hack/generate-testdata to build testdata mocks and samples. Introduced logic to build Operators
1 parent 365831a commit 111c910

File tree

8 files changed

+335
-0
lines changed

8 files changed

+335
-0
lines changed

hack/generate-testdata/.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
bin/*
8+
9+
# editor and IDE paraphernalia
10+
.vscode
11+
.idea/
12+
.run/
13+
*.swp
14+
*.swo
15+
*~
16+
\#*\#
17+
.\#*
18+
19+

hack/generate-testdata/Makefile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
KUBEBUILDER_VERSION := 4.7.0
2+
BIN_DIR := ./bin
3+
OS := $(shell uname | tr A-Z a-z)
4+
ARCH := $(shell uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
5+
KUBEBUILDER_BIN := $(BIN_DIR)/kubebuilder
6+
7+
.PHONY: install-kubebuilder
8+
install-kubebuilder:
9+
mkdir -p $(BIN_DIR)
10+
curl -fsSL -o $(KUBEBUILDER_BIN) https://github.com/kubernetes-sigs/kubebuilder/releases/download/v$(KUBEBUILDER_VERSION)/kubebuilder_$(OS)_$(ARCH)
11+
chmod +x $(KUBEBUILDER_BIN)
12+
13+
.PHONY: generate-testdata
14+
generate-testdata: install-kubebuilder
15+
go run ./internal

hack/generate-testdata/go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/operator-framework/operator-controller/hack/generate-testdata
2+
3+
go 1.24.3
4+
5+
require (
6+
github.com/sirupsen/logrus v1.9.3 // indirect
7+
golang.org/x/sys v0.34.0 // indirect
8+
sigs.k8s.io/kubebuilder/v4 v4.7.0 // indirect
9+
)

hack/generate-testdata/go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
4+
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
5+
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
6+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
9+
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
10+
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
11+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
12+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
13+
sigs.k8s.io/kubebuilder/v4 v4.7.0 h1:qeuV2Eeropc7e3AQi4CPAjUu2zm/wvWYu5EXYnNbCh4=
14+
sigs.k8s.io/kubebuilder/v4 v4.7.0/go.mod h1:lOUlbL+p12PPhTDjSuPj6nurMi9q277CIbmlx397d/E=
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package main
2+
3+
import (
4+
"github.com/operator-framework/operator-controller/hack/generate-testdata/internal/samples"
5+
"github.com/operator-framework/operator-controller/hack/generate-testdata/internal/utils"
6+
"log"
7+
"os"
8+
"path/filepath"
9+
)
10+
11+
func main() {
12+
wd, err := os.Getwd()
13+
if err != nil {
14+
log.Fatal(err)
15+
}
16+
17+
// TODO: Use cobra to allow pass the path to the testdata directory
18+
samplesPath := filepath.Join(wd, "./../../testdata/operators")
19+
log.Printf("Writing sample directories under: %s", samplesPath)
20+
21+
// Remove all previous sample output before regenerating
22+
if err := os.RemoveAll(samplesPath); err != nil {
23+
log.Fatalf("failed to clean testdata/operators: %v", err)
24+
}
25+
26+
// Sample v1.0.0 — Basic Operator with one API and controller to deploy and manage a simple workload.
27+
pathV1 := filepath.Join(samplesPath, "v1.0.0", "test-operator")
28+
utils.ResetSampleDir(pathV1)
29+
samples.BuildSampleV1(pathV1)
30+
utils.BuildOLMBundleRegistryV1(pathV1)
31+
32+
// Sample v2.0.0 — Introduce a new API version with webhook conversion
33+
// And enable NetworkPolicy
34+
pathV2 := filepath.Join(samplesPath, "v2.0.0", "test-operator")
35+
utils.ResetSampleDir(pathV2)
36+
samples.BuildSampleV2(pathV2)
37+
utils.BuildOLMBundleRegistryV1(pathV2)
38+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package samples
2+
3+
import (
4+
"github.com/operator-framework/operator-controller/hack/generate-testdata/internal/utils"
5+
"log"
6+
"path/filepath"
7+
pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
8+
)
9+
10+
// BuildSampleV1 generates test operator v1.0.0
11+
// ---------------------------------------------
12+
// Create a basic Operator using Busybox image.
13+
// This Operator provides an API to deploy and manage a simple workload.
14+
// It is built using kubebuilder with the deploy-image plugin.
15+
// Useful for testing OLM with a minimal Operator.
16+
//
17+
// Commands:
18+
// kubebuilder init
19+
// kubebuilder create api --group example.com --version v1alpha1 --kind Busybox --image=busybox:1.36.1 --plugins="deploy-image/v1-alpha"
20+
func BuildSampleV1(samplesPath string) {
21+
generateBasicOperator(samplesPath)
22+
enableNetworkPolicies(samplesPath)
23+
utils.RunMake(samplesPath, "generate", "manifests", "fmt", "vet")
24+
}
25+
26+
func generateBasicOperator(path string) {
27+
utils.RunKubebuilderCommand(path,
28+
"init",
29+
"--domain", "olmv1.com",
30+
"--owner", "OLMv1 operator-framework",
31+
)
32+
33+
// We use the deploy-image plugin because it scaffolds the full API and controller
34+
// logic for deploying a container image. This is especially useful for quick-start
35+
// Operator development, as it removes the need to manually implement reconcile logic.
36+
//
37+
// For more details, see:
38+
// https://book.kubebuilder.io/plugins/available/deploy-image-plugin-v1-alpha
39+
utils.RunKubebuilderCommand(path,
40+
"create", "api",
41+
"--group", "example",
42+
"--version", "v1",
43+
"--kind", "Busybox",
44+
"--image", "busybox:1.36.1",
45+
"--plugins", "deploy-image/v1-alpha",
46+
)
47+
}
48+
49+
func enableNetworkPolicies(path string) {
50+
err := pluginutil.UncommentCode(
51+
filepath.Join(path, "config", "default", "kustomization.yaml"),
52+
"#- ../network-policy",
53+
"#")
54+
if err != nil {
55+
log.Fatalf("Failed to enable network policies in %s: %v", path, err)
56+
}
57+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package samples
2+
3+
import (
4+
"fmt"
5+
"github.com/operator-framework/operator-controller/hack/generate-testdata/internal/utils"
6+
"path/filepath"
7+
pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
8+
)
9+
10+
// BuildSampleV2 generate sample v2.0.0 with breaking changes
11+
// ---------------------------------------------
12+
// This version introduces a new API version with webhook conversion.
13+
// on top of v1.0.0 version.
14+
func BuildSampleV2(samplesPath string) {
15+
BuildSampleV1(samplesPath)
16+
17+
// Create v2 API version without controller
18+
utils.RunKubebuilderCommand(samplesPath,
19+
"create", "api",
20+
"--group", "example",
21+
"--version", "v2",
22+
"--kind", "Busybox",
23+
"--resource",
24+
"--controller=false",
25+
)
26+
27+
// Create conversion webhook for Busybox v1 -> v2
28+
// Create webhook with defaulting and validation
29+
utils.RunKubebuilderCommand(samplesPath,
30+
"create", "webhook",
31+
"--group", "example",
32+
"--version", "v1",
33+
"--kind", "Busybox",
34+
"--conversion",
35+
"--programmatic-validation",
36+
"--defaulting",
37+
"--spoke", "v2",
38+
)
39+
40+
implementV2Type(samplesPath)
41+
implementV2WebhookConversion(samplesPath)
42+
implementValidationDefaultWebhookV1(samplesPath)
43+
44+
utils.RunMake(samplesPath, "generate", "manifests", "fmt", "vet")
45+
}
46+
47+
func implementV2Type(path string) {
48+
fmt.Println("Adding `Replicas` field to Busybox v2 spec")
49+
v2TypesPath := filepath.Join(path, "api", "v2", "busybox_types.go")
50+
if err := pluginutil.ReplaceInFile(
51+
v2TypesPath,
52+
"Foo *string `json:\"foo,omitempty\"`",
53+
"Replicas *int32 `json:\"replicas,omitempty\"` // Number of replicas",
54+
); err != nil {
55+
panic(fmt.Sprintf("failed to insert replicas field in v2 BusyboxSpec: %v", err))
56+
}
57+
}
58+
59+
func implementV2WebhookConversion(path string) {
60+
fmt.Println("Implementing conversion logic for v2 <-> v1")
61+
conversionPath := filepath.Join(path, "api", "v2", "busybox_conversion.go")
62+
if err := pluginutil.UncommentCode(
63+
conversionPath,
64+
"// dst.Spec.Size = src.Spec.Replicas",
65+
"//",
66+
); err != nil {
67+
panic(fmt.Sprintf("failed to implement v2->v1 conversion: %v", err))
68+
}
69+
70+
if err := pluginutil.UncommentCode(
71+
conversionPath,
72+
"// dst.Spec.Replicas = src.Spec.Size",
73+
"//",
74+
); err != nil {
75+
panic(fmt.Sprintf("failed to implement v1->v2 conversion: %v", err))
76+
}
77+
}
78+
79+
// implementValidationDefaultWebhookV1 injects validation and defaulting logic into the Busybox v1 webhook.
80+
func implementValidationDefaultWebhookV1(samplesPath string) {
81+
webhookPath := filepath.Join(samplesPath, "internal", "webhook", "v1", "busybox_webhook.go")
82+
83+
fmt.Println("Injecting validation logic into Busybox v1 webhook")
84+
// ValidateCreate logic
85+
if err := pluginutil.ReplaceInFile(
86+
webhookPath,
87+
"// TODO(user): fill in your validation logic upon object creation.",
88+
`if busybox.Spec.Size != nil && *busybox.Spec.Size < 0 {
89+
return nil, fmt.Errorf("spec.size must be >= 0")
90+
}`,
91+
); err != nil {
92+
panic(fmt.Sprintf("failed to apply ValidateCreate logic: %v", err))
93+
}
94+
95+
// ValidateUpdate logic
96+
if err := pluginutil.ReplaceInFile(
97+
webhookPath,
98+
"// TODO(user): fill in your validation logic upon object update.",
99+
`if busybox.Spec.Size != nil && *busybox.Spec.Size < 0 {
100+
return nil, fmt.Errorf("spec.size must be >= 0")
101+
}`,
102+
); err != nil {
103+
panic(fmt.Sprintf("failed to apply ValidateUpdate logic: %v", err))
104+
}
105+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package utils
2+
3+
import (
4+
"log"
5+
"os"
6+
7+
"os/exec"
8+
"path/filepath"
9+
)
10+
11+
// RunKubebuilderCommand run command with kubebuilder binary
12+
func RunKubebuilderCommand(dir string, args ...string) {
13+
kbPath, err := filepath.Abs(filepath.Join("bin", "kubebuilder"))
14+
if err != nil {
15+
log.Fatalf("Failed to resolve absolute path to kubebuilder: %v", err)
16+
}
17+
cmd := exec.Command(kbPath, args...)
18+
cmd.Dir = dir
19+
cmd.Stdout = os.Stdout
20+
cmd.Stderr = os.Stderr
21+
log.Printf("Running command: %s %v", kbPath, args)
22+
if err := cmd.Run(); err != nil {
23+
log.Fatalf("Command failed: %v", err)
24+
}
25+
}
26+
27+
func RunMake(dir string, targets ...string) {
28+
for _, target := range targets {
29+
cmd := exec.Command("make", target)
30+
cmd.Dir = dir
31+
cmd.Stdout = os.Stdout
32+
cmd.Stderr = os.Stderr
33+
log.Printf("Running make %s in %s", target, dir)
34+
if err := cmd.Run(); err != nil {
35+
log.Fatalf("make %s failed: %v", target, err)
36+
}
37+
}
38+
}
39+
40+
// ResetSampleDir will create clean dir
41+
func ResetSampleDir(path string) {
42+
if err := os.RemoveAll(path); err != nil {
43+
log.Fatalf("Failed to remove old sample dir: %v", err)
44+
}
45+
if err := os.MkdirAll(path, 0755); err != nil {
46+
log.Fatalf("Failed to create sample dir: %v", err)
47+
}
48+
}
49+
50+
// BuildOLMBundleRegistryV1 will build OLM bundle registry v1
51+
// This is a temporary approach until replaced by KPM-based tooling.
52+
func BuildOLMBundleRegistryV1(path string) {
53+
// TODO: Implement OLM bundle registry v1 build logic
54+
// The same pain that we will have to deal with it here
55+
// is the pain that our users, Content Authors have today,
56+
// PS: SDK does not work well at all. What SDK does is not
57+
// addressing the need and Content Authors need to create
58+
// scripts to build OLM bundles or change things manually
59+
// which is error-prone.
60+
// It tried to use the SDK as our users do (https://kubernetes.slack.com/archives/C0181L6JYQ2/p1748738124826539), but it is not
61+
// and still a lot of manual work.
62+
}
63+
64+
// runOperatorSDK run command with sdk binary
65+
func runOperatorSDK(dir string, args ...string) {
66+
kbPath, err := filepath.Abs(filepath.Join("bin", "operator-sdk"))
67+
if err != nil {
68+
log.Fatalf("Failed to resolve absolute path to kubebuilder: %v", err)
69+
}
70+
cmd := exec.Command(kbPath, args...)
71+
cmd.Dir = dir
72+
cmd.Stdout = os.Stdout
73+
cmd.Stderr = os.Stderr
74+
log.Printf("Running command: %s %v", kbPath, args)
75+
if err := cmd.Run(); err != nil {
76+
log.Fatalf("Command failed: %v", err)
77+
}
78+
}

0 commit comments

Comments
 (0)