Skip to content
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
58 changes: 58 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ before:
- go mod tidy
- go mod download
builds:
- id: object-controller
main: ./cmd/object-controller/
binary: object-controller
asmflags: "{{ .Env.GO_BUILD_ASMFLAGS }}"
gcflags: "{{ .Env.GO_BUILD_GCFLAGS }}"
ldflags: "{{ .Env.GO_BUILD_LDFLAGS }}"
tags:
- "{{ .Env.GO_BUILD_TAGS }}"
mod_timestamp: "{{ .CommitTimestamp }}"
goos:
- linux
goarch:
- amd64
- arm64
- ppc64le
- s390x
- id: operator-controller
main: ./cmd/operator-controller/
binary: operator-controller
Expand Down Expand Up @@ -37,6 +53,42 @@ builds:
- ppc64le
- s390x
dockers:
- image_templates:
- "{{ .Env.OBJECT_CONTROLLER_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}-amd64"
dockerfile: Dockerfile.object-controller
goos: linux
goarch: amd64
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--provenance=false"
- image_templates:
- "{{ .Env.OBJECT_CONTROLLER_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}-arm64"
dockerfile: Dockerfile.object-controller
goos: linux
goarch: arm64
use: buildx
build_flag_templates:
- "--platform=linux/arm64"
- "--provenance=false"
- image_templates:
- "{{ .Env.OBJECT_CONTROLLER_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}-ppc64le"
dockerfile: Dockerfile.object-controller
goos: linux
goarch: ppc64le
use: buildx
build_flag_templates:
- "--platform=linux/ppc64le"
- "--provenance=false"
- image_templates:
- "{{ .Env.OBJECT_CONTROLLER_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}-s390x"
dockerfile: Dockerfile.object-controller
goos: linux
goarch: s390x
use: buildx
build_flag_templates:
- "--platform=linux/s390x"
- "--provenance=false"
- image_templates:
- "{{ .Env.OPCON_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}-amd64"
dockerfile: Dockerfile.operator-controller
Expand Down Expand Up @@ -110,6 +162,12 @@ dockers:
- "--platform=linux/s390x"
- "--provenance=false"
docker_manifests:
- name_template: "{{ .Env.OBJECT_CONTROLLER_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}"
image_templates:
- "{{ .Env.OBJECT_CONTROLLER_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}-amd64"
- "{{ .Env.OBJECT_CONTROLLER_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}-arm64"
- "{{ .Env.OBJECT_CONTROLLER_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}-ppc64le"
- "{{ .Env.OBJECT_CONTROLLER_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}-s390x"
- name_template: "{{ .Env.OPCON_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}"
image_templates:
- "{{ .Env.OPCON_IMAGE_REPO }}:{{ .Env.IMAGE_TAG }}-amd64"
Expand Down
7 changes: 7 additions & 0 deletions Dockerfile.object-controller
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Build the binary with make build-linux before building this image.
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY object-controller object-controller
USER 65532:65532

ENTRYPOINT ["/object-controller"]
14 changes: 11 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@ CATD_IMAGE_REPO := $(IMAGE_REGISTRY)/catalogd
endif
export CATD_IMAGE_REPO

ifeq ($(origin OBJECT_CONTROLLER_IMAGE_REPO), undefined)
OBJECT_CONTROLLER_IMAGE_REPO := $(IMAGE_REGISTRY)/object-controller
endif
export OBJECT_CONTROLLER_IMAGE_REPO

ifeq ($(origin IMAGE_TAG), undefined)
IMAGE_TAG := devel
endif
export IMAGE_TAG

OPCON_IMG := $(OPCON_IMAGE_REPO):$(IMAGE_TAG)
CATD_IMG := $(CATD_IMAGE_REPO):$(IMAGE_TAG)
OBJECT_CONTROLLER_IMG := $(OBJECT_CONTROLLER_IMAGE_REPO):$(IMAGE_TAG)

