Skip to content

WIP: POC #2102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
19 changes: 19 additions & 0 deletions hack/generate-testdata/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/*

# editor and IDE paraphernalia
.vscode
.idea/
.run/
*.swp
*.swo
*~
\#*\#
.\#*


15 changes: 15 additions & 0 deletions hack/generate-testdata/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
KUBEBUILDER_VERSION := 4.7.0
BIN_DIR := ./bin
OS := $(shell uname | tr A-Z a-z)
ARCH := $(shell uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
KUBEBUILDER_BIN := $(BIN_DIR)/kubebuilder

.PHONY: install-kubebuilder
install-kubebuilder:
mkdir -p $(BIN_DIR)
curl -fsSL -o $(KUBEBUILDER_BIN) https://github.com/kubernetes-sigs/kubebuilder/releases/download/v$(KUBEBUILDER_VERSION)/kubebuilder_$(OS)_$(ARCH)
chmod +x $(KUBEBUILDER_BIN)

.PHONY: generate-testdata
generate-testdata: install-kubebuilder
go run ./internal
9 changes: 9 additions & 0 deletions hack/generate-testdata/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/operator-framework/operator-controller/hack/generate-testdata

go 1.24.3

require (
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/sys v0.34.0 // indirect
sigs.k8s.io/kubebuilder/v4 v4.7.0 // indirect
)
14 changes: 14 additions & 0 deletions hack/generate-testdata/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/kubebuilder/v4 v4.7.0 h1:qeuV2Eeropc7e3AQi4CPAjUu2zm/wvWYu5EXYnNbCh4=
sigs.k8s.io/kubebuilder/v4 v4.7.0/go.mod h1:lOUlbL+p12PPhTDjSuPj6nurMi9q277CIbmlx397d/E=
38 changes: 38 additions & 0 deletions hack/generate-testdata/internal/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"github.com/operator-framework/operator-controller/hack/generate-testdata/internal/samples"
"github.com/operator-framework/operator-controller/hack/generate-testdata/internal/utils"
"log"
"os"
"path/filepath"
)

func main() {
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}

// TODO: Use cobra to allow pass the path to the testdata directory
samplesPath := filepath.Join(wd, "./../../testdata/operators")
log.Printf("Writing sample directories under: %s", samplesPath)

// Remove all previous sample output before regenerating
if err := os.RemoveAll(samplesPath); err != nil {
log.Fatalf("failed to clean testdata/operators: %v", err)
}

// Sample v1.0.0 — Basic Operator with one API and controller to deploy and manage a simple workload.
pathV1 := filepath.Join(samplesPath, "v1.0.0", "test-operator")
utils.ResetSampleDir(pathV1)
samples.BuildSampleV1(pathV1)
utils.BuildOLMBundleRegistryV1(pathV1)

// Sample v2.0.0 — Introduce a new API version with webhook conversion
// And enable NetworkPolicy
pathV2 := filepath.Join(samplesPath, "v2.0.0", "test-operator")
utils.ResetSampleDir(pathV2)
samples.BuildSampleV2(pathV2)
utils.BuildOLMBundleRegistryV1(pathV2)
}
57 changes: 57 additions & 0 deletions hack/generate-testdata/internal/samples/sample_v1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package samples

import (
"github.com/operator-framework/operator-controller/hack/generate-testdata/internal/utils"
"log"
"path/filepath"
pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
)

// BuildSampleV1 generates test operator v1.0.0
// ---------------------------------------------
// Create a basic Operator using Busybox image.
// This Operator provides an API to deploy and manage a simple workload.
// It is built using kubebuilder with the deploy-image plugin.
// Useful for testing OLM with a minimal Operator.
//
// Commands:
// kubebuilder init
// kubebuilder create api --group example.com --version v1alpha1 --kind Busybox --image=busybox:1.36.1 --plugins="deploy-image/v1-alpha"
func BuildSampleV1(samplesPath string) {
generateBasicOperator(samplesPath)
enableNetworkPolicies(samplesPath)
utils.RunMake(samplesPath, "generate", "manifests", "fmt", "vet")
}

func generateBasicOperator(path string) {
utils.RunKubebuilderCommand(path,
"init",
"--domain", "olmv1.com",
"--owner", "OLMv1 operator-framework",
)

// We use the deploy-image plugin because it scaffolds the full API and controller
// logic for deploying a container image. This is especially useful for quick-start
// Operator development, as it removes the need to manually implement reconcile logic.
//
// For more details, see:
// https://book.kubebuilder.io/plugins/available/deploy-image-plugin-v1-alpha
utils.RunKubebuilderCommand(path,
"create", "api",
"--group", "example",
"--version", "v1",
"--kind", "Busybox",
"--image", "busybox:1.36.1",
"--plugins", "deploy-image/v1-alpha",
)
}

func enableNetworkPolicies(path string) {
err := pluginutil.UncommentCode(
filepath.Join(path, "config", "default", "kustomization.yaml"),
"#- ../network-policy",
"#")
if err != nil {
log.Fatalf("Failed to enable network policies in %s: %v", path, err)
}
}
105 changes: 105 additions & 0 deletions hack/generate-testdata/internal/samples/sample_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package samples

import (
"fmt"
"github.com/operator-framework/operator-controller/hack/generate-testdata/internal/utils"
"path/filepath"
pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
)

// BuildSampleV2 generate sample v2.0.0 with breaking changes
// ---------------------------------------------
// This version introduces a new API version with webhook conversion.
// on top of v1.0.0 version.
func BuildSampleV2(samplesPath string) {
BuildSampleV1(samplesPath)

// Create v2 API version without controller
utils.RunKubebuilderCommand(samplesPath,
"create", "api",
"--group", "example",
"--version", "v2",
"--kind", "Busybox",
"--resource",
"--controller=false",
)

// Create conversion webhook for Busybox v1 -> v2
// Create webhook with defaulting and validation
utils.RunKubebuilderCommand(samplesPath,
"create", "webhook",
"--group", "example",
"--version", "v1",
"--kind", "Busybox",
"--conversion",
"--programmatic-validation",
"--defaulting",
"--spoke", "v2",
)

implementV2Type(samplesPath)
implementV2WebhookConversion(samplesPath)
implementValidationDefaultWebhookV1(samplesPath)

utils.RunMake(samplesPath, "generate", "manifests", "fmt", "vet")
}

func implementV2Type(path string) {
fmt.Println("Adding `Replicas` field to Busybox v2 spec")
v2TypesPath := filepath.Join(path, "api", "v2", "busybox_types.go")
if err := pluginutil.ReplaceInFile(
v2TypesPath,
"Foo *string `json:\"foo,omitempty\"`",
"Replicas *int32 `json:\"replicas,omitempty\"` // Number of replicas",
); err != nil {
panic(fmt.Sprintf("failed to insert replicas field in v2 BusyboxSpec: %v", err))
}
}

func implementV2WebhookConversion(path string) {
fmt.Println("Implementing conversion logic for v2 <-> v1")
conversionPath := filepath.Join(path, "api", "v2", "busybox_conversion.go")
if err := pluginutil.UncommentCode(
conversionPath,
"// dst.Spec.Size = src.Spec.Replicas",
"//",
); err != nil {
panic(fmt.Sprintf("failed to implement v2->v1 conversion: %v", err))
}

if err := pluginutil.UncommentCode(
conversionPath,
"// dst.Spec.Replicas = src.Spec.Size",
"//",
); err != nil {
panic(fmt.Sprintf("failed to implement v1->v2 conversion: %v", err))
}
}

// implementValidationDefaultWebhookV1 injects validation and defaulting logic into the Busybox v1 webhook.
func implementValidationDefaultWebhookV1(samplesPath string) {
webhookPath := filepath.Join(samplesPath, "internal", "webhook", "v1", "busybox_webhook.go")

fmt.Println("Injecting validation logic into Busybox v1 webhook")
// ValidateCreate logic
if err := pluginutil.ReplaceInFile(
webhookPath,
"// TODO(user): fill in your validation logic upon object creation.",
`if busybox.Spec.Size != nil && *busybox.Spec.Size < 0 {
return nil, fmt.Errorf("spec.size must be >= 0")
}`,
); err != nil {
panic(fmt.Sprintf("failed to apply ValidateCreate logic: %v", err))
}

// ValidateUpdate logic
if err := pluginutil.ReplaceInFile(
webhookPath,
"// TODO(user): fill in your validation logic upon object update.",
`if busybox.Spec.Size != nil && *busybox.Spec.Size < 0 {
return nil, fmt.Errorf("spec.size must be >= 0")
}`,
); err != nil {
panic(fmt.Sprintf("failed to apply ValidateUpdate logic: %v", err))
}
}
78 changes: 78 additions & 0 deletions hack/generate-testdata/internal/utils/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package utils

import (
"log"
"os"

"os/exec"
"path/filepath"
)

// RunKubebuilderCommand run command with kubebuilder binary
func RunKubebuilderCommand(dir string, args ...string) {
kbPath, err := filepath.Abs(filepath.Join("bin", "kubebuilder"))
if err != nil {
log.Fatalf("Failed to resolve absolute path to kubebuilder: %v", err)
}
cmd := exec.Command(kbPath, args...)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
log.Printf("Running command: %s %v", kbPath, args)
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}

func RunMake(dir string, targets ...string) {
for _, target := range targets {
cmd := exec.Command("make", target)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
log.Printf("Running make %s in %s", target, dir)
if err := cmd.Run(); err != nil {
log.Fatalf("make %s failed: %v", target, err)
}
}
}

// ResetSampleDir will create clean dir
func ResetSampleDir(path string) {
if err := os.RemoveAll(path); err != nil {
log.Fatalf("Failed to remove old sample dir: %v", err)
}
if err := os.MkdirAll(path, 0755); err != nil {
log.Fatalf("Failed to create sample dir: %v", err)
}
}

// BuildOLMBundleRegistryV1 will build OLM bundle registry v1
// This is a temporary approach until replaced by KPM-based tooling.
func BuildOLMBundleRegistryV1(path string) {
// TODO: Implement OLM bundle registry v1 build logic
// The same pain that we will have to deal with it here
// is the pain that our users, Content Authors have today,
// PS: SDK does not work well at all. What SDK does is not
// addressing the need and Content Authors need to create
// scripts to build OLM bundles or change things manually
// which is error-prone.
// It tried to use the SDK as our users do (https://kubernetes.slack.com/archives/C0181L6JYQ2/p1748738124826539), but it is not
// and still a lot of manual work.
}

// runOperatorSDK run command with sdk binary
func runOperatorSDK(dir string, args ...string) {
kbPath, err := filepath.Abs(filepath.Join("bin", "operator-sdk"))
if err != nil {
log.Fatalf("Failed to resolve absolute path to kubebuilder: %v", err)
}
cmd := exec.Command(kbPath, args...)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
log.Printf("Running command: %s %v", kbPath, args)
if err := cmd.Run(); err != nil {
log.Fatalf("Command failed: %v", err)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "Kubebuilder DevContainer",
"image": "golang:1.24",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/git:1": {}
},

"runArgs": ["--network=host"],

"customizations": {
"vscode": {
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
"extensions": [
"ms-kubernetes-tools.vscode-kubernetes-tools",
"ms-azuretools.vscode-docker"
]
}
},

"onCreateCommand": "bash .devcontainer/post-install.sh"
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
set -x

curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
chmod +x ./kind
mv ./kind /usr/local/bin/kind

curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/linux/amd64
chmod +x kubebuilder
mv kubebuilder /usr/local/bin/

KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
curl -LO "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/kubectl

docker network create -d=bridge --subnet=172.19.0.0/24 kind

kind version
kubebuilder version
docker --version
go version
kubectl version --client
3 changes: 3 additions & 0 deletions testdata/operators/v1.0.0/test-operator/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
# Ignore build and test binaries.
bin/
Loading
Loading