Skip to content

Commit 20bfbd7

Browse files
committed
capiinstaller: Read manifests direct from payload image
1 parent 41ea269 commit 20bfbd7

File tree

8 files changed

+1066
-40
lines changed

8 files changed

+1066
-40
lines changed

cmd/cluster-capi-operator/main.go

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,20 @@ import (
6363
"github.com/openshift/cluster-capi-operator/pkg/controllers/kubeconfig"
6464
"github.com/openshift/cluster-capi-operator/pkg/controllers/secretsync"
6565
"github.com/openshift/cluster-capi-operator/pkg/operatorstatus"
66+
"github.com/openshift/cluster-capi-operator/pkg/providerimages"
6667
"github.com/openshift/cluster-capi-operator/pkg/util"
6768
"github.com/openshift/cluster-capi-operator/pkg/webhook"
6869
)
6970

7071
const (
71-
defaultImagesLocation = "./dev-images.json"
72+
defaultImagesLocation = "./dev-images.json"
73+
7274
defaultMachineAPINamespace = "openshift-machine-api"
75+
76+
pullSecretPathEnvVar = "PULL_SECRET_PATH"
77+
defaultPullSecretPath = "/var/run/secrets/pull-secret/config.json"
78+
providerImageDirEnvVar = "PROVIDER_IMAGE_DIR"
79+
defaultProviderImageDirPath = "/var/lib/provider-images"
7380
)
7481