# Extract Kubernetes client-go version used to set the version to the PSA labels, for ENVTEST and KIND
ifeq ($(origin K8S_VERSION), undefined)
Expand Down Expand Up @@ -351,6 +357,7 @@ kind-cluster-%: $(KIND) #EXHELP Create a kind cluster named after the stem (%).
kind-load-%: kind-cluster-% docker-build
$(KIND) load docker-image $(OPCON_IMG) --name $*
$(KIND) load docker-image $(CATD_IMG) --name $*
$(KIND) load docker-image $(OBJECT_CONTROLLER_IMG) --name $*

.PHONY: kind-deploy-%
kind-deploy-%: kind-load-% manifests
Expand Down Expand Up @@ -605,7 +612,7 @@ export GO_BUILD_LDFLAGS := -s -w \
-X '$(VERSION_PATH).version=$(VERSION)' \
-X '$(VERSION_PATH).gitCommit=$(GIT_COMMIT)' \

BINARIES=operator-controller catalogd
BINARIES=operator-controller catalogd object-controller

.PHONY: $(BINARIES)
$(BINARIES):
Expand Down Expand Up @@ -644,9 +651,10 @@ FLUENTBIT_NAMESPACE := fluent-bit
FLUENTBIT_CHART_VERSION := 0.57.9

.PHONY: docker-build
docker-build: build-linux #EXHELP Build docker image for operator-controller and catalog with GOOS=linux and local GOARCH.
docker-build: build-linux #EXHELP Build controller images with GOOS=linux and local GOARCH.
$(CONTAINER_RUNTIME) build -t $(OPCON_IMG) -f Dockerfile.operator-controller ./bin/linux
$(CONTAINER_RUNTIME) build -t $(CATD_IMG) -f Dockerfile.catalogd ./bin/linux
$(CONTAINER_RUNTIME) build -t $(OBJECT_CONTROLLER_IMG) -f Dockerfile.object-controller ./bin/linux

#SECTION Release
ifeq ($(origin ENABLE_RELEASE_PIPELINE), undefined)
Expand All @@ -661,7 +669,7 @@ export GORELEASER_ARGS

.PHONY: release
release: $(GORELEASER) #EXHELP Runs goreleaser for the operator-controller. By default, this will run only as a snapshot and will not publish any artifacts unless it is run with different arguments. To override the arguments, run with "GORELEASER_ARGS=...". When run as a github action from a tag, this target will publish a full release.
OPCON_IMAGE_REPO=$(OPCON_IMAGE_REPO) CATD_IMAGE_REPO=$(CATD_IMAGE_REPO) $(GORELEASER) $(GORELEASER_ARGS)
OPCON_IMAGE_REPO=$(OPCON_IMAGE_REPO) CATD_IMAGE_REPO=$(CATD_IMAGE_REPO) OBJECT_CONTROLLER_IMAGE_REPO=$(OBJECT_CONTROLLER_IMAGE_REPO) $(GORELEASER) $(GORELEASER_ARGS)

.PHONY: quickstart
quickstart: export STANDARD_MANIFEST_URL := "https://github.com/operator-framework/operator-controller/releases/download/$(VERSION)/$(notdir $(STANDARD_RELEASE_MANIFEST))"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ controllers, and tooling that support the packaging, distribution, and lifecycli
- provide secure, high-quality, and predictable user experiences centered around declarative GitOps concepts
- give cluster admins the minimal necessary controls to build their desired cluster architectures and to have ultimate control

OLM v1 consists of two different components:
OLM v1 consists of the following components:

* operator-controller
* catalogd
* object-controller (experimental): manages `ClusterObjectSet` resources independently of `ClusterExtension`

For a more complete overview of OLM v1 and how it differs from OLM v0, see our [overview](docs/project/olmv1_design_decisions.md).

Expand Down
15 changes: 15 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,19 @@ olmv1 = {
'yaml': 'helm/tilt.yaml',
}

