diff --git a/pkg/controllers/revision/helpers_test.go b/pkg/controllers/revision/helpers_test.go index 77e65456c6..74a0bae892 100644 --- a/pkg/controllers/revision/helpers_test.go +++ b/pkg/controllers/revision/helpers_test.go @@ -18,9 +18,6 @@ package revision import ( "context" - "fmt" - "os" - "path/filepath" "slices" . "github.com/onsi/ginkgo/v2" @@ -52,7 +49,6 @@ func newManagerWrapper(providerImgs []providerimages.ProviderImageManifests) *ma // Clone so callers' slices are not mutated. imgs := slices.Clone(providerImgs) - ensureManifestPaths(imgs) mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: cl.Scheme(), @@ -185,35 +181,3 @@ func latestRevision(revisions []operatorv1alpha1.ClusterAPIInstallerRevision) op return latest } - -// configMapYAML returns a minimal valid ConfigMap YAML document. -func configMapYAML(name string) string { - return fmt.Sprintf(`apiVersion: v1 -kind: ConfigMap -metadata: - name: %s - namespace: default -data: - key: value`, name) -} - -// ensureManifestPaths creates manifest files for providers that don't already -// have a ManifestsPath set. Each provider's ContentID is used as the ConfigMap -// name so that different providers produce different revision contentIDs. -func ensureManifestPaths(providerImgs []providerimages.ProviderImageManifests) { - GinkgoHelper() - - manifestDir, err := os.MkdirTemp("", "revision-test-manifests") - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() { - Expect(os.RemoveAll(manifestDir)).To(Succeed()) - }) - - for i := range providerImgs { - if providerImgs[i].ManifestsPath == "" { - path := filepath.Join(manifestDir, providerImgs[i].Name+"-manifests.yaml") - Expect(os.WriteFile(path, []byte(configMapYAML(providerImgs[i].ContentID)), 0644)).To(Succeed()) - providerImgs[i].ManifestsPath = path - } - } -} diff --git a/pkg/controllers/revision/revision_controller_test.go b/pkg/controllers/revision/revision_controller_test.go index b71260be8d..ccf13aa4ee 100644 --- a/pkg/controllers/revision/revision_controller_test.go +++ b/pkg/controllers/revision/revision_controller_test.go @@ -41,62 +41,10 @@ var ( clusterAPI *operatorv1alpha1.ClusterAPI clusterOperator *configv1.ClusterOperator - defaultProviderImgs = []providerimages.ProviderImageManifests{ - { - ProviderMetadata: providerimages.ProviderMetadata{ - Name: "core", - InstallOrder: 10, - }, - ContentID: "core-content-id", - ImageRef: "registry.example.com/core@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", - Profile: "default", - }, - { - ProviderMetadata: providerimages.ProviderMetadata{ - Name: "infra-aws", - InstallOrder: 20, - OCPPlatform: configv1.AWSPlatformType, - }, - ContentID: "infra-aws-content-id", - ImageRef: "registry.example.com/infra-aws@sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210", - Profile: "default", - }, - } - - updatedProviderImgs = []providerimages.ProviderImageManifests{ - { - ProviderMetadata: providerimages.ProviderMetadata{ - Name: "core", - InstallOrder: 10, - }, - ContentID: "core-content-id-2", - ImageRef: "registry.example.com/core@sha256:1111111111111111111111111111111111111111111111111111111111111111", - Profile: "default", - }, - { - ProviderMetadata: providerimages.ProviderMetadata{ - Name: "infra-aws", - InstallOrder: 20, - OCPPlatform: configv1.AWSPlatformType, - }, - ContentID: "infra-aws-content-id-2", - ImageRef: "registry.example.com/infra-aws@sha256:2222222222222222222222222222222222222222222222222222222222222222", - Profile: "default", - }, - } - - nonMatchingProviderImgs = []providerimages.ProviderImageManifests{ - { - ProviderMetadata: providerimages.ProviderMetadata{ - Name: "infra-gcp", - InstallOrder: 20, - OCPPlatform: configv1.GCPPlatformType, - }, - ContentID: "infra-gcp-content-id", - ImageRef: "registry.example.com/infra-gcp@sha256:3333333333333333333333333333333333333333333333333333333333333333", - Profile: "default", - }, - } + // Provider image fixtures - set by setupProviderFixtures in BeforeSuite. + defaultProviderImgs []providerimages.ProviderImageManifests + updatedProviderImgs []providerimages.ProviderImageManifests + nonMatchingProviderImgs []providerimages.ProviderImageManifests ) const ( @@ -522,10 +470,9 @@ var _ = Describe("RevisionController error handling", Serial, func() { BeforeEach(func(ctx context.Context) { createFixtures(ctx) - // Clone provider images and ensure manifest paths for direct reconcile use. + // Clone provider images for direct reconcile use. providerImgs := make([]providerimages.ProviderImageManifests, len(defaultProviderImgs)) copy(providerImgs, defaultProviderImgs) - ensureManifestPaths(providerImgs) interceptorCl = interceptor.NewClient(cl, interceptor.Funcs{ SubResourcePatch: func(ctx context.Context, c client.Client, subResourceName string, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error { diff --git a/pkg/controllers/revision/suite_test.go b/pkg/controllers/revision/suite_test.go index 054fd88223..45134cd10c 100644 --- a/pkg/controllers/revision/suite_test.go +++ b/pkg/controllers/revision/suite_test.go @@ -23,6 +23,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/cluster-capi-operator/pkg/providerimages" "k8s.io/client-go/rest" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" @@ -62,8 +64,57 @@ var _ = BeforeSuite(func() { By("tearing down the test environment") Expect(test.StopEnvTest(testEnv)).To(Succeed()) }) + + By("setting up provider image fixtures") + setupProviderFixtures() }) +func setupProviderFixtures() { + tb := GinkgoTB() + + // Each provider uses its ContentID as the ConfigMap name so that + // different provider sets produce different revision contentIDs. + defaultProviderImgs = []providerimages.ProviderImageManifests{ + test.NewProviderImageManifests(tb, "core"). + WithContentID("core-content-id"). + WithImageRef("registry.example.com/core@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"). + WithManifests(test.ConfigMapYAML("core-content-id")). + Build(), + test.NewProviderImageManifests(tb, "infra-aws"). + WithInstallOrder(20). + WithPlatform(configv1.AWSPlatformType). + WithContentID("infra-aws-content-id"). + WithImageRef("registry.example.com/infra-aws@sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"). + WithManifests(test.ConfigMapYAML("infra-aws-content-id")). + Build(), + } + + updatedProviderImgs = []providerimages.ProviderImageManifests{ + test.NewProviderImageManifests(tb, "core"). + WithContentID("core-content-id-2"). + WithImageRef("registry.example.com/core@sha256:1111111111111111111111111111111111111111111111111111111111111111"). + WithManifests(test.ConfigMapYAML("core-content-id-2")). + Build(), + test.NewProviderImageManifests(tb, "infra-aws"). + WithInstallOrder(20). + WithPlatform(configv1.AWSPlatformType). + WithContentID("infra-aws-content-id-2"). + WithImageRef("registry.example.com/infra-aws@sha256:2222222222222222222222222222222222222222222222222222222222222222"). + WithManifests(test.ConfigMapYAML("infra-aws-content-id-2")). + Build(), + } + + nonMatchingProviderImgs = []providerimages.ProviderImageManifests{ + test.NewProviderImageManifests(tb, "infra-gcp"). + WithInstallOrder(20). + WithPlatform(configv1.GCPPlatformType). + WithContentID("infra-gcp-content-id"). + WithImageRef("registry.example.com/infra-gcp@sha256:3333333333333333333333333333333333333333333333333333333333333333"). + WithManifests(test.ConfigMapYAML("infra-gcp-content-id")). + Build(), + } +} + func kWithCtx(ctx context.Context) komega.Komega { return komega.New(cl).WithContext(ctx) } diff --git a/pkg/revisiongenerator/helpers_test.go b/pkg/revisiongenerator/helpers_test.go index 769da570ad..b90a9bf669 100644 --- a/pkg/revisiongenerator/helpers_test.go +++ b/pkg/revisiongenerator/helpers_test.go @@ -17,14 +17,12 @@ limitations under the License. package revisiongenerator import ( - "os" - "path/filepath" - "strings" "testing" . "github.com/onsi/gomega" "github.com/openshift/cluster-capi-operator/pkg/providerimages" + "github.com/openshift/cluster-capi-operator/pkg/test" ) func must[T any](value T, err error) func(g *WithT) T { @@ -36,28 +34,16 @@ func must[T any](value T, err error) func(g *WithT) T { } } -// writeManifestFile writes YAML content to a temp file and returns the path. -func writeManifestFile(t *testing.T, content string) string { - t.Helper() - dir := t.TempDir() - path := filepath.Join(dir, "manifests.yaml") - g := NewWithT(t) - g.Expect(os.WriteFile(path, []byte(content), 0644)).To(Succeed()) - - return path -} - // profile creates a ProviderImageManifests with the given fields and manifest // content written to a temp file. func profile(t *testing.T, name, imageRef, profileName, manifestContent string) providerimages.ProviderImageManifests { t.Helper() - return providerimages.ProviderImageManifests{ - ProviderMetadata: providerimages.ProviderMetadata{Name: name}, - ImageRef: imageRef, - Profile: profileName, - ManifestsPath: writeManifestFile(t, manifestContent), - } + return test.NewProviderImageManifests(t, name). + WithImageRef(imageRef). + WithProfile(profileName). + WithManifests(manifestContent). + Build() } // contentIDForProfiles computes the contentID for a set of profiles. @@ -76,5 +62,5 @@ func forInstall(g *WithT, rev RenderedRevision, releaseVersion string, revisionI // multiDoc joins YAML documents with the standard separator. func multiDoc(docs ...string) string { - return strings.Join(docs, "\n---\n") + return test.MultiDoc(docs...) } diff --git a/pkg/revisiongenerator/revision_test.go b/pkg/revisiongenerator/revision_test.go index cb1028fde8..88fffec080 100644 --- a/pkg/revisiongenerator/revision_test.go +++ b/pkg/revisiongenerator/revision_test.go @@ -25,6 +25,7 @@ import ( operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1" "github.com/openshift/cluster-capi-operator/pkg/providerimages" + "github.com/openshift/cluster-capi-operator/pkg/test" ) // Reusable YAML manifest fixtures. @@ -357,28 +358,27 @@ func TestNewRenderedRevision(t *testing.T) { // Manifest with envsubst variable; revision built from this should // produce the same contentID as one built from the expanded form. - pathWithVar := writeManifestFile(t, `apiVersion: v1 + provWithVar := test.NewProviderImageManifests(t, "p1"). + WithImageRef("img1"). + WithManifests(`apiVersion: v1 kind: ConfigMap metadata: name: cm data: - v: "${EXP_BOOTSTRAP_FORMAT_IGNITION}"`) - pathExpanded := writeManifestFile(t, `apiVersion: v1 + v: "${EXP_BOOTSTRAP_FORMAT_IGNITION}"`). + Build() + provExpanded := test.NewProviderImageManifests(t, "p1"). + WithImageRef("img1"). + WithManifests(`apiVersion: v1 kind: ConfigMap metadata: name: cm data: - v: "true"`) + v: "true"`). + Build() - p := providerimages.ProviderImageManifests{ - ProviderMetadata: providerimages.ProviderMetadata{Name: "p1"}, ImageRef: "img1", Profile: "default", - } - - p.ManifestsPath = pathWithVar - rev1 := must(NewRenderedRevision([]providerimages.ProviderImageManifests{p}))(g) - - p.ManifestsPath = pathExpanded - rev2 := must(NewRenderedRevision([]providerimages.ProviderImageManifests{p}))(g) + rev1 := must(NewRenderedRevision([]providerimages.ProviderImageManifests{provWithVar}))(g) + rev2 := must(NewRenderedRevision([]providerimages.ProviderImageManifests{provExpanded}))(g) id1 := must(rev1.ContentID())(g) id2 := must(rev2.ContentID())(g) diff --git a/pkg/test/provider_fixtures.go b/pkg/test/provider_fixtures.go new file mode 100644 index 0000000000..91afa92b18 --- /dev/null +++ b/pkg/test/provider_fixtures.go @@ -0,0 +1,242 @@ +/* +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 test + +import ( + "fmt" + "maps" + "os" + "path/filepath" + "strings" + "testing" + + configv1 "github.com/openshift/api/config/v1" + "github.com/openshift/cluster-capi-operator/pkg/providerimages" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sigs.k8s.io/yaml" +) + +// ProviderImageManifestsBuilder constructs a ProviderImageManifests for testing with sensible defaults. +// Manifest files are written to tb.TempDir() and cleaned up automatically. +type ProviderImageManifestsBuilder struct { + tb testing.TB + name string + imageRef string + profile string + contentID string + installOrder int + platform configv1.PlatformType + attributes map[string]string + selfImageRef string + manifests []string +} + +// NewProviderImageManifests returns a builder with sensible defaults for a test provider. +func NewProviderImageManifests(tb testing.TB, name string) *ProviderImageManifestsBuilder { + return &ProviderImageManifestsBuilder{ + tb: tb, + name: name, + imageRef: fmt.Sprintf("registry.example.com/%s@sha256:%s", name, SHA256Pad(name)), + profile: "default", + contentID: fmt.Sprintf("%s-content-id", name), + installOrder: 10, + } +} + +// WithImageRef sets a custom image reference. +func (b *ProviderImageManifestsBuilder) WithImageRef(ref string) *ProviderImageManifestsBuilder { + b.imageRef = ref + return b +} + +// WithProfile sets a custom profile name. +func (b *ProviderImageManifestsBuilder) WithProfile(profile string) *ProviderImageManifestsBuilder { + b.profile = profile + return b +} + +// WithContentID sets a custom content ID. +func (b *ProviderImageManifestsBuilder) WithContentID(id string) *ProviderImageManifestsBuilder { + b.contentID = id + return b +} + +// WithInstallOrder sets a custom install order. +func (b *ProviderImageManifestsBuilder) WithInstallOrder(order int) *ProviderImageManifestsBuilder { + b.installOrder = order + return b +} + +// WithPlatform sets the OCP platform type. +func (b *ProviderImageManifestsBuilder) WithPlatform(platform configv1.PlatformType) *ProviderImageManifestsBuilder { + b.platform = platform + return b +} + +// WithAttributes merges the given attributes into the provider attributes map. +func (b *ProviderImageManifestsBuilder) WithAttributes(attrs map[string]string) *ProviderImageManifestsBuilder { + if b.attributes == nil { + b.attributes = attrs + } else { + maps.Copy(b.attributes, attrs) + } + + return b +} + +// WithSelfImageRef sets the self image reference. +func (b *ProviderImageManifestsBuilder) WithSelfImageRef(ref string) *ProviderImageManifestsBuilder { + b.selfImageRef = ref + return b +} + +// WithManifests appends the given YAML documents to the manifest list. +func (b *ProviderImageManifestsBuilder) WithManifests(docs ...string) *ProviderImageManifestsBuilder { + b.manifests = append(b.manifests, docs...) + return b +} + +// Build constructs the ProviderImageManifests, writing any manifests to a temp file. +func (b *ProviderImageManifestsBuilder) Build() providerimages.ProviderImageManifests { + b.tb.Helper() + + p := providerimages.ProviderImageManifests{ + ProviderMetadata: providerimages.ProviderMetadata{ + Name: b.name, + InstallOrder: b.installOrder, + OCPPlatform: b.platform, + Attributes: b.attributes, + SelfImageRef: b.selfImageRef, + }, + ContentID: b.contentID, + ImageRef: b.imageRef, + Profile: b.profile, + } + + if len(b.manifests) > 0 { + content := MultiDoc(b.manifests...) + + dir := b.tb.TempDir() + path := filepath.Join(dir, b.name+"-manifests.yaml") + + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + b.tb.Fatalf("writing manifest file: %v", err) + } + + p.ManifestsPath = path + } + + return p +} + +// ConfigMapYAML returns a ConfigMap YAML document. If no data map is provided, +// a default of {"key": "value"} is used. +func ConfigMapYAML(name string, data ...map[string]string) string { + d := map[string]string{"key": "value"} + if len(data) > 0 { + d = data[0] + } + + var pairs strings.Builder + for k, v := range d { + fmt.Fprintf(&pairs, " %s: %s\n", k, v) + } + + return fmt.Sprintf(`apiVersion: v1 +kind: ConfigMap +metadata: + name: %s + namespace: default +data: +%s`, name, strings.TrimRight(pairs.String(), "\n")) +} + +// ClusterRoleYAML returns a minimal ClusterRole YAML document. +func ClusterRoleYAML(name string) string { + return fmt.Sprintf(`apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: %s +rules: []`, name) +} + +// NamespaceYAML returns a Namespace YAML document. +func NamespaceYAML(name string) string { + return fmt.Sprintf(`apiVersion: v1 +kind: Namespace +metadata: + name: %s`, name) +} + +// CRDToYAML marshals a CRD object to YAML. Use with GenerateCRD or +// GenerateSchemalessSpecStatusCRD from crdbuilder.go to create CRD fixtures +// for use with WithManifests. +func CRDToYAML(crd *apiextensionsv1.CustomResourceDefinition) string { + out, err := yaml.Marshal(crd) + if err != nil { + panic(fmt.Sprintf("marshalling CRD to YAML: %v", err)) + } + + return string(out) +} + +// DeploymentYAML returns a minimal Deployment YAML document with a single replica. +func DeploymentYAML(name string) string { + return fmt.Sprintf(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: %s + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: %s + template: + metadata: + labels: + app: %s + spec: + containers: + - name: test + image: registry.example.com/test:latest`, name, name, name) +} + +// SHA256Pad creates a deterministic 64-char hex string from a name (for fake digests). +func SHA256Pad(name string) string { + if name == "" { + // Otherwise we'll get into an infinite loop. + panic("SHA256Pad: name is required") + } + + padded := name + for len(padded) < 64 { + padded += name + } + + var hex strings.Builder + for _, c := range padded[:64] { + fmt.Fprintf(&hex, "%02x", c) + } + + return hex.String()[:64] +} + +// MultiDoc joins YAML documents with the standard separator. +func MultiDoc(docs ...string) string { + return strings.Join(docs, "\n---\n") +}