7582
func initScheme(scheme *runtime.Scheme) {
@@ -208,6 +215,28 @@ func main() {
208215
os.Exit(1)
209216
}
210217

218+
pullSecretPath := os.Getenv(pullSecretPathEnvVar)
219+
if pullSecretPath == "" {
220+
pullSecretPath = defaultPullSecretPath
221+
}
222+
223+
providerImageDir := os.Getenv(providerImageDirEnvVar)
224+
if providerImageDir == "" {
225+
providerImageDir = defaultProviderImageDirPath
226+
}
227+
228+
pullSecret, err := os.ReadFile(pullSecretPath)
229+
if err != nil {
230+
klog.Error(err, "unable to read pull secret", "path", pullSecretPath)
231+
os.Exit(1)
232+
}
233+
234+
providerImages, err := providerimages.ReadProviderImages(context.Background(), containerImages, providerImageDir, pullSecret)
235+
if err != nil {
236+
klog.Error(err, "unable to get provider image metadata")
237+
os.Exit(1)
238+
}
239+
211240
infra, err := util.GetInfra(context.Background(), mgr.GetAPIReader())
212241
if err != nil {
213242
klog.Error(err, "unable to get infrastructure object")
@@ -220,7 +249,7 @@ func main() {
220249
os.Exit(1)
221250
}
222251

223-
setupPlatformReconcilers(mgr, infra, platform, containerImages, applyClient, apiextensionsClient, *managedNamespace)
252+
setupPlatformReconcilers(mgr, infra, platform, containerImages, providerImages, applyClient, apiextensionsClient, *managedNamespace)
224253

225254
// +kubebuilder:scaffold:builder
226255

@@ -252,17 +281,17 @@ func getClusterOperatorStatusClient(mgr manager.Manager, controller string, plat
252281
}
253282
}
254283

255-
func setupPlatformReconcilers(mgr manager.Manager, infra *configv1.Infrastructure, platform configv1.PlatformType, containerImages map[string]string, applyClient *kubernetes.Clientset, apiextensionsClient *apiextensionsclient.Clientset, managedNamespace string) {
284+
func setupPlatformReconcilers(mgr manager.Manager, infra *configv1.Infrastructure, platform configv1.PlatformType, containerImages map[string]string, providerImages map[string]providerimages.ProviderImageManifests, applyClient *kubernetes.Clientset, apiextensionsClient *apiextensionsclient.Clientset, managedNamespace string) {
256285
// Only setup reconcile controllers and webhooks when the platform is supported.
257286
// This avoids unnecessary CAPI providers discovery, installs and reconciles when the platform is not supported.
258287
isUnsupportedPlatform := false
259288

260289
switch platform {
261290
case configv1.AWSPlatformType:
262-
setupReconcilers(mgr, infra, platform, &awsv1.AWSCluster{}, containerImages, applyClient, apiextensionsClient, managedNamespace)
291+
setupReconcilers(mgr, infra, platform, &awsv1.AWSCluster{}, containerImages, providerImages, applyClient, apiextensionsClient, managedNamespace)
263292
setupWebhooks(mgr)
264293
case configv1.GCPPlatformType:
265-
setupReconcilers(mgr, infra, platform, &gcpv1.GCPCluster{}, containerImages, applyClient, apiextensionsClient, managedNamespace)
294+
setupReconcilers(mgr, infra, platform, &gcpv1.GCPCluster{}, containerImages, providerImages, applyClient, apiextensionsClient, managedNamespace)
266295
setupWebhooks(mgr)
267296
case configv1.AzurePlatformType:
268297
azureCloudEnvironment := getAzureCloudEnvironment(infra.Status.PlatformStatus)
@@ -272,20 +301,20 @@ func setupPlatformReconcilers(mgr manager.Manager, infra *configv1.Infrastructur
272301
isUnsupportedPlatform = true
273302
} else {
274303
// The ClusterOperator Controller must run in all cases.
275-
setupReconcilers(mgr, infra, platform, &azurev1.AzureCluster{}, containerImages, applyClient, apiextensionsClient, managedNamespace)
304+
setupReconcilers(mgr, infra, platform, &azurev1.AzureCluster{}, containerImages, providerImages, applyClient, apiextensionsClient, managedNamespace)
276305
setupWebhooks(mgr)
277306
}
278307
case configv1.PowerVSPlatformType:
279-
setupReconcilers(mgr, infra, platform, &ibmpowervsv1.IBMPowerVSCluster{}, containerImages, applyClient, apiextensionsClient, managedNamespace)
308+
setupReconcilers(mgr, infra, platform, &ibmpowervsv1.IBMPowerVSCluster{}, containerImages, providerImages, applyClient, apiextensionsClient, managedNamespace)
280309
setupWebhooks(mgr)
281310
case configv1.VSpherePlatformType:
282-
setupReconcilers(mgr, infra, platform, &vspherev1.VSphereCluster{}, containerImages, applyClient, apiextensionsClient, managedNamespace)
311+
setupReconcilers(mgr, infra, platform, &vspherev1.VSphereCluster{}, containerImages, providerImages, applyClient, apiextensionsClient, managedNamespace)
283312
setupWebhooks(mgr)
284313
case configv1.OpenStackPlatformType:
285-
setupReconcilers(mgr, infra, platform, &openstackv1.OpenStackCluster{}, containerImages, applyClient, apiextensionsClient, managedNamespace)
314+
setupReconcilers(mgr, infra, platform, &openstackv1.OpenStackCluster{}, containerImages, providerImages, applyClient, apiextensionsClient, managedNamespace)
286315
setupWebhooks(mgr)
287316
case configv1.BareMetalPlatformType:
288-
setupReconcilers(mgr, infra, platform, &metal3v1.Metal3Cluster{}, containerImages, applyClient, apiextensionsClient, managedNamespace)
317+
setupReconcilers(mgr, infra, platform, &metal3v1.Metal3Cluster{}, containerImages, providerImages, applyClient, apiextensionsClient, managedNamespace)
289318
setupWebhooks(mgr)
290319
default:
291320
klog.Infof("Detected platform %q is not supported, skipping capi controllers setup", platform)
@@ -297,7 +326,7 @@ func setupPlatformReconcilers(mgr manager.Manager, infra *configv1.Infrastructur
297326
setupClusterOperatorController(mgr, platform, managedNamespace, isUnsupportedPlatform)
298327
}
299328

300-
func setupReconcilers(mgr manager.Manager, infra *configv1.Infrastructure, platform configv1.PlatformType, infraClusterObject client.Object, containerImages map[string]string, applyClient *kubernetes.Clientset, apiextensionsClient *apiextensionsclient.Clientset, managedNamespace string) {
329+
func setupReconcilers(mgr manager.Manager, infra *configv1.Infrastructure, platform configv1.PlatformType, infraClusterObject client.Object, containerImages map[string]string, providerImages map[string]providerimages.ProviderImageManifests, applyClient *kubernetes.Clientset, apiextensionsClient *apiextensionsclient.Clientset, managedNamespace string) {
301330
if err := (&corecluster.CoreClusterController{
302331
ClusterOperatorStatusClient: getClusterOperatorStatusClient(mgr, "cluster-capi-operator-cluster-resource-controller", platform, managedNamespace),
303332
Cluster: &clusterv1.Cluster{},
@@ -333,6 +362,7 @@ func setupReconcilers(mgr manager.Manager, infra *configv1.Infrastructure, platf
333362
Platform: platform,
334363
ApplyClient: applyClient,
335364
APIExtensionsClient: apiextensionsClient,
365+
ProviderImages: providerImages,
336366
}).SetupWithManager(mgr); err != nil {
337367
klog.Error(err, "unable to create capi installer controller", "controller", "CAPIInstaller")
338368
os.Exit(1)

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ require (
1717
github.com/google/go-cmp v0.7.0
1818
github.com/google/uuid v1.6.0
1919
github.com/gophercloud/gophercloud/v2 v2.9.0
20-
github.com/klauspost/compress v1.18.0
20+
github.com/klauspost/compress v1.18.1
2121
github.com/metal3-io/cluster-api-provider-metal3/api v1.11.2
2222
github.com/onsi/ginkgo/v2 v2.27.2
2323
github.com/onsi/gomega v1.38.2
@@ -27,7 +27,7 @@ require (
2727
github.com/openshift/library-go v0.0.0-20251112091634-ab97ebb73f0f
2828
github.com/pkg/errors v0.9.1
2929
github.com/spf13/pflag v1.0.10
30-
golang.org/x/tools v0.38.0
30+
golang.org/x/tools v0.39.0
3131
gopkg.in/yaml.v2 v2.4.0
3232
k8s.io/api v0.34.1
3333
k8s.io/apiextensions-apiserver v0.34.1
@@ -154,6 +154,7 @@ require (
154154
github.com/google/btree v1.1.3 // indirect
155155
github.com/google/cel-go v0.26.0 // indirect
156156
github.com/google/gnostic-models v0.7.0 // indirect
157+
github.com/google/go-containerregistry v0.20.7 // indirect
157158
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 // indirect
158159
github.com/gophercloud/utils/v2 v2.0.0-20241220104409-2e0af06694a1 // indirect
159160
github.com/gordonklaus/ineffassign v0.1.0 // indirect
@@ -291,7 +292,7 @@ require (
291292
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
292293
golang.org/x/mod v0.30.0 // indirect
293294
golang.org/x/net v0.47.0 // indirect
294-
golang.org/x/oauth2 v0.32.0 // indirect
295+
golang.org/x/oauth2 v0.33.0 // indirect
295296
golang.org/x/sync v0.18.0 // indirect
296297
golang.org/x/sys v0.38.0 // indirect
297298
golang.org/x/term v0.37.0 // indirect

manifests/0000_30_cluster-api_11_deployment.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ spec:
3535
env:
3636
- name: RELEASE_VERSION
3737
value: "0.0.1-snapshot"
38+
- name: PULL_SECRET_PATH
39+
value: "/var/run/secrets/pull-secret/config.json"
40+
- name: PROVIDER_IMAGE_DIR
41+
value: "/var/lib/provider-images"
3842
ports:
3943
- containerPort: 9443
4044
name: webhook-server
@@ -53,6 +57,11 @@ spec:
5357
- name: cert
5458
mountPath: /tmp/k8s-webhook-server/serving-certs
5559
readOnly: true
60+
- name: pull-secret
61+
mountPath: /var/run/secrets/pull-secret
62+
readOnly: true
63+
- name: provider-images
64+
mountPath: /var/lib/provider-images
5665
- name: machine-api-migration
5766
image: registry.ci.openshift.org/openshift:cluster-capi-operator
5867
command:
@@ -88,3 +97,9 @@ spec:
8897
secret:
8998
defaultMode: 420
9099
secretName: cluster-capi-operator-webhook-service-cert
100+
- name: pull-secret
101+
hostPath:
102+
path: /var/lib/kubelet/config.json
103+
type: File
104+
- name: provider-images
105+
emptyDir: {}

pkg/controllers/capiinstaller/capi_installer_controller.go

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ limitations under the License.
1616
package capiinstaller
1717

1818
import (
19+
"bytes"
1920
"context"
2021
"errors"
2122
"fmt"
23+
"io"
24+
"maps"
25+
"os"
2226
"regexp"
2327
"strings"
2428

@@ -35,7 +39,6 @@ import (
3539
"k8s.io/apimachinery/pkg/runtime"
3640
"k8s.io/apimachinery/pkg/runtime/serializer"
3741
"k8s.io/utils/clock"
38-
"k8s.io/utils/strings/slices"
3942

4043
"k8s.io/client-go/kubernetes"
4144
"k8s.io/client-go/rest"
@@ -47,6 +50,7 @@ import (
4750
configv1 "github.com/openshift/api/config/v1"
4851
"github.com/openshift/cluster-capi-operator/pkg/metadata"
4952
"github.com/openshift/cluster-capi-operator/pkg/operatorstatus"
53+
"github.com/openshift/cluster-capi-operator/pkg/providerimages"
5054
"github.com/openshift/library-go/pkg/operator/events"
5155
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
5256
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
@@ -89,6 +93,7 @@ type CapiInstallerController struct {
8993
Platform configv1.PlatformType
9094
ApplyClient *kubernetes.Clientset
9195
APIExtensionsClient *apiextensionsclient.Clientset
96+
ProviderImages map[string]providerimages.ProviderImageManifests
9297
}
9398

9499
// Reconcile reconciles the cluster-api ClusterOperator object.
@@ -185,6 +190,38 @@ func (r *CapiInstallerController) reconcile(ctx context.Context, log logr.Logger
185190
log.Info("finished reconciling CAPI provider", "name", name)
186191
}
187192

193+
providerImages := func(yield func(providerImage providerimages.ProviderImageManifests) bool) {
194+
for providerImage := range maps.Values(r.ProviderImages) {
195+
if providerImage.Type == "core" {
196+
if !yield(providerImage) {
197+
return
198+
}
199+
}
200+
201+
if providerImage.Type == "infrastructure" && providerImage.OCPPlatform == string(r.Platform) {
202+
if !yield(providerImage) {
203+
return
204+
}
205+
}
206+
}
207+
}
208+
209+
for providerImage := range providerImages {
210+
reader, err := providerManifestReader(providerImage)
211+
if err != nil {
212+
return fmt.Errorf("failed to create provider manifest reader: %w", err)
213+
}
214+
215+
yamlManifests, err := extractManifests(reader)
216+
if err != nil {
217+
return fmt.Errorf("failed to extract manifests from provider manifest: %w", err)
218+
}
219+
220+
if err := r.applyProviderComponents(ctx, yamlManifests); err != nil {
221+
return fmt.Errorf("failed to apply provider components: %w", err)
222+
}
223+
}
224+
188225
return nil
189226
}
190227

@@ -283,6 +320,11 @@ func getProviderComponents(scheme *runtime.Scheme, components []string) ([]strin
283320
customResourceDefinitionAssets := make(map[string]string)
284321

285322
for i, m := range components {
323+
// Skip empty manifests.
324+
if strings.TrimSpace(m) == "" {
325+
continue
326+
}
327+
286328
// Parse the YAML manifests into unstructure objects.
287329
u, err := yamlToUnstructured(scheme, m)
288330
if err != nil {
@@ -409,7 +451,12 @@ func (r *CapiInstallerController) SetupWithManager(mgr ctrl.Manager) error {
409451
// clusterctl Provider Contract - Components YAML file contract defined at:
410452
// https://github.com/kubernetes-sigs/cluster-api/blob/a36712e28bf5d54e398ea84cb3e20102c0499426/docs/book/src/clusterctl/provider-contract.md?plain=1#L157-L162
411453
func (r *CapiInstallerController) extractProviderComponents(cm corev1.ConfigMap) ([]string, error) {
412-
yamlManifests, err := extractManifests(cm)
454+
reader, err := configMapReader(cm)
455+
if err != nil {
456+
return nil, fmt.Errorf("failed to create config map reader: %w", err)
457+
}
458+
459+
yamlManifests, err := extractManifests(reader)
413460
if err != nil {
414461
return nil, fmt.Errorf("failed to extract manifests from configMap: %w", err)
415462
}
@@ -427,46 +474,41 @@ func (r *CapiInstallerController) extractProviderComponents(cm corev1.ConfigMap)
427474
return replacedYamlManifests, nil
428475
}
429476

430-
// extractManifests extracts and processes component manifests from given ConfigMap.
431-
// If the data is in compressed binary form, it decompresses them.
432-
func extractManifests(cm corev1.ConfigMap) ([]string, error) {
433-
data, hasData := cm.Data["components"]
434-
binaryData, hasBinary := cm.BinaryData["components-zstd"]
477+
func configMapReader(cm corev1.ConfigMap) (io.Reader, error) {
478+
if data, ok := cm.Data["components"]; ok {
479+
return strings.NewReader(data), nil
480+
}
435481

436-
if !(hasBinary || hasData) {
437-
return nil, errEmptyProviderConfigMap
482+
if binaryData, ok := cm.BinaryData["components-zstd"]; ok {
483+
return zstd.NewReader(bytes.NewReader(binaryData))
438484
}
439485

440-
if hasBinary {
441-
decoder, err := zstd.NewReader(nil)
442-
if err != nil {
443-
return nil, fmt.Errorf("failed to create zstd reader: %w", err)
444-
}
486+
return nil, errEmptyProviderConfigMap
487+
}
445488

446-
decoded, err := decoder.DecodeAll(binaryData, []byte{})
447-
if err != nil {
448-
return nil, fmt.Errorf("failed to decompress components: %w", err)
449-
}
489+
func providerManifestReader(providerImage providerimages.ProviderImageManifests) (io.Reader, error) {
490+
return os.Open(providerImage.ManifestsPath)
491+
}
450492

451-
data = string(decoded)
493+
// extractManifests extracts and processes component manifests from given ConfigMap.
494+
// If the data is in compressed binary form, it decompresses them.
495+
func extractManifests(reader io.Reader) ([]string, error) {
496+
data, err := io.ReadAll(reader)
497+
if err != nil {
498+
return nil, fmt.Errorf("failed to read components: %w", err)
452499
}
453500

454501
// Certain provider components have drone/envsubst environment variables interpolated within the manifest.
455502
// Substitute them with the value defined in the environment variable (see setFeatureGatesEnvVars()).
456503
// If that's not set, fallback to the default value defined in the template.
457-
components, err := envsubst.EvalEnv(data)
504+
components, err := envsubst.EvalEnv(string(data))
458505
if err != nil {
459506
return nil, fmt.Errorf("failed to substitute environment variables in component manifests: %w", err)
460507
}
461508

462509
// Split multi-document YAML into single manifests.
463510
yamlManifests := regexp.MustCompile("(?m)^---$").Split(components, -1)
464511

465-
// Filter out empty manifests, e.g. when a bundle starts with '---'
466-
yamlManifests = slices.Filter(nil, yamlManifests, func(m string) bool {
467-
return strings.TrimSpace(m) != ""
468-
})
469-
470512
return yamlManifests, nil
471513
}
472514

pkg/controllers/capiinstaller/capi_installer_controller_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,12 @@ var _ = Describe("extractManifests", func() {
9393

9494
for _, tc := range testCases {
9595
It(tc.name, func() {
96-
manifests, err := extractManifests(tc.configMap)
96+
reader, err := configMapReader(tc.configMap)
97+
if err != nil {
98+
Expect(err).To(MatchError(errEmptyProviderConfigMap))
99+
}
100+
101+
manifests, err := extractManifests(reader)
97102

98103
if tc.expectedError != nil {
99104
Expect(err).To(MatchError(tc.expectedError))

0 commit comments

Comments
 (0)