Skip to content
Draft
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
13 changes: 12 additions & 1 deletion Containerfile.operator
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,26 @@ RUN make hypershift \
&& make hypershift-no-cgo \
&& make hypershift-operator \
&& make product-cli \
&& make karpenter-operator
&& make karpenter-operator \
&& make control-plane-operator \
&& make control-plane-pki-operator

FROM registry.access.redhat.com/ubi9/ubi-minimal:9.6-1760515502
COPY --from=builder /hypershift/bin/hypershift \
/hypershift/bin/hypershift-no-cgo \
/hypershift/bin/hcp \
/hypershift/bin/hypershift-operator \
/hypershift/bin/karpenter-operator \
/hypershift/bin/control-plane-operator \
/hypershift/bin/control-plane-pki-operator \
/usr/bin/

RUN cd /usr/bin && \
ln -s control-plane-operator ignition-server && \
ln -s control-plane-operator konnectivity-socks5-proxy && \
ln -s control-plane-operator availability-prober && \
ln -s control-plane-operator token-minter

ENTRYPOINT ["/usr/bin/hypershift"]

LABEL name="multicluster-engine/hypershift-operator"
Expand All @@ -41,4 +51,5 @@ LABEL io.openshift.hypershift.control-plane-operator-applies-management-kas-netw
LABEL io.openshift.hypershift.restricted-psa=true
LABEL io.openshift.hypershift.control-plane-pki-operator-signs-csrs=true
LABEL io.openshift.hypershift.hosted-cluster-config-operator-reports-node-count=true
LABEL io.openshift.hypershift.control-plane-operator-supports-kas-custom-kubeconfig=true
LABEL io.openshift.hypershift.control-plane-operator.v2-isdefault=true
13 changes: 12 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,26 @@ RUN make hypershift \
&& make hypershift-no-cgo \
&& make hypershift-operator \
&& make product-cli \
&& make karpenter-operator
&& make karpenter-operator \
&& make control-plane-operator \
&& make control-plane-pki-operator

FROM registry.access.redhat.com/ubi9:latest
COPY --from=builder /hypershift/bin/hypershift \
/hypershift/bin/hypershift-no-cgo \
/hypershift/bin/hcp \
/hypershift/bin/hypershift-operator \
/hypershift/bin/karpenter-operator \
/hypershift/bin/control-plane-operator \
/hypershift/bin/control-plane-pki-operator \
/usr/bin/

RUN cd /usr/bin && \
ln -s control-plane-operator ignition-server && \
ln -s control-plane-operator konnectivity-socks5-proxy && \
ln -s control-plane-operator availability-prober && \
ln -s control-plane-operator token-minter

ENTRYPOINT ["/usr/bin/hypershift"]

LABEL io.openshift.hypershift.control-plane-operator-subcommands=true
Expand All @@ -32,4 +42,5 @@ LABEL io.openshift.hypershift.control-plane-operator-applies-management-kas-netw
LABEL io.openshift.hypershift.restricted-psa=true
LABEL io.openshift.hypershift.control-plane-pki-operator-signs-csrs=true
LABEL io.openshift.hypershift.hosted-cluster-config-operator-reports-node-count=true
LABEL io.openshift.hypershift.control-plane-operator-supports-kas-custom-kubeconfig=true
LABEL io.openshift.hypershift.control-plane-operator.v2-isdefault=true
46 changes: 42 additions & 4 deletions support/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"regexp"
"sort"
"strings"
"sync"
"time"

hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
Expand Down Expand Up @@ -627,17 +628,43 @@ func GetPullSecretBytes(ctx context.Context, c client.Client, hc *hyperv1.Hosted
return pullSecretBytes, nil
}

// cpoBinaryPath is the expected location of the control-plane-operator binary
const cpoBinaryPath = "/usr/bin/control-plane-operator"

var (
cpoBinaryExistsOnce sync.Once
cpoBinaryExistsResult bool
// cpoBinaryExistsFunc is used for testing to override the binary existence check.
// When nil, the actual file system check is performed.
cpoBinaryExistsFunc func() bool
)

