Skip to content
Merged
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
1 change: 1 addition & 0 deletions Dockerfile.rhel
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ RUN make clean build

FROM registry.ci.openshift.org/ocp/4.22:base-rhel9
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/capi-operator .
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/capi-installer .
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/capi-controllers .
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/machine-api-migration .
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/crd-compatibility-checker .
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ CONTAINER_TOOL ?= docker
PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))

# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
ENVTEST_K8S_VERSION = 1.33.2
ENVTEST_K8S_VERSION = 1.35.1

ENVTEST = go run -mod=vendor ${PROJECT_DIR}/vendor/sigs.k8s.io/controller-runtime/tools/setup-envtest
GOLANGCI_LINT = go run -mod=vendor ${PROJECT_DIR}/vendor/github.com/golangci/golangci-lint/v2/cmd/golangci-lint
Expand All @@ -30,7 +30,7 @@ verify: fmt lint verify-ocp-manifests ## Run formatting and linting checks

test: verify unit ## Run verification and unit tests

build: bin/capi-operator bin/capi-controllers bin/machine-api-migration bin/crd-compatibility-checker manifests-gen ## Build all binaries
build: bin/capi-operator bin/capi-installer bin/capi-controllers bin/machine-api-migration bin/crd-compatibility-checker manifests-gen ## Build all binaries

