Using the Scaffolder
This guide walks through bootstrapping a new component operator with the
Scaffolder. As a running example we will build an operator
that manages resources of type Gizmo in the API group tools.acme.io (version
v1alpha1).
Prerequisites
A Kubernetes cluster to deploy to later. If you do not have one, you can quickly spin up a local kind cluster:
kind create clusterThe
scaffoldbinary. Download the latest release from the GitHub Releases page and make sure it is on yourPATH. See the Scaffolder tool reference for the full list of options.
Generating the skeleton
First, create an empty working directory for the project:
mkdir gizmo-operator
Change into it:
cd gizmo-operator
Then run the scaffolder, pointing it at the current directory (.) as the output
target:
scaffold \
--group-name tools.acme.io \
--group-version v1alpha1 \
--kind Gizmo \
--operator-name gizmo-operator.acme.io \
--go-module acme.io/gizmo-operator \
--image ghcr.io/acme/gizmo-operator:latest \
.
This generates a complete, ready-to-build operator project in the current directory,
pre-wired to component-operator-runtime for managing Gizmo resources.
Understand the Makefile
A Makefile is generated. Run make help to see what it can do:
Usage:
make <target>
General
help Display this help
Development
manifests Generate CustomResourceDefinition objects
generate Generate required code pieces
generate-deepcopy Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations
generate-client Generate typed client
fmt Run go fmt against code
vet Run go vet against code
Testing
test Run tests
Build
build Build manager binary
run Run a controller from your host
docker-build Build docker image with the manager
docker-push Push docker image with the manager
docker-buildx Build and push docker image for the manager for cross-platform support
Build Dependencies
controller-gen Install controller-gen
setup-envtest Install setup-envtest
envtest Install envtest binaries
Run the Operator
Running make build compiles the operator into bin/manager.
Ensure that KUBECONFIG is set to the kubeconfig for your playground cluster. Then, install the CRD into your cluster:
kubectl apply -f crds
And start the operator
./bin/manager
It’s possible now to apply some Gizmo instance:
kubectl apply -f examples/sample.yaml
If you are using Visual Studio Code, you can use the generated ./vscode/launch.json to start the operator from vscode. Note that this requires to copy the kubeconfig into the ./tmp directory:
mkdir -p tmp
cp $KUBECONFIG tmp/kubeconfig
Developing the Operator
Now it’s time to breathe some life into the operator.
The first step is to replace the DummyGenerator in ./pkg/operator/operator.go
func (o *Operator) Setup(mgr ctrl.Manager) error {
// Replace this by a real resource generator (e.g. HelmGenerator or KustomizeGenerator, or your own one).
resourceGenerator, err := manifests.NewDummyGenerator()
if err != nil {
return fmt.Errorf("error initializing resource generator: %w", err)
}
if err := component.NewReconciler[*operatorv1alpha1.Gizmo](
o.options.Name,
resourceGenerator,
component.ReconcilerOptions{},
).SetupWithManager(mgr); err != nil {
return fmt.Errorf("unable to create controller: %w", err)
}
return nil
}
with your own implementation. There are various alternatives how to do this. For example …
Implement the Generator from Scratch
Let’s define a package ./internal/generator, and a file ./internal/generator/generator.go:
package generator
import (
"context"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/sap/component-operator-runtime/pkg/manifests"
"github.com/sap/component-operator-runtime/pkg/types"
operatorv1alpha1 "acme.io/gizmo-operator/api/v1alpha1"
)
type Generator struct{}
var _ manifests.Generator = &Generator{}
func New() (*Generator, error) {
return &Generator{}, nil
}
func (g *Generator) Generate(ctx context.Context, namespace string, name string, parameters types.Unstructurable) ([]client.Object, error) {
// the following cast is safe because of the way how Gizmo.GetSpec() is implemented
spec := parameters.(*operatorv1alpha1.GizmoSpec)
_ = spec
// define dependent objects here ...
return []client.Object{}, nil
}
And replace the manifests.NewDummyGenerator() invocation above by generator.New().
Now you have full control and can construct your dependent objects in whatever way you want.
Whenever types in ./api are changed, you should re-run
make generate manifests
and re-apply the CRDs to the cluster:
kubectl apply -f crds
Embed an Existing Helm Chart
Another common approach is to vendor an existing Helm Chart.
To do so, copy the extracted Helm Chart to ./pkg/operator/data/chart, and reference it from ./pkg/operator/operator.go as
//go:embed all:data
var data embed.FS
Then you can consume it in ./pkg/operator/operator.go using
resourceGenerator, err := helm.NewHelmGenerator(data, "data/chart", nil)
In this form, the spec of the reconciled Gizmo object would, after being converted to unstructured, be passed unchanged as values to the Helm chart. Often this does not fit to the needs. In such cases you could attach a ParameterTransformer:
parameterTransformer := // implement your own custom ParameterTransformer
resourceGenerator, err := helm.NewHelmGeneratorWithParameterTransformer(data, "data/chart", nil, parameterTransformer)
Similarly, if you want to mutate the output of the rendered Helm chart, an ObjectTransformer can be used:
helmGenerator, err := helm.NewHelmGenerator(data, "data/chart", nil)
if err != nil {
return fmt.Errorf("error initializing resource generator: %w", err)
}
// the file ./pkg/operator/data/parameter-transformer.yaml must be created, containing
// a go template taking the unstructured Gizmo spec as input, and returning the final helm values as yaml
parameterTransformer, err := manifests.NewTemplateParameterTransformer(data, "data/parameter-transformer.yaml")
if err != nil {
return fmt.Errorf("error initializing parameter transformer: %w", err)
}
// the directory ./pkg/operator/data/patches must be created, containing
// patches that shall be applied to the Helm output
patches, err := readPatches(data, "data/patches") // to be implemented ...
if err != nil {
return fmt.Errorf("error reading patches: %w", err)
}
objectTransformer, err := manifests.NewKustomizeObjectTransformer(patches, nil)
if err != nil {
return fmt.Errorf("error initializing object transformer: %w", err)
}
resourceGenerator := manifests.NewGenerator(helmGenerator).
WithParameterTransformer(parameterTransformer).
WithObjectTransformer(objectTransformer)
Check the generators documentation for more details.