// cpoBinaryExistsInHOImage checks if the control-plane-operator binary exists in
// the current container image. This is used to determine if the HO image can be
// used as the CPO image for 4.20+ clusters. The result is cached after the first check.
func cpoBinaryExistsInHOImage() bool {
if cpoBinaryExistsFunc != nil {
return cpoBinaryExistsFunc()
}
cpoBinaryExistsOnce.Do(func() {
_, err := os.Stat(cpoBinaryPath)
cpoBinaryExistsResult = err == nil
})
return cpoBinaryExistsResult
}

// GetControlPlaneOperatorImage resolves the appropriate control plane operator
// image based on the following order of precedence (from most to least
// preferred):
//
// 1. The image specified by the ControlPlaneOperatorImageAnnotation on the
// HostedCluster resource itself
// 2. The hypershift image specified in the release payload indicated by the
// 2. If CPO overrides are enabled, the override image for the platform and version
// 3. For release versions 4.20+, the hypershift-operator's own image (if it
// contains the control-plane-operator binary)
// 4. The hypershift image specified in the release payload indicated by the
// HostedCluster's release field
// 3. The hypershift-operator's own image for release versions 4.9 and 4.10
// 4. The registry.ci.openshift.org/hypershift/hypershift:4.8 image for release
// version 4.8
// 5. The hypershift-operator's own image for release versions 4.9+
//
// If no image can be found according to these rules, an error is returned.
func GetControlPlaneOperatorImage(ctx context.Context, hc *hyperv1.HostedCluster, releaseProvider releaseinfo.Provider, hypershiftOperatorImage string, pullSecret []byte) (string, error) {
Expand All @@ -659,6 +686,17 @@ func GetControlPlaneOperatorImage(ctx context.Context, hc *hyperv1.HostedCluster
}
}

// For 4.20+, use the hypershift-operator image directly if it contains the
// control-plane-operator binary. This enables faster feature delivery and
// simplified hotfix processes, as the CPO is delivered with the HO rather
// than being tied to the OCP release payload.
minVersionForHOImage := semver.Version{Major: 4, Minor: 20, Patch: 0}
// Compare only Major.Minor.Patch, ignoring pre-release info (e.g., 4.20.0-rc.1 should match >= 4.20.0)
versionWithoutPrerelease := semver.Version{Major: version.Major, Minor: version.Minor, Patch: version.Patch}
if versionWithoutPrerelease.GTE(minVersionForHOImage) && cpoBinaryExistsInHOImage() {
return hypershiftOperatorImage, nil
}

if hypershiftImage, exists := releaseInfo.ComponentImages()["hypershift"]; exists {
return hypershiftImage, nil
}
Expand Down
167 changes: 167 additions & 0 deletions support/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (

hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
"github.com/openshift/hypershift/support/api"
"github.com/openshift/hypershift/support/releaseinfo"

imagev1 "github.com/openshift/api/image/v1"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -1009,3 +1012,167 @@ func TestCountAvailableNodes(t *testing.T) {
})
}
}

// testReleaseProvider is a simple fake release provider for testing GetControlPlaneOperatorImage
type testReleaseProvider struct {
version string
components map[string]string
}

func (f *testReleaseProvider) Lookup(_ context.Context, _ string, _ []byte) (*releaseinfo.ReleaseImage, error) {
releaseImage := &releaseinfo.ReleaseImage{
ImageStream: &imagev1.ImageStream{
ObjectMeta: metav1.ObjectMeta{Name: f.version},
Spec: imagev1.ImageStreamSpec{},
},
}
for name, image := range f.components {
releaseImage.ImageStream.Spec.Tags = append(releaseImage.ImageStream.Spec.Tags, imagev1.TagReference{
Name: name,
From: &corev1.ObjectReference{Name: image},
})
}
return releaseImage, nil
}