clean:
rm -rf bin/*
Expand Down
193 changes: 193 additions & 0 deletions cmd/capi-installer/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Copyright 2026 Red Hat, Inc.
//
// 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 (
"context"
"errors"
"flag"
"fmt"
"os"
"time"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/utils/ptr"

ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"

configv1 "github.com/openshift/api/config/v1"
operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1"

"github.com/openshift/cluster-capi-operator/pkg/commoncmdoptions"
"github.com/openshift/cluster-capi-operator/pkg/controllers"
"github.com/openshift/cluster-capi-operator/pkg/controllers/installer"
"github.com/openshift/cluster-capi-operator/pkg/controllers/revision"
"github.com/openshift/cluster-capi-operator/pkg/providerimages"
"github.com/openshift/cluster-capi-operator/pkg/util"
)

var errPodIdentityNotSet = errors.New("POD_NAME and POD_NAMESPACE must be set")

const (
managerName = "capi-installer"
)

func initScheme(scheme *runtime.Scheme) {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(configv1.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
utilruntime.Must(appsv1.AddToScheme(scheme))
utilruntime.Must(operatorv1alpha1.AddToScheme(scheme))
}

func main() {
ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler())
cfg := ctrl.GetConfigOrDie()

scheme := runtime.NewScheme()
initScheme(scheme)

extraflags := flag.NewFlagSet("", flag.ContinueOnError)
providerImageDir := extraflags.String(
"provider-image-dir",
providerimages.ProviderImageMountBase,
"Directory containing provider image manifests. In dev mode, set to a local directory to skip pod spec reading.",
)

log, operatorConfig, mgrOpts, initManager, err := commoncmdoptions.InitOperatorConfig(ctx, cfg, scheme, managerName, controllers.DefaultOperatorNamespace, extraflags)
if err != nil {
log.Error(err, "unable to initialize operator config")
os.Exit(1)
}

mgrOpts.Cache = cache.Options{
DefaultNamespaces: map[string]cache.Config{
*operatorConfig.CAPINamespace: {},
*operatorConfig.OperatorNamespace: {},
},
SyncPeriod: ptr.To(10 * time.Minute),
}

mgr, err := initManager(ctx, cancel, mgrOpts)
if err != nil {
log.Error(err, "unable to initialize manager")
os.Exit(1)
}

if err := setupControllers(ctx, mgr, operatorConfig, *providerImageDir); err != nil {
log.Error(err, "unable to setup controllers")
os.Exit(1)
}

log.Info("Starting " + managerName + " manager")

if err := mgr.Start(ctx); err != nil {
log.Error(err, "problem running manager")
os.Exit(1)
}
}

func setupControllers(ctx context.Context, mgr ctrl.Manager, operatorConfig commoncmdoptions.OperatorConfig, providerImageDir string) error {
allProviderProfiles, err := loadProviderImages(ctx, mgr, providerImageDir)
if err != nil {
return err
}

currentReleaseRefs, err := loadCurrentReleaseImageRefs(ctx, mgr, *operatorConfig.OperatorNamespace)
if err != nil {
return err
}

currentReleaseProfiles := make([]providerimages.ProviderImageManifests, 0, len(allProviderProfiles))
for _, profile := range allProviderProfiles {
if currentReleaseRefs.Has(profile.ImageRef) {
currentReleaseProfiles = append(currentReleaseProfiles, profile)
}
}

log := ctrl.LoggerFrom(ctx)
for _, profile := range allProviderProfiles {
log.Info("loaded provider profile", "name", profile.Name, "imageRef", profile.ImageRef, "profile", profile.Profile)
}

if err := (&revision.RevisionController{
Client: mgr.GetClient(),
ProviderProfiles: currentReleaseProfiles,
ReleaseVersion: util.GetReleaseVersion(),
}).SetupWithManager(mgr, operatorConfig.TLSOptions); err != nil {
log.Error(err, "unable to create revision controller", "controller", "RevisionController")
return fmt.Errorf("unable to create revision controller: %w", err)
}

if err := installer.SetupWithManager(mgr, allProviderProfiles); err != nil {
return fmt.Errorf("unable to create installer controller: %w", err)
}

return nil
}

func loadProviderImages(ctx context.Context, mgr ctrl.Manager, providerImageDir string) ([]providerimages.ProviderImageManifests, error) {
podName := os.Getenv("POD_NAME")
podNamespace := os.Getenv("POD_NAMESPACE")

if podName == "" || podNamespace == "" {
return nil, errPodIdentityNotSet
}

var pod corev1.Pod
if err := mgr.GetAPIReader().Get(ctx, types.NamespacedName{Name: podName, Namespace: podNamespace}, &pod); err != nil {
return nil, fmt.Errorf("unable to get pod %s/%s: %w", podNamespace, podName, err)
}

imageRefMap, err := providerimages.BuildImageRefMap(pod.Spec, managerName)
if err != nil {
return nil, fmt.Errorf("unable to build image ref map from pod spec: %w", err)
}

log := ctrl.LoggerFrom(ctx)

providerProfiles, err := providerimages.ScanProviderImages(log, providerImageDir, imageRefMap)
if err != nil {
return nil, fmt.Errorf("unable to scan provider images: %w", err)
}

return providerProfiles, nil
}

func loadCurrentReleaseImageRefs(ctx context.Context, mgr ctrl.Manager, operatorNamespace string) (sets.Set[string], error) {
configMap := &corev1.ConfigMap{}

if err := mgr.GetAPIReader().Get(ctx, types.NamespacedName{
Name: providerimages.ConfigMapName,
Namespace: operatorNamespace,
}, configMap); err != nil {
return nil, fmt.Errorf("unable to get ConfigMap %s/%s: %w", operatorNamespace, providerimages.ConfigMapName, err)
}

imageRefs, err := providerimages.ImageRefsFromConfigMap(configMap)
if err != nil {
return nil, fmt.Errorf("unable to extract image refs from ConfigMap: %w", err)
}

return imageRefs, nil
}
92 changes: 32 additions & 60 deletions cmd/capi-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"time"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
Expand All @@ -33,6 +31,7 @@ import (

ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/go-logr/logr"
configv1 "github.com/openshift/api/config/v1"
Expand All @@ -41,24 +40,22 @@ import (
"github.com/openshift/cluster-capi-operator/pkg/commoncmdoptions"
"github.com/openshift/cluster-capi-operator/pkg/controllers"
"github.com/openshift/cluster-capi-operator/pkg/controllers/clusteroperator"
"github.com/openshift/cluster-capi-operator/pkg/controllers/installer"
"github.com/openshift/cluster-capi-operator/pkg/controllers/revision"
"github.com/openshift/cluster-capi-operator/pkg/providerimages"
"github.com/openshift/cluster-capi-operator/pkg/controllers/installerdeployment"
"github.com/openshift/cluster-capi-operator/pkg/util"
)

var errPodIdentityNotSet = errors.New("POD_NAME and POD_NAMESPACE must be set")
var (
errPodIdentityNotSet = errors.New("POD_NAME and POD_NAMESPACE must be set")
errContainerNotInPod = errors.New("container not found in pod spec")
)

const (
managerName = "capi-operator"

defaultProviderImageDirPath = "/var/lib/provider-images"
)

func initScheme(scheme *runtime.Scheme) {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(configv1.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
utilruntime.Must(appsv1.AddToScheme(scheme))
utilruntime.Must(operatorv1alpha1.AddToScheme(scheme))
}
Expand All @@ -70,14 +67,7 @@ func main() {
scheme := runtime.NewScheme()
initScheme(scheme)

extraflags := flag.NewFlagSet("", flag.ContinueOnError)
providerImageDir := extraflags.String(
"provider-image-dir",
defaultProviderImageDirPath,
"Directory containing provider image manifests. In dev mode, set to a local directory to skip pod spec reading.",
)

log, operatorConfig, mgrOpts, initManager, err := commoncmdoptions.InitOperatorConfig(ctx, cfg, scheme, managerName, controllers.DefaultOperatorNamespace, extraflags)
log, operatorConfig, mgrOpts, initManager, err := commoncmdoptions.InitOperatorConfig(ctx, cfg, scheme, managerName, controllers.DefaultOperatorNamespace, nil)
if err != nil {
log.Error(err, "unable to initialize operator config")
os.Exit(1)
Expand All @@ -97,7 +87,7 @@ func main() {
os.Exit(1)
}

if err := setupControllers(ctx, log, mgr, operatorConfig, *providerImageDir, cancel); err != nil {
if err := setupControllers(ctx, log, mgr, operatorConfig, cancel); err != nil {
log.Error(err, "unable to setup controllers")
os.Exit(1)
}
Expand All @@ -110,7 +100,7 @@ func main() {
}
}

func setupControllers(ctx context.Context, log logr.Logger, mgr ctrl.Manager, operatorConfig commoncmdoptions.OperatorConfig, providerImageDir string, cancel context.CancelFunc) error {
func setupControllers(ctx context.Context, log logr.Logger, mgr ctrl.Manager, operatorConfig commoncmdoptions.OperatorConfig, cancel context.CancelFunc) error {
infra, err := util.GetInfra(ctx, mgr.GetAPIReader())
if err != nil {
return fmt.Errorf("unable to get infrastructure: %w", err)
Expand All @@ -136,63 +126,45 @@ func setupControllers(ctx context.Context, log logr.Logger, mgr ctrl.Manager, op
return fmt.Errorf("unable to create clusteroperator controller: %w", err)
}

// The ClusterOperatorController MUST run if we were installed, otherwise
// our ClusterOperator will not be reconciled and installation will not
// progress. We don't run any other controllers if the current platform is
// not supported.
if !supportedPlatform {
return nil
}

providerProfiles, err := loadProviderImages(ctx, mgr, providerImageDir)
// Get container image from own pod spec
containerImage, err := getContainerImage(ctx, mgr.GetAPIReader())
if err != nil {
return err
}

for _, profile := range providerProfiles {
log.Info("loaded provider profile", "name", profile.Name, "imageRef", profile.ImageRef, "profile", profile.Profile)
}

if err := (&revision.RevisionController{
Client: mgr.GetClient(),
ProviderProfiles: providerProfiles,
ReleaseVersion: util.GetReleaseVersion(),
}).SetupWithManager(mgr, operatorConfig.TLSOptions); err != nil {
log.Error(err, "unable to create revision controller", "controller", "RevisionController")
return fmt.Errorf("unable to create revision controller: %w", err)
return fmt.Errorf("unable to get container image: %w", err)
}

if err := installer.SetupWithManager(mgr, providerProfiles); err != nil {
return fmt.Errorf("unable to create installer controller: %w", err)
// Setup InstallerDeploymentController (runs on all platforms)
if err := (&installerdeployment.InstallerDeploymentReconciler{
Client: mgr.GetClient(),
Namespace: *operatorConfig.OperatorNamespace,
ContainerImage: containerImage,
SupportedPlatform: supportedPlatform,
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("unable to create installerdeployment controller: %w", err)
}

return nil
}

func loadProviderImages(ctx context.Context, mgr ctrl.Manager, providerImageDir string) ([]providerimages.ProviderImageManifests, error) {
// getContainerImage reads the container image from the capi-operator pod spec.
func getContainerImage(ctx context.Context, k8sClient client.Reader) (string, error) {
podName := os.Getenv("POD_NAME")

podNamespace := os.Getenv("POD_NAMESPACE")

if podName == "" || podNamespace == "" {
return nil, errPodIdentityNotSet
return "", errPodIdentityNotSet
}

var pod corev1.Pod
if err := mgr.GetAPIReader().Get(ctx, types.NamespacedName{Name: podName, Namespace: podNamespace}, &pod); err != nil {
return nil, fmt.Errorf("unable to get pod %s/%s: %w", podNamespace, podName, err)
}

imageRefMap, err := providerimages.BuildImageRefMap(pod.Spec, managerName)
if err != nil {
return nil, fmt.Errorf("unable to build image ref map from pod spec: %w", err)
if err := k8sClient.Get(ctx, types.NamespacedName{Name: podName, Namespace: podNamespace}, &pod); err != nil {
return "", fmt.Errorf("unable to get pod %s/%s: %w", podNamespace, podName, err)
}

log := ctrl.LoggerFrom(ctx)

providerProfiles, err := providerimages.ScanProviderImages(log, providerImageDir, imageRefMap)
if err != nil {
return nil, fmt.Errorf("unable to scan provider images: %w", err)
// Find the capi-operator container
for _, container := range pod.Spec.Containers {
if container.Name == managerName {
return container.Image, nil
}
}

return providerProfiles, nil
return "", fmt.Errorf("%s: %w", managerName, errContainerNotInPod)
}
Loading