values = read_yaml(olmv1['yaml'])
options = values.get('options', {})
features = options.get('operatorController', {}).get('features', {})
object_controller_enabled = options.get('objectController', {}).get('enabled')
if object_controller_enabled == None:
object_controller_enabled = options.get('operatorController', {}).get('enabled', True) and 'BoxcutterRuntime' in features.get('enabled', []) and 'BoxcutterRuntime' not in features.get('disabled', ['BoxcutterRuntime'])
if object_controller_enabled:
olmv1['repos']['object-controller'] = {
'image': 'quay.io/operator-framework/object-controller',
'binary': './cmd/object-controller',
'deployment': 'object-controller-controller-manager',
'deps': ['api', 'cmd/object-controller', 'internal/object-controller', 'internal/shared', 'go.mod', 'go.sum'],
'starting_debug_port': 40000,
}

deploy_repo(olmv1, '-tags containers_image_openpgp')
211 changes: 211 additions & 0 deletions cmd/object-controller/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
Copyright 2026.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"crypto/tls"
"flag"
"fmt"
"os"
"time"

"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
"pkg.package-operator.run/boxcutter/managedcache"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/certwatcher"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
"sigs.k8s.io/controller-runtime/pkg/metrics/server"

ocv1 "github.com/operator-framework/operator-controller/api/v1"
"github.com/operator-framework/operator-controller/internal/object-controller/controllers"
"github.com/operator-framework/operator-controller/internal/object-controller/scheme"
cacheutil "github.com/operator-framework/operator-controller/internal/shared/util/cache"
"github.com/operator-framework/operator-controller/internal/shared/util/tlsprofiles"
"github.com/operator-framework/operator-controller/internal/shared/version"
)

type config struct {
metricsAddr string
pprofAddr string
probeAddr string
certFile string
keyFile string
enableLeaderElection bool
}

func newCommand() *cobra.Command {
cfg := &config{}
cmd := &cobra.Command{
Use: "object-controller",
Short: "Manage Kubernetes objects through ClusterObjectSets",
RunE: func(cmd *cobra.Command, _ []string) error {
if err := cfg.validate(); err != nil {
return err
}
restConfig, err := ctrl.GetConfig()
if err != nil {
return err
}
mgr, err := newManager(cfg, restConfig)
if err != nil {
return err
}
ctrl.Log.WithName("setup").Info("starting object-controller", "version info", version.String())
return mgr.Start(cmd.Context())
},
}
flags := cmd.Flags()
flags.StringVar(&cfg.metricsAddr, "metrics-bind-address", "", "The metrics endpoint address. Requires tls-cert and tls-key. (Default: ':8443')")
flags.StringVar(&cfg.pprofAddr, "pprof-bind-address", "0", "The pprof endpoint address. An empty string or 0 disables pprof.")
flags.StringVar(&cfg.probeAddr, "health-probe-bind-address", ":8081", "The health probe endpoint address.")
flags.StringVar(&cfg.certFile, "tls-cert", "", "The certificate file for the metrics server. Requires tls-key.")
flags.StringVar(&cfg.keyFile, "tls-key", "", "The key file for the metrics server. Requires tls-cert.")
flags.BoolVar(&cfg.enableLeaderElection, "leader-elect", false, "Enable leader election for the controller manager.")
logFlags := flag.NewFlagSet("logging", flag.ContinueOnError)
klog.InitFlags(logFlags)
flags.AddGoFlagSet(logFlags)
tlsprofiles.AddFlags(flags)
cmd.AddCommand(&cobra.Command{
Use: "version",
Short: "Print object-controller version information",
Run: func(cmd *cobra.Command, _ []string) {
cmd.Println(version.String())
},
})
return cmd
}

func (c *config) validate() error {
if (c.certFile == "") != (c.keyFile == "") {
return fmt.Errorf("tls-cert and tls-key flags must be used together")
}
if c.metricsAddr != "" && c.certFile == "" {
return fmt.Errorf("metrics-bind-address requires tls-cert and tls-key flags to be set")
}
if c.certFile != "" && c.metricsAddr == "" {
c.metricsAddr = ":8443"
}
return nil
}