func TestGetControlPlaneOperatorImage(t *testing.T) {
const (
hoImage = "quay.io/hypershift/hypershift-operator:latest"
payloadCPOImage = "quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:abc123"
annotationCPOImage = "quay.io/custom/cpo:v1"
)

testCases := []struct {
name string
version string
hostedClusterAnnotation map[string]string
payloadHasHypershift bool
cpoBinaryExists bool
expectedImage string
}{
{
name: "When annotation is set it should use annotation image",
version: "4.20.0",
hostedClusterAnnotation: map[string]string{
hyperv1.ControlPlaneOperatorImageAnnotation: annotationCPOImage,
},
payloadHasHypershift: true,
cpoBinaryExists: true,
expectedImage: annotationCPOImage,
},
{
name: "When version is 4.20 and CPO binary exists it should use HO image",
version: "4.20.0",
payloadHasHypershift: true,
cpoBinaryExists: true,
expectedImage: hoImage,
},
{
name: "When version is 4.21 and CPO binary exists it should use HO image",
version: "4.21.0",
payloadHasHypershift: true,
cpoBinaryExists: true,
expectedImage: hoImage,
},
{
name: "When version is 4.22 and CPO binary exists it should use HO image",
version: "4.22.5",
payloadHasHypershift: true,
cpoBinaryExists: true,
expectedImage: hoImage,
},
{
name: "When version is 4.20 but CPO binary does not exist it should use payload image",
version: "4.20.0",
payloadHasHypershift: true,
cpoBinaryExists: false,
expectedImage: payloadCPOImage,
},
{
name: "When version is 4.19 with payload hypershift it should use payload image",
version: "4.19.0",
payloadHasHypershift: true,
cpoBinaryExists: true,
expectedImage: payloadCPOImage,
},
{
name: "When version is 4.18 with payload hypershift it should use payload image",
version: "4.18.5",
payloadHasHypershift: true,
cpoBinaryExists: true,
expectedImage: payloadCPOImage,
},
{
name: "When version is 4.14 with payload hypershift it should use payload image",
version: "4.14.0",
payloadHasHypershift: true,
cpoBinaryExists: true,
expectedImage: payloadCPOImage,
},
{
name: "When version is 4.19 without payload hypershift it should fallback to HO image",
version: "4.19.0",
payloadHasHypershift: false,
cpoBinaryExists: true,
expectedImage: hoImage,
},
{
name: "When version is 4.10 without payload hypershift it should fallback to HO image",
version: "4.10.0",
payloadHasHypershift: false,
cpoBinaryExists: true,
expectedImage: hoImage,
},
{
name: "When version is 5.0 and CPO binary exists it should use HO image",
version: "5.0.0",
payloadHasHypershift: true,
cpoBinaryExists: true,
expectedImage: hoImage,
},
{
name: "When version is 4.20.0-rc.1 and CPO binary exists it should use HO image",
version: "4.20.0-rc.1",
payloadHasHypershift: true,
cpoBinaryExists: true,
expectedImage: hoImage,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)

// Set the CPO binary existence for this test case
cpoBinaryExistsFunc = func() bool { return tc.cpoBinaryExists }
defer func() { cpoBinaryExistsFunc = nil }()

components := map[string]string{}
if tc.payloadHasHypershift {
components["hypershift"] = payloadCPOImage
}

releaseProvider := &testReleaseProvider{
version: tc.version,
components: components,
}

hc := &hyperv1.HostedCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cluster",
Namespace: "test-ns",
Annotations: tc.hostedClusterAnnotation,
},
Spec: hyperv1.HostedClusterSpec{
Release: hyperv1.Release{
Image: "quay.io/openshift-release-dev/ocp-release:4.20.0-x86_64",
},
},
}

image, err := GetControlPlaneOperatorImage(context.Background(), hc, releaseProvider, hoImage, nil)
g.Expect(err).ToNot(HaveOccurred())
g.Expect(image).To(Equal(tc.expectedImage))
})
}
}