Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1e9dac5
feat(cli): add ocf command skeleton with version subcommand
sourcehawk Aug 2, 2026
338fb71
feat(scaffold): add wrapper option validation and derivation
sourcehawk Aug 2, 2026
7ddc994
feat(scaffold): render wrapper packages from embedded templates
sourcehawk Aug 2, 2026
e6caa41
feat(scaffold): write generated wrapper packages to disk
sourcehawk Aug 2, 2026
40b8cb9
feat(cli): add ocf scaffold wrapper command
sourcehawk Aug 2, 2026
c66d487
fix(cli): make scaffold wrapper's go test hint copy-pasteable for abs…
sourcehawk Aug 2, 2026
5bf22ae
test(scaffold): gate templates on scaffolded packages compiling and p…
sourcehawk Aug 2, 2026
ce45a2d
fix(scaffold): gate positively asserts tests ran, not just exit code
sourcehawk Aug 2, 2026
7e6ab7b
docs: document the ocf scaffolding CLI
sourcehawk Aug 2, 2026
93329da
docs: document version and alias derivation failure modes in cli.md
sourcehawk Aug 2, 2026
9045f52
fix(scaffold): validate --group and --version and quote generated ide…
sourcehawk Aug 2, 2026
75c7bf7
chore(lint): lint the scaffold-tagged gate file and bound its go invo…
sourcehawk Aug 2, 2026
c034dba
refactor(scaffold): drive the Resource interface list from VariantSpec
sourcehawk Aug 2, 2026
747f902
fix(scaffold): write generated sources with normal file permissions
sourcehawk Aug 2, 2026
a0bfacf
refactor(cli): rename displayDir and drop the wrapper's dead SilenceU…
sourcehawk Aug 2, 2026
354c364
fix(scaffold): validate the --type import path and quote generated im…
sourcehawk Aug 2, 2026
9cf2713
test(scaffold): fail the gate on corrupted go test -json output
sourcehawk Aug 2, 2026
6cfd4f0
fix(scaffold): variant-accurate handler GoDoc and cache-free gate runs
sourcehawk Aug 2, 2026
483d8bb
Merge remote-tracking branch 'origin/main' into worktree-ocf-scaffold…
sourcehawk Aug 2, 2026
4ee6107
chore: mark generated doc copies as generated files
sourcehawk Aug 2, 2026
bb31b23
docs(plugin): point the wrapper skill and command at the ocf CLI
sourcehawk Aug 2, 2026
d4cdb50
refactor(scaffold): extract the repeated ConvergingStatus method name
sourcehawk Aug 2, 2026
6f3f9a4
fix(component): use reflect.Pointer in the data cell nil check
sourcehawk Aug 2, 2026
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
3 changes: 3 additions & 0 deletions .ai/base.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Understand the intended design first:
- `docs/primitives.md` — primitive categories, field application, mutation system, editors, selectors
- `docs/primitives/*.md` — primitive implementations
- `docs/custom-resource.md` — implementing custom resource wrappers using `pkg/generic`
- `docs/cli.md` — the `ocf` scaffolding CLI, its flags, and what the generated code contains
- `docs/guidelines.md` — best practices for structuring operators (desired state, one component per condition, etc.)
- `docs/compatibility.md` — supported version combinations and compatibility policy

Expand All @@ -38,6 +39,7 @@ Verify the real API before using or documenting it. Key packages:
plus the per-kind `LiftMutation` adapters
- `pkg/generic/` — generic building blocks for custom resource wrappers (reconciliation, mutation sequencing,
suspension, data extraction)
- `cmd/ocf/` and `internal/scaffold/` — the `ocf` CLI and the wrapper templates it renders
- `pkg/mutation/editors/` — available methods per editor type
- `pkg/mutation/selectors/` — available container selectors
- `pkg/feature/feature.go` — `NewVersionGate`, `Mutation[T]`
Expand Down Expand Up @@ -98,6 +100,7 @@ Update documentation in the **same response** as the code change — never leave
| Primitives, field application, editors, selectors | `docs/primitives.md` |
| Primitive implementations | `docs/primitives/*.md` |
| Generic building blocks, custom resource wrappers | `docs/custom-resource.md` |
| Wrapper templates, CLI flags | `docs/cli.md` |
| Operator structuring patterns, best practices | `docs/guidelines.md` |
| Any `pkg/` export visible in the quick start | `README.md` |
| Examples | `examples/*/README.md` |
Expand Down
10 changes: 10 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Generated files. These are collapsed by default in pull request diffs and
# excluded from language statistics. Never edit them directly: change the source
# and re-run the target that produces them.

# Verbatim copies of docs/, produced by `make sync-plugin`.
plugin/skills/*/references/** linguist-generated=true

# Produced by `make ai-instructions` from .ai/base.md and .ai/review.md.
.github/copilot-instructions.md linguist-generated=true
.github/copilot-review-guidelines.md linguist-generated=true
3 changes: 3 additions & 0 deletions .github/copilot-instructions.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ jobs:
- name: Running Tests
run: |
go mod tidy
make test
make test

- name: Running Scaffold Gate
run: make test-scaffold
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ version: "2"

run:
timeout: 5m
build-tags:
- scaffold

linters:
default: none
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: all
all: fmt lint test test-examples build-examples
all: fmt lint test test-scaffold test-examples build-examples

##@ General

Expand Down Expand Up @@ -147,6 +147,10 @@ lint-go:
test: setup-envtest
go test -v $(shell go list ./... | grep -v /examples/) -coverprofile cover.out

.PHONY: test-scaffold
test-scaffold: ## Scaffold every wrapper variant into a temp module and run its tests.
go test -tags scaffold -count=1 -run TestScaffoldedWrappers ./internal/scaffold/...

.PHONY: build-examples
build-examples: ## Build all example binaries.
go build ./examples/...
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ go get github.com/sourcehawk/operator-component-framework

Requires Go 1.25.6+ and [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) v0.22 or later.

## Scaffolding

Wrapping a CRD the built-in primitives do not cover is mechanical. The `ocf` CLI generates the whole wrapper package,
compiling and tested, from one command:

```bash
go install github.com/sourcehawk/operator-component-framework/cmd/ocf@latest

ocf scaffold wrapper \
--type github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1.Certificate \
--variant integration \
--group cert-manager.io
```

See the [CLI guide](https://sourcehawk.github.io/operator-component-framework/cli/) for the full flag set and what to
replace in the generated code.

## Documentation

Full documentation, including a step-by-step tutorial, is at
Expand All @@ -94,6 +111,7 @@ Full documentation, including a step-by-step tutorial, is at
| [Component](https://sourcehawk.github.io/operator-component-framework/component/) | Lifecycle, status model, grace periods, suspension, guards |
| [Primitives](https://sourcehawk.github.io/operator-component-framework/primitives/) | Typed wrappers, the mutation system, editors, feature gating |
| [Custom Resources](https://sourcehawk.github.io/operator-component-framework/custom-resource/) | Wrap your own CRDs with `pkg/generic` |
| [CLI](https://sourcehawk.github.io/operator-component-framework/cli/) | Scaffold wrapper packages with `ocf scaffold wrapper` |
| [Guidelines](https://sourcehawk.github.io/operator-component-framework/guidelines/) | Patterns for structuring operators well |
| [Testing](https://sourcehawk.github.io/operator-component-framework/testing/) | Golden snapshots and version-matrix coverage |
| [Compatibility](https://sourcehawk.github.io/operator-component-framework/compatibility/) | Supported Kubernetes and controller-runtime versions |
Expand Down
217 changes: 217 additions & 0 deletions cmd/ocf/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package main

import (
"bytes"
"os"
"path/filepath"
"runtime/debug"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// runCommand executes the root command with args, capturing stdout and stderr.
func runCommand(t *testing.T, args ...string) (string, error) {
t.Helper()

root := newRootCommand()
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs(args)

err := root.Execute()

return out.String(), err
}

func TestRootCommandListsSubcommands(t *testing.T) {
t.Parallel()

out, err := runCommand(t, "--help")
require.NoError(t, err)
assert.Contains(t, out, "scaffold")
assert.Contains(t, out, "version")
}

func TestVersionCommandPrintsVersion(t *testing.T) {
t.Parallel()

out, err := runCommand(t, "version")
require.NoError(t, err)
assert.NotEmpty(t, out)
}

func TestScaffoldWrapperGeneratesPackage(t *testing.T) {
t.Parallel()

dir := filepath.Join(t.TempDir(), "certificate")

out, err := runCommand(t,
"scaffold", "wrapper",
"--type", "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1.Certificate",
"--variant", "integration",
"--group", "cert-manager.io",
"--out", dir,
)
require.NoError(t, err)

for _, name := range []string{"builder.go", "builder_test.go", "mutator.go", "resource.go"} {
assert.FileExists(t, filepath.Join(dir, name))
}

assert.Contains(t, out, dir)
assert.Contains(t, out, "go mod tidy")
assert.Contains(t, out, "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1")

// dir is absolute (rooted at t.TempDir()), so the printed go test invocation
// must use it as-is, not glued onto a "./" prefix.
assert.Contains(t, out, " 2. Run go test "+dir+"/... to verify the generated package.\n")
}

func TestScaffoldWrapperDefaultsOutToPackageDirectory(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)

out, err := runCommand(t,
"scaffold", "wrapper",
"--type", "k8s.io/api/core/v1.ConfigMap",
"--variant", "static",
"--group", "",
)
require.NoError(t, err)
assert.FileExists(t, filepath.Join(dir, "configmap", "builder.go"))

// The default output directory is relative, so the printed go test
// invocation must be a copy-pasteable relative path, prefixed with "./".
assert.Contains(t, out, " 2. Run go test ./configmap/... to verify the generated package.\n")
}

func TestScaffoldWrapperRefusesNonEmptyDirectoryWithoutForce(t *testing.T) {
t.Parallel()

dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "keep.go"), []byte("package keep\n"), 0o644))

_, err := runCommand(t,
"scaffold", "wrapper",
"--type", "k8s.io/api/core/v1.ConfigMap",
"--variant", "static",
"--group", "",
"--out", dir,
)
require.Error(t, err)
assert.Contains(t, err.Error(), "is not empty")
}

func TestScaffoldWrapperForceWritesIntoNonEmptyDirectory(t *testing.T) {
t.Parallel()

dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "keep.go"), []byte("package keep\n"), 0o644))

_, err := runCommand(t,
"scaffold", "wrapper",
"--type", "k8s.io/api/core/v1.ConfigMap",
"--variant", "static",
"--group", "",
"--out", dir,
"--force",
)
require.NoError(t, err)
assert.FileExists(t, filepath.Join(dir, "builder.go"))
assert.FileExists(t, filepath.Join(dir, "keep.go"))
}

func TestScaffoldWrapperFlagErrors(t *testing.T) {
t.Parallel()

tests := []struct {
name string
args []string
expectedErr string
}{
{
name: "missing type",
args: []string{"scaffold", "wrapper", "--variant", "static", "--group", ""},
expectedErr: "--type is required",
},
{
name: "missing variant",
args: []string{"scaffold", "wrapper", "--type", "k8s.io/api/core/v1.ConfigMap", "--group", ""},
expectedErr: "--variant is required",
},
{
name: "unknown variant",
args: []string{
"scaffold", "wrapper",
"--type", "k8s.io/api/core/v1.ConfigMap", "--variant", "daemon", "--group", "",
},
expectedErr: "--variant must be one of static, workload, task, integration",
},
{
name: "group not provided",
args: []string{"scaffold", "wrapper", "--type", "k8s.io/api/core/v1.ConfigMap", "--variant", "static"},
expectedErr: "--group is required",
},
{
name: "version not derivable",
args: []string{
"scaffold", "wrapper",
"--type", "example.io/apis/messaging.Queue", "--variant", "static", "--group", "messaging.example.io",
},
expectedErr: "--version is required",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

args := make([]string, 0, len(tt.args)+2)
args = append(args, tt.args...)
args = append(args, "--out", filepath.Join(t.TempDir(), "pkg"))
_, err := runCommand(t, args...)
require.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedErr)
})
}
}

func TestVersionFrom(t *testing.T) {
t.Parallel()

tests := []struct {
name string
info *debug.BuildInfo
ok bool
expected string
}{
{
name: "build info unavailable",
info: nil,
ok: false,
expected: "unknown",
},
{
name: "empty main version",
info: &debug.BuildInfo{Main: debug.Module{Version: ""}},
ok: true,
expected: "unknown",
},
{
name: "tagged version",
info: &debug.BuildInfo{Main: debug.Module{Version: "v1.2.3"}},
ok: true,
expected: "v1.2.3",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expected, versionFrom(tt.info, tt.ok))
})
}
}
10 changes: 10 additions & 0 deletions cmd/ocf/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Command ocf generates code for operators built on the operator component framework.
package main

import "os"

func main() {
if err := newRootCommand().Execute(); err != nil {
os.Exit(1)
}
}
19 changes: 19 additions & 0 deletions cmd/ocf/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package main

import "github.com/spf13/cobra"

// newRootCommand builds the ocf command tree.
func newRootCommand() *cobra.Command {
root := &cobra.Command{
Use: "ocf",
Short: "Code generation for the operator component framework",
Long: "ocf generates code for operators built on the operator component framework.\n\n" +
"Templates are embedded in the binary, so generated code always matches the\n" +
"framework version this CLI was built from.",
SilenceUsage: true,
}

root.AddCommand(newScaffoldCommand(), newVersionCommand())

return root
}
Loading
Loading