Skip to content

Commit a89b628

Browse files
committed
Installer is separate deployment managed by capi-operator
1 parent ee8aae1 commit a89b628

39 files changed

Lines changed: 1656 additions & 297 deletions

File tree

Dockerfile.rhel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ RUN make clean build
55

66
FROM registry.ci.openshift.org/ocp/4.22:base-rhel9
77
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/capi-operator .
8+
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/capi-installer .
89
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/capi-controllers .
910
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/machine-api-migration .
1011
COPY --from=builder /go/src/github.com/openshift/cluster-capi-operator/bin/crd-compatibility-checker .

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ verify: fmt lint verify-ocp-manifests ## Run formatting and linting checks
2929

3030
test: verify unit ## Run verification and unit tests
3131

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

3434
clean:
3535
rm -rf bin/*

cmd/capi-installer/main.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2026 Red Hat, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
"errors"
20+
"flag"
21+
"fmt"
22+
"os"
23+
"time"
24+
25+
appsv1 "k8s.io/api/apps/v1"
26+
corev1 "k8s.io/api/core/v1"
27+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
28+
"k8s.io/apimachinery/pkg/runtime"
29+
"k8s.io/apimachinery/pkg/types"
30+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
31+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
32+
"k8s.io/utils/ptr"
33+
34+
ctrl "sigs.k8s.io/controller-runtime"
35+
"sigs.k8s.io/controller-runtime/pkg/cache"
36+
37+
configv1 "github.com/openshift/api/config/v1"
38+
operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1"
39+
40+
"github.com/openshift/cluster-capi-operator/pkg/commoncmdoptions"
41+
"github.com/openshift/cluster-capi-operator/pkg/controllers"
42+
"github.com/openshift/cluster-capi-operator/pkg/controllers/installer"
43+
"github.com/openshift/cluster-capi-operator/pkg/controllers/revision"
44+
"github.com/openshift/cluster-capi-operator/pkg/providerimages"
45+
"github.com/openshift/cluster-capi-operator/pkg/util"
46+
)
47+
48+
var errPodIdentityNotSet = errors.New("POD_NAME and POD_NAMESPACE must be set")
49+
50+
const (
51+
managerName = "capi-installer"
52+
53+
defaultProviderImageDirPath = "/var/lib/provider-images"
54+
)
55+
56+
func initScheme(scheme *runtime.Scheme) {
57+
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
58+
utilruntime.Must(configv1.AddToScheme(scheme))
59+
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
60+
utilruntime.Must(appsv1.AddToScheme(scheme))
61+
utilruntime.Must(operatorv1alpha1.AddToScheme(scheme))
62+
}
63+
64+
func main() {
65+
ctx, cancel := context.WithCancel(ctrl.SetupSignalHandler())
66+
cfg := ctrl.GetConfigOrDie()
67+
68+
scheme := runtime.NewScheme()
69+
initScheme(scheme)
70+
71+
extraflags := flag.NewFlagSet("", flag.ContinueOnError)
72+
providerImageDir := extraflags.String(
73+
"provider-image-dir",
74+
defaultProviderImageDirPath,
75+
"Directory containing provider image manifests. In dev mode, set to a local directory to skip pod spec reading.",
76+
)
77+
78+
log, operatorConfig, mgrOpts, initManager, err := commoncmdoptions.InitOperatorConfig(ctx, cfg, scheme, managerName, controllers.DefaultOperatorNamespace, extraflags)
79+
if err != nil {
80+
log.Error(err, "unable to initialize operator config")
81+
os.Exit(1)
82+
}
83+
84+
mgrOpts.Cache = cache.Options{
85+
DefaultNamespaces: map[string]cache.Config{
86+
*operatorConfig.CAPINamespace: {},
87+
*operatorConfig.OperatorNamespace: {},
88+
},
89+
SyncPeriod: ptr.To(10 * time.Minute),
90+
}
91+
92+
mgr, err := initManager(ctx, cancel, mgrOpts)
93+
if err != nil {
94+
log.Error(err, "unable to initialize manager")
95+
os.Exit(1)
96+
}
97+
98+
if err := setupControllers(ctx, mgr, operatorConfig, *providerImageDir); err != nil {
99+
log.Error(err, "unable to setup controllers")
100+
os.Exit(1)
101+
}
102+
103+
log.Info("Starting " + managerName + " manager")
104+
105+
if err := mgr.Start(ctx); err != nil {
106+
log.Error(err, "problem running manager")
107+
os.Exit(1)
108+
}
109+
}
110+
111+
func setupControllers(ctx context.Context, mgr ctrl.Manager, operatorConfig commoncmdoptions.OperatorConfig, providerImageDir string) error {
112+
providerProfiles, err := loadProviderImages(ctx, mgr, providerImageDir)
113+
if err != nil {
114+
return err
115+
}
116+
117+
log := ctrl.LoggerFrom(ctx)
118+
for _, profile := range providerProfiles {
119+
log.Info("loaded provider profile", "name", profile.Name, "imageRef", profile.ImageRef, "profile", profile.Profile)
120+
}
121+
122+
if err := (&revision.RevisionController{
123+
Client: mgr.GetClient(),
124+
ProviderProfiles: providerProfiles,
125+
ReleaseVersion: util.GetReleaseVersion(),
126+
}).SetupWithManager(mgr, operatorConfig.TLSOptions); err != nil {
127+
log.Error(err, "unable to create revision controller", "controller", "RevisionController")
128+
return fmt.Errorf("unable to create revision controller: %w", err)
129+
}
130+
131+
if err := installer.SetupWithManager(mgr, providerProfiles); err != nil {
132+
return fmt.Errorf("unable to create installer controller: %w", err)
133+
}
134+
135+
return nil
136+
}
137+
138+
func loadProviderImages(ctx context.Context, mgr ctrl.Manager, providerImageDir string) ([]providerimages.ProviderImageManifests, error) {
139+
podName := os.Getenv("POD_NAME")
140+
podNamespace := os.Getenv("POD_NAMESPACE")
141+
142+
if podName == "" || podNamespace == "" {
143+
return nil, errPodIdentityNotSet
144+
}
145+
146+
var pod corev1.Pod
147+
if err := mgr.GetAPIReader().Get(ctx, types.NamespacedName{Name: podName, Namespace: podNamespace}, &pod); err != nil {
148+
return nil, fmt.Errorf("unable to get pod %s/%s: %w", podNamespace, podName, err)
149+
}
150+
151+
imageRefMap, err := providerimages.BuildImageRefMap(pod.Spec, managerName)
152+
if err != nil {
153+
return nil, fmt.Errorf("unable to build image ref map from pod spec: %w", err)
154+
}
155+
156+
log := ctrl.LoggerFrom(ctx)
157+
158+
providerProfiles, err := providerimages.ScanProviderImages(log, providerImageDir, imageRefMap)
159+
if err != nil {
160+
return nil, fmt.Errorf("unable to scan provider images: %w", err)
161+
}
162+
163+
return providerProfiles, nil
164+
}

cmd/capi-operator/main.go

Lines changed: 32 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@ package main
1717
import (
1818
"context"
1919
"errors"
20-
"flag"
2120
"fmt"
2221
"os"
2322
"time"
2423

2524
appsv1 "k8s.io/api/apps/v1"
2625
corev1 "k8s.io/api/core/v1"
27-
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2826
"k8s.io/apimachinery/pkg/runtime"
2927
"k8s.io/apimachinery/pkg/types"
3028
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -33,6 +31,7 @@ import (
3331

3432
ctrl "sigs.k8s.io/controller-runtime"
3533
"sigs.k8s.io/controller-runtime/pkg/cache"
34+
"sigs.k8s.io/controller-runtime/pkg/client"
3635

3736
"github.com/go-logr/logr"
3837
configv1 "github.com/openshift/api/config/v1"
@@ -41,24 +40,22 @@ import (
4140
"github.com/openshift/cluster-capi-operator/pkg/commoncmdoptions"
4241
"github.com/openshift/cluster-capi-operator/pkg/controllers"
4342
"github.com/openshift/cluster-capi-operator/pkg/controllers/clusteroperator"
44-
"github.com/openshift/cluster-capi-operator/pkg/controllers/installer"
45-
"github.com/openshift/cluster-capi-operator/pkg/controllers/revision"
46-
"github.com/openshift/cluster-capi-operator/pkg/providerimages"
43+
"github.com/openshift/cluster-capi-operator/pkg/controllers/installerdeployment"
4744
"github.com/openshift/cluster-capi-operator/pkg/util"
4845
)
4946

50-
var errPodIdentityNotSet = errors.New("POD_NAME and POD_NAMESPACE must be set")
47+
var (
48+
errPodIdentityNotSet = errors.New("POD_NAME and POD_NAMESPACE must be set")
49+
errContainerNotInPod = errors.New("container not found in pod spec")
50+
)
5151

5252
const (
5353
managerName = "capi-operator"
54-
55-
defaultProviderImageDirPath = "/var/lib/provider-images"
5654
)
5755

5856
func initScheme(scheme *runtime.Scheme) {
5957
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
6058
utilruntime.Must(configv1.AddToScheme(scheme))
61-
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
6259
utilruntime.Must(appsv1.AddToScheme(scheme))
6360
utilruntime.Must(operatorv1alpha1.AddToScheme(scheme))
6461
}
@@ -70,14 +67,7 @@ func main() {
7067
scheme := runtime.NewScheme()
7168
initScheme(scheme)
7269

73-
extraflags := flag.NewFlagSet("", flag.ContinueOnError)
74-
providerImageDir := extraflags.String(
75-
"provider-image-dir",
76-
defaultProviderImageDirPath,
77-
"Directory containing provider image manifests. In dev mode, set to a local directory to skip pod spec reading.",
78-
)
79-
80-
log, operatorConfig, mgrOpts, initManager, err := commoncmdoptions.InitOperatorConfig(ctx, cfg, scheme, managerName, controllers.DefaultOperatorNamespace, extraflags)
70+
log, operatorConfig, mgrOpts, initManager, err := commoncmdoptions.InitOperatorConfig(ctx, cfg, scheme, managerName, controllers.DefaultOperatorNamespace, nil)
8171
if err != nil {
8272
log.Error(err, "unable to initialize operator config")
8373
os.Exit(1)
@@ -97,7 +87,7 @@ func main() {
9787
os.Exit(1)
9888
}
9989

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

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

139-
// The ClusterOperatorController MUST run if we were installed, otherwise
140-
// our ClusterOperator will not be reconciled and installation will not
141-
// progress. We don't run any other controllers if the current platform is
142-
// not supported.
143-
if !supportedPlatform {
144-
return nil
145-
}
146-
147-
providerProfiles, err := loadProviderImages(ctx, mgr, providerImageDir)
129+
// Get container image from own pod spec
130+
containerImage, err := getContainerImage(ctx, mgr.GetAPIReader())
148131
if err != nil {
149-
return err
150-
}
151-
152-
for _, profile := range providerProfiles {
153-
log.Info("loaded provider profile", "name", profile.Name, "imageRef", profile.ImageRef, "profile", profile.Profile)
154-
}
155-
156-
if err := (&revision.RevisionController{
157-
Client: mgr.GetClient(),
158-
ProviderProfiles: providerProfiles,
159-
ReleaseVersion: util.GetReleaseVersion(),
160-
}).SetupWithManager(mgr, operatorConfig.TLSOptions); err != nil {
161-
log.Error(err, "unable to create revision controller", "controller", "RevisionController")
162-
return fmt.Errorf("unable to create revision controller: %w", err)
132+
return fmt.Errorf("unable to get container image: %w", err)
163133
}
164134

165-
if err := installer.SetupWithManager(mgr, providerProfiles); err != nil {
166-
return fmt.Errorf("unable to create installer controller: %w", err)
135+
// Setup InstallerDeploymentController (runs on all platforms)
136+
if err := (&installerdeployment.InstallerDeploymentReconciler{
137+
Client: mgr.GetClient(),
138+
Namespace: *operatorConfig.OperatorNamespace,
139+
ContainerImage: containerImage,
140+
SupportedPlatform: supportedPlatform,
141+
}).SetupWithManager(mgr); err != nil {
142+
return fmt.Errorf("unable to create installerdeployment controller: %w", err)
167143
}
168144

169145
return nil
170146
}
171147

172-
func loadProviderImages(ctx context.Context, mgr ctrl.Manager, providerImageDir string) ([]providerimages.ProviderImageManifests, error) {
148+
// getContainerImage reads the container image from the capi-operator pod spec.
149+
func getContainerImage(ctx context.Context, k8sClient client.Reader) (string, error) {
173150
podName := os.Getenv("POD_NAME")
174-
175151
podNamespace := os.Getenv("POD_NAMESPACE")
152+
176153
if podName == "" || podNamespace == "" {
177-
return nil, errPodIdentityNotSet
154+
return "", errPodIdentityNotSet
178155
}
179156

180157
var pod corev1.Pod
181-
if err := mgr.GetAPIReader().Get(ctx, types.NamespacedName{Name: podName, Namespace: podNamespace}, &pod); err != nil {
182-
return nil, fmt.Errorf("unable to get pod %s/%s: %w", podNamespace, podName, err)
183-
}
184-
185-
imageRefMap, err := providerimages.BuildImageRefMap(pod.Spec, managerName)
186-
if err != nil {
187-
return nil, fmt.Errorf("unable to build image ref map from pod spec: %w", err)
158+
if err := k8sClient.Get(ctx, types.NamespacedName{Name: podName, Namespace: podNamespace}, &pod); err != nil {
159+
return "", fmt.Errorf("unable to get pod %s/%s: %w", podNamespace, podName, err)
188160
}
189161

190-
log := ctrl.LoggerFrom(ctx)
191-
192-
providerProfiles, err := providerimages.ScanProviderImages(log, providerImageDir, imageRefMap)
193-
if err != nil {
194-
return nil, fmt.Errorf("unable to scan provider images: %w", err)
162+
// Find the capi-operator container
163+
for _, container := range pod.Spec.Containers {
164+
if container.Name == managerName {
165+
return container.Image, nil
166+
}
195167
}
196168

197-
return providerProfiles, nil
169+
return "", fmt.Errorf("%s: %w", managerName, errContainerNotInPod)
198170
}

0 commit comments

Comments
 (0)