func newManager(cfg *config, restConfig *rest.Config) (manager.Manager, error) {
metricsOptions := server.Options{BindAddress: "0"}
var certWatcher *certwatcher.CertWatcher
if cfg.certFile != "" {
var err error
certWatcher, err = certwatcher.New(cfg.certFile, cfg.keyFile)
if err != nil {
return nil, fmt.Errorf("initializing certificate watcher: %w", err)
}
tlsProfile, err := tlsprofiles.GetTLSConfigFunc()
if err != nil {
return nil, fmt.Errorf("getting TLS profile: %w", err)
}
metricsOptions = server.Options{
BindAddress: cfg.metricsAddr,
SecureServing: true,
FilterProvider: filters.WithAuthenticationAndAuthorization,
TLSOpts: []func(*tls.Config){
func(c *tls.Config) {
c.GetCertificate = certWatcher.GetCertificate
c.NextProtos = []string{"http/1.1"}
},
tlsProfile,
},
}
}
mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
Scheme: scheme.Scheme,
Metrics: metricsOptions,
PprofBindAddress: cfg.pprofAddr,
HealthProbeBindAddress: cfg.probeAddr,
LeaderElection: cfg.enableLeaderElection,
LeaderElectionID: "object-controller-lock.olm.operatorframework.io",
LeaderElectionReleaseOnCancel: true,
LeaseDuration: ptr.To(137 * time.Second),
RenewDeadline: ptr.To(107 * time.Second),
RetryPeriod: ptr.To(26 * time.Second),
Cache: cache.Options{
ByObject: map[client.Object]cache.ByObject{&ocv1.ClusterObjectSet{}: {}},
ReaderFailOnMissingInformer: true,
DefaultTransform: cacheutil.StripAnnotations(),
},
// References can point to immutable Secrets in any namespace. Read them
// directly, without caching unrelated cluster Secrets or requiring a system namespace.
Client: client.Options{Cache: &client.CacheOptions{DisableFor: []client.Object{&corev1.Secret{}}}},
})
if err != nil {
return nil, fmt.Errorf("creating manager: %w", err)
}
if certWatcher != nil {
if err := mgr.Add(certWatcher); err != nil {
return nil, fmt.Errorf("adding certificate watcher: %w", err)
}
}
trackingCache, err := managedcache.NewTrackingCache(
ctrl.Log.WithName("trackingCache"), mgr.GetConfig(),
cache.Options{Scheme: mgr.GetScheme(), Mapper: mgr.GetRESTMapper()},
)
if err != nil {
return nil, fmt.Errorf("creating tracking cache: %w", err)
}
if err := mgr.Add(trackingCache); err != nil {
return nil, fmt.Errorf("adding tracking cache: %w", err)
}
discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig())
if err != nil {
return nil, fmt.Errorf("creating discovery client: %w", err)
}
// Keep the field owner prefix unchanged so existing objects can be reconciled after migration.
factory, err := controllers.NewDefaultRevisionEngineFactory(
mgr.GetScheme(), trackingCache, memory.NewMemCacheClient(discoveryClient),
mgr.GetRESTMapper(), "olm.operatorframework.io", mgr.GetConfig(),
)
if err != nil {
return nil, fmt.Errorf("creating revision engine factory: %w", err)
}
if err := (&controllers.ClusterObjectSetReconciler{
Client: mgr.GetClient(), RevisionEngineFactory: factory, TrackingCache: trackingCache,
}).SetupWithManager(mgr); err != nil {
return nil, fmt.Errorf("setting up ClusterObjectSet controller: %w", err)
}
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
return nil, err
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
return nil, err
}
return mgr, nil
}

func main() {
ctrl.SetLogger(klog.NewKlogr())
if err := newCommand().ExecuteContext(ctrl.SetupSignalHandler()); err != nil {
os.Exit(1)
}
}
Loading
Loading