From 302867dbcac8e846b82cfcf794bef3dade53ae33 Mon Sep 17 00:00:00 2001
From: David Eads
Date: Fri, 4 Sep 2026 11:12:07 -0400
Subject: [PATCH 1/7] feat(api): add ingress defaultCertificate field and
synced condition
Add an optional defaultCertificate reference to IngressOperatorSpec (shared by
HostedCluster and HostedControlPlane) so managed services can supply a custom
TLS certificate for the default ingress controller:
- Add IngressDefaultCertificateReference with a CEL rule validating the secret
name as an RFC 1123 DNS subdomain, plus MinLength/MaxLength bounds.
- Add the IngressDefaultCertificateSynced condition type and its reasons
(SecretNotFound, InvalidCertificateSecret, PlatformNotSupported).
- Document that a guest cluster-admin override of the ingress controller
certificate takes precedence over this field.
- Regenerate deepcopy and the featuregated CRD manifests.
Signed-off-by: David Eads
Commit-Message-Assisted-by: Claude (via Claude Code)
Co-Authored-By: Claude Opus 4.8 (1M context)
(cherry picked from commit 2d054700e067b07e69248d2c88a28810f46a5595)
---
.../v1beta1/hostedcluster_conditions.go | 22 ++++++++++
api/hypershift/v1beta1/operator.go | 42 ++++++++++++++++++
.../v1beta1/zz_generated.deepcopy.go | 16 +++++++
.../AAA_ungated.yaml | 44 +++++++++++++++++++
.../ClusterUpdateAcceptRisks.yaml | 44 +++++++++++++++++++
.../ClusterVersionOperatorConfiguration.yaml | 44 +++++++++++++++++++
.../EtcdSharding.yaml | 44 +++++++++++++++++++
.../ExternalOIDC.yaml | 44 +++++++++++++++++++
.../ExternalOIDCExternalClaimsSourcing.yaml | 44 +++++++++++++++++++
...ernalOIDCWithUIDAndExtraClaimMappings.yaml | 44 +++++++++++++++++++
.../ExternalOIDCWithUpstreamParity.yaml | 44 +++++++++++++++++++
.../GCPPlatform.yaml | 44 +++++++++++++++++++
.../HCPEtcdBackup.yaml | 44 +++++++++++++++++++
...perShiftOnlyDynamicResourceAllocation.yaml | 44 +++++++++++++++++++
.../ImageStreamImportMode.yaml | 44 +++++++++++++++++++
.../IngressComponentRouteLabels.yaml | 44 +++++++++++++++++++
.../KMSEncryption.yaml | 44 +++++++++++++++++++
.../NetworkObservabilityInstall.yaml | 44 +++++++++++++++++++
.../OpenStack.yaml | 44 +++++++++++++++++++
.../TLSAdherence.yaml | 44 +++++++++++++++++++
.../TLSGroupPreferences.yaml | 44 +++++++++++++++++++
.../AAA_ungated.yaml | 44 +++++++++++++++++++
.../ClusterUpdateAcceptRisks.yaml | 44 +++++++++++++++++++
.../ClusterVersionOperatorConfiguration.yaml | 44 +++++++++++++++++++
.../EtcdSharding.yaml | 44 +++++++++++++++++++
.../ExternalOIDC.yaml | 44 +++++++++++++++++++
.../ExternalOIDCExternalClaimsSourcing.yaml | 44 +++++++++++++++++++
...ernalOIDCWithUIDAndExtraClaimMappings.yaml | 44 +++++++++++++++++++
.../ExternalOIDCWithUpstreamParity.yaml | 44 +++++++++++++++++++
.../GCPPlatform.yaml | 44 +++++++++++++++++++
.../HCPEtcdBackup.yaml | 44 +++++++++++++++++++
...perShiftOnlyDynamicResourceAllocation.yaml | 44 +++++++++++++++++++
.../ImageStreamImportMode.yaml | 44 +++++++++++++++++++
.../IngressComponentRouteLabels.yaml | 44 +++++++++++++++++++
.../KMSEncryption.yaml | 44 +++++++++++++++++++
.../NetworkObservabilityInstall.yaml | 44 +++++++++++++++++++
.../OpenStack.yaml | 44 +++++++++++++++++++
.../TLSAdherence.yaml | 44 +++++++++++++++++++
.../TLSGroupPreferences.yaml | 44 +++++++++++++++++++
39 files changed, 1664 insertions(+)
diff --git a/api/hypershift/v1beta1/hostedcluster_conditions.go b/api/hypershift/v1beta1/hostedcluster_conditions.go
index 2a00effd35d0..c1d3add9148a 100644
--- a/api/hypershift/v1beta1/hostedcluster_conditions.go
+++ b/api/hypershift/v1beta1/hostedcluster_conditions.go
@@ -266,6 +266,18 @@ const (
// cluster's shared ingress. Status reflects observed state: True means
// public endpoints are reachable, False means they are not.
PublicEndpointExposed ConditionType = "PublicEndpointExposed"
+
+ // IngressDefaultCertificateSynced indicates whether the user-provided default
+ // ingress certificate referenced by
+ // spec.operatorConfiguration.ingressOperator.defaultCertificate has been
+ // synced from the HostedCluster namespace into the control plane namespace.
+ // **True** means the referenced Secret was found, contains tls.crt and tls.key,
+ // and its data was synced.
+ // **False** means the referenced Secret is missing or malformed; in that case
+ // the previously synced certificate (or the auto-generated wildcard certificate)
+ // keeps serving and the HostedCluster does not become degraded.
+ // The condition is absent when no defaultCertificate is configured.
+ IngressDefaultCertificateSynced ConditionType = "IngressDefaultCertificateSynced"
)
// Reasons for PublicEndpointExposed condition.
@@ -340,6 +352,16 @@ const (
ReconcileErrorReason = "ReconcileError"
+ // IngressDefaultCertificateInvalidReason is used when the referenced default
+ // ingress certificate Secret exists but does not contain the required tls.crt
+ // and tls.key entries.
+ IngressDefaultCertificateInvalidReason = "InvalidCertificateSecret"
+
+ // IngressDefaultCertificatePlatformNotSupportedReason is used when a default
+ // ingress certificate is configured on a platform whose ingress controller does
+ // not consume it (e.g. IBM Cloud), so the certificate is intentionally not synced.
+ IngressDefaultCertificatePlatformNotSupportedReason = "PlatformNotSupported"
+
CloudResourcesCleanupSkippedReason = "CloudResourcesCleanupSkipped"
CloudResourcesDeletionTimedOutReason = "CloudResourcesDeletionTimedOut"
diff --git a/api/hypershift/v1beta1/operator.go b/api/hypershift/v1beta1/operator.go
index 5c4160fb257a..3d62df567350 100644
--- a/api/hypershift/v1beta1/operator.go
+++ b/api/hypershift/v1beta1/operator.go
@@ -283,4 +283,46 @@ type IngressOperatorSpec struct {
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Type=object
EndpointPublishingStrategy *operatorv1.EndpointPublishingStrategy `json:"endpointPublishingStrategy,omitempty"`
+
+ // defaultCertificate is a reference to a secret in the HostedCluster namespace
+ // that contains the default certificate served by the default ingress controller.
+ // When Routes don't specify their own certificate, defaultCertificate is used.
+ //
+ // The secret must contain the following keys and data:
+ // tls.crt: certificate file contents
+ // tls.key: key file contents
+ //
+ // When set, this certificate replaces the auto-generated wildcard certificate
+ // that is normally created by the control plane operator. The secret is synced
+ // from the HostedCluster namespace to the control plane, and then propagated
+ // to the hosted cluster's openshift-ingress namespace.
+ //
+ // When the referenced secret is updated, the new certificate data is
+ // automatically propagated to the hosted cluster.
+ //
+ // When not set, the control plane operator generates a wildcard certificate
+ // signed by the cluster's root CA.
+ //
+ // Note: a cluster-admin in the hosted cluster can override the default ingress
+ // controller's certificate directly. That override takes precedence and the
+ // certificate referenced here is no longer served.
+ //
+ // +optional
+ DefaultCertificate IngressDefaultCertificateReference `json:"defaultCertificate,omitzero"`
+}
+
+// IngressDefaultCertificateReference contains a reference to a TLS Secret
+// in the HostedCluster namespace used as the default serving certificate
+// for the ingress controller.
+type IngressDefaultCertificateReference struct {
+ // name is the name of the Secret containing tls.crt and tls.key.
+ // The Secret must exist in the same namespace as the HostedCluster.
+ // name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ // lowercase alphanumeric characters, '-' or '.', and start and end with an
+ // alphanumeric character.
+ // +required
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')",message="name must be a valid DNS subdomain name: contain no more than 253 characters, contain only lowercase alphanumeric characters, '-' or '.', and start and end with an alphanumeric character"
+ Name string `json:"name,omitempty"`
}
diff --git a/api/hypershift/v1beta1/zz_generated.deepcopy.go b/api/hypershift/v1beta1/zz_generated.deepcopy.go
index 202d09220b3e..95411870c24c 100644
--- a/api/hypershift/v1beta1/zz_generated.deepcopy.go
+++ b/api/hypershift/v1beta1/zz_generated.deepcopy.go
@@ -2992,6 +2992,21 @@ func (in *InPlaceUpgrade) DeepCopy() *InPlaceUpgrade {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IngressDefaultCertificateReference) DeepCopyInto(out *IngressDefaultCertificateReference) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressDefaultCertificateReference.
+func (in *IngressDefaultCertificateReference) DeepCopy() *IngressDefaultCertificateReference {
+ if in == nil {
+ return nil
+ }
+ out := new(IngressDefaultCertificateReference)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressOperatorSpec) DeepCopyInto(out *IngressOperatorSpec) {
*out = *in
@@ -3000,6 +3015,7 @@ func (in *IngressOperatorSpec) DeepCopyInto(out *IngressOperatorSpec) {
*out = new(operatorv1.EndpointPublishingStrategy)
(*in).DeepCopyInto(*out)
}
+ out.DefaultCertificate = in.DefaultCertificate
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressOperatorSpec.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml
index 5a80e8855788..65ade753df7a 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AAA_ungated.yaml
@@ -3512,6 +3512,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml
index 0019f6e9265c..daa662bff864 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml
@@ -3503,6 +3503,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml
index 2731aeb1783f..5fc1e206518d 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml
@@ -3523,6 +3523,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/EtcdSharding.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/EtcdSharding.yaml
index 789748e9af31..ab462a6a6406 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/EtcdSharding.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/EtcdSharding.yaml
@@ -3974,6 +3974,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml
index bad87ba4a5ec..76b68afad0a0 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDC.yaml
@@ -3835,6 +3835,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCExternalClaimsSourcing.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCExternalClaimsSourcing.yaml
index bc81bbde3552..c5bc94baeb57 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCExternalClaimsSourcing.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCExternalClaimsSourcing.yaml
@@ -4257,6 +4257,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml
index 5007539d13c2..3308767b222c 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml
@@ -3975,6 +3975,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml
index 8927e70d57e8..42ff4b483c8d 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml
@@ -3966,6 +3966,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml
index a4d8e8978bb7..30af4c430387 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml
@@ -3503,6 +3503,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HCPEtcdBackup.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HCPEtcdBackup.yaml
index e605d8d717f2..02d7b9c44ccb 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HCPEtcdBackup.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HCPEtcdBackup.yaml
@@ -3569,6 +3569,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml
index a5788f942ad1..248d12ff9ed0 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml
@@ -3525,6 +3525,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ImageStreamImportMode.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ImageStreamImportMode.yaml
index a6c56e2e16f9..b48cdeae0de8 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ImageStreamImportMode.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/ImageStreamImportMode.yaml
@@ -3521,6 +3521,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/IngressComponentRouteLabels.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/IngressComponentRouteLabels.yaml
index 7621c7ac5426..c95d4f3b35bd 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/IngressComponentRouteLabels.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/IngressComponentRouteLabels.yaml
@@ -3559,6 +3559,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/KMSEncryption.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/KMSEncryption.yaml
index 19a1635022d4..0d9d18f70a7e 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/KMSEncryption.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/KMSEncryption.yaml
@@ -3816,6 +3816,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/NetworkObservabilityInstall.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/NetworkObservabilityInstall.yaml
index 27bbad959941..0dba2535fbf4 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/NetworkObservabilityInstall.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/NetworkObservabilityInstall.yaml
@@ -3525,6 +3525,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml
index da5d461789fc..9d30c0820ef6 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/OpenStack.yaml
@@ -3503,6 +3503,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/TLSAdherence.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/TLSAdherence.yaml
index e0ee95e20a24..a08c4845ed7e 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/TLSAdherence.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/TLSAdherence.yaml
@@ -3543,6 +3543,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/TLSGroupPreferences.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/TLSGroupPreferences.yaml
index 77e582330b0f..13479ae9d926 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/TLSGroupPreferences.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/TLSGroupPreferences.yaml
@@ -3543,6 +3543,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AAA_ungated.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AAA_ungated.yaml
index 0e03e0871f8d..e96feba2997e 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AAA_ungated.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AAA_ungated.yaml
@@ -3398,6 +3398,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml
index fd3387a11e76..9a49905ae1f6 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterUpdateAcceptRisks.yaml
@@ -3389,6 +3389,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml
index fc28d58a46e7..d0129092b7e4 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ClusterVersionOperatorConfiguration.yaml
@@ -3409,6 +3409,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/EtcdSharding.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/EtcdSharding.yaml
index b792bf33fb2c..cf3773ba6a74 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/EtcdSharding.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/EtcdSharding.yaml
@@ -3860,6 +3860,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDC.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDC.yaml
index c8407db292a3..1fee7374fb57 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDC.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDC.yaml
@@ -3721,6 +3721,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCExternalClaimsSourcing.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCExternalClaimsSourcing.yaml
index f83f04619667..55d55e396a2e 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCExternalClaimsSourcing.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCExternalClaimsSourcing.yaml
@@ -4143,6 +4143,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml
index 5701b6bf05b8..78741f6b18fa 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUIDAndExtraClaimMappings.yaml
@@ -3861,6 +3861,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml
index f6583f3b618d..b4b9e61a6c79 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ExternalOIDCWithUpstreamParity.yaml
@@ -3852,6 +3852,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml
index b259cfd23cca..bbf133bfdd87 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml
@@ -3389,6 +3389,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HCPEtcdBackup.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HCPEtcdBackup.yaml
index 51b9f77d4793..6b3bc1c28cce 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HCPEtcdBackup.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HCPEtcdBackup.yaml
@@ -3455,6 +3455,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml
index 524e30325062..ed60f377bcad 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/HyperShiftOnlyDynamicResourceAllocation.yaml
@@ -3411,6 +3411,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ImageStreamImportMode.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ImageStreamImportMode.yaml
index 85d5daca0665..7ebd499694c7 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ImageStreamImportMode.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/ImageStreamImportMode.yaml
@@ -3407,6 +3407,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/IngressComponentRouteLabels.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/IngressComponentRouteLabels.yaml
index 8ec343606d4c..dc069f7d0bc2 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/IngressComponentRouteLabels.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/IngressComponentRouteLabels.yaml
@@ -3445,6 +3445,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/KMSEncryption.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/KMSEncryption.yaml
index ea6021a43610..0d0b487b3ac8 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/KMSEncryption.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/KMSEncryption.yaml
@@ -3702,6 +3702,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/NetworkObservabilityInstall.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/NetworkObservabilityInstall.yaml
index 5351222dbfd9..5e88df5b1ce6 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/NetworkObservabilityInstall.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/NetworkObservabilityInstall.yaml
@@ -3411,6 +3411,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/OpenStack.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/OpenStack.yaml
index 8dfb9b2b1c70..ff1845747e9d 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/OpenStack.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/OpenStack.yaml
@@ -3389,6 +3389,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/TLSAdherence.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/TLSAdherence.yaml
index 59a0b5a1e8cb..105f79ace7b0 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/TLSAdherence.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/TLSAdherence.yaml
@@ -3429,6 +3429,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/TLSGroupPreferences.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/TLSGroupPreferences.yaml
index 32d5a9acbd1d..a74abce2ddb7 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/TLSGroupPreferences.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/TLSGroupPreferences.yaml
@@ -3429,6 +3429,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
From fda1062c9f0cd3591195d3fb160e51518bb1a056 Mon Sep 17 00:00:00 2001
From: David Eads
Date: Fri, 4 Sep 2026 11:12:14 -0400
Subject: [PATCH 2/7] chore(api): regenerate CRDs, clients, deepcopy, and
vendor
Regenerated output for the ingress defaultCertificate field and the
IngressDefaultCertificateSynced condition:
- applyconfiguration clients for IngressDefaultCertificateReference and
IngressOperatorSpec.
- Installed CRD manifests for hostedclusters and hostedcontrolplanes.
- Vendored copy of the api module.
Signed-off-by: David Eads
Commit-Message-Assisted-by: Claude (via Claude Code)
Co-Authored-By: Claude Opus 4.8 (1M context)
(cherry picked from commit aa8e527f12cd070abbe71900329bc61ed31b1922)
---
.../ingressdefaultcertificatereference.go | 38 ++++++++++++++++
.../hypershift/v1beta1/ingressoperatorspec.go | 11 ++++-
client/applyconfiguration/utils.go | 2 +
...usters-Hypershift-CustomNoUpgrade.crd.yaml | 44 +++++++++++++++++++
...hostedclusters-Hypershift-Default.crd.yaml | 44 +++++++++++++++++++
...s-Hypershift-TechPreviewNoUpgrade.crd.yaml | 44 +++++++++++++++++++
...planes-Hypershift-CustomNoUpgrade.crd.yaml | 44 +++++++++++++++++++
...dcontrolplanes-Hypershift-Default.crd.yaml | 44 +++++++++++++++++++
...s-Hypershift-TechPreviewNoUpgrade.crd.yaml | 44 +++++++++++++++++++
.../v1beta1/hostedcluster_conditions.go | 22 ++++++++++
.../api/hypershift/v1beta1/operator.go | 42 ++++++++++++++++++
.../v1beta1/zz_generated.deepcopy.go | 16 +++++++
12 files changed, 394 insertions(+), 1 deletion(-)
create mode 100644 client/applyconfiguration/hypershift/v1beta1/ingressdefaultcertificatereference.go
diff --git a/client/applyconfiguration/hypershift/v1beta1/ingressdefaultcertificatereference.go b/client/applyconfiguration/hypershift/v1beta1/ingressdefaultcertificatereference.go
new file mode 100644
index 000000000000..4742bd895968
--- /dev/null
+++ b/client/applyconfiguration/hypershift/v1beta1/ingressdefaultcertificatereference.go
@@ -0,0 +1,38 @@
+/*
+
+
+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.
+*/
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1beta1
+
+// IngressDefaultCertificateReferenceApplyConfiguration represents a declarative configuration of the IngressDefaultCertificateReference type for use
+// with apply.
+type IngressDefaultCertificateReferenceApplyConfiguration struct {
+ Name *string `json:"name,omitempty"`
+}
+
+// IngressDefaultCertificateReferenceApplyConfiguration constructs a declarative configuration of the IngressDefaultCertificateReference type for use with
+// apply.
+func IngressDefaultCertificateReference() *IngressDefaultCertificateReferenceApplyConfiguration {
+ return &IngressDefaultCertificateReferenceApplyConfiguration{}
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *IngressDefaultCertificateReferenceApplyConfiguration) WithName(value string) *IngressDefaultCertificateReferenceApplyConfiguration {
+ b.Name = &value
+ return b
+}
diff --git a/client/applyconfiguration/hypershift/v1beta1/ingressoperatorspec.go b/client/applyconfiguration/hypershift/v1beta1/ingressoperatorspec.go
index 326782d2e7cd..f7d6fcde6f29 100644
--- a/client/applyconfiguration/hypershift/v1beta1/ingressoperatorspec.go
+++ b/client/applyconfiguration/hypershift/v1beta1/ingressoperatorspec.go
@@ -24,7 +24,8 @@ import (
// IngressOperatorSpecApplyConfiguration represents a declarative configuration of the IngressOperatorSpec type for use
// with apply.
type IngressOperatorSpecApplyConfiguration struct {
- EndpointPublishingStrategy *v1.EndpointPublishingStrategy `json:"endpointPublishingStrategy,omitempty"`
+ EndpointPublishingStrategy *v1.EndpointPublishingStrategy `json:"endpointPublishingStrategy,omitempty"`
+ DefaultCertificate *IngressDefaultCertificateReferenceApplyConfiguration `json:"defaultCertificate,omitempty"`
}
// IngressOperatorSpecApplyConfiguration constructs a declarative configuration of the IngressOperatorSpec type for use with
@@ -40,3 +41,11 @@ func (b *IngressOperatorSpecApplyConfiguration) WithEndpointPublishingStrategy(v
b.EndpointPublishingStrategy = &value
return b
}
+
+// WithDefaultCertificate sets the DefaultCertificate field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DefaultCertificate field is set to the value of the last call.
+func (b *IngressOperatorSpecApplyConfiguration) WithDefaultCertificate(value *IngressDefaultCertificateReferenceApplyConfiguration) *IngressOperatorSpecApplyConfiguration {
+ b.DefaultCertificate = value
+ return b
+}
diff --git a/client/applyconfiguration/utils.go b/client/applyconfiguration/utils.go
index 4ccc8f871cd4..477488a6f1b4 100644
--- a/client/applyconfiguration/utils.go
+++ b/client/applyconfiguration/utils.go
@@ -271,6 +271,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &hypershiftv1beta1.IBMCloudPlatformSpecApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("ImageContentSource"):
return &hypershiftv1beta1.ImageContentSourceApplyConfiguration{}
+ case v1beta1.SchemeGroupVersion.WithKind("IngressDefaultCertificateReference"):
+ return &hypershiftv1beta1.IngressDefaultCertificateReferenceApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("IngressOperatorSpec"):
return &hypershiftv1beta1.IngressOperatorSpecApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("InPlaceUpgrade"):
diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml
index f156d452b95f..92a31753e9d5 100644
--- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml
+++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-CustomNoUpgrade.crd.yaml
@@ -5615,6 +5615,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml
index 74258fffc51a..3febd06be751 100644
--- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml
+++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-Default.crd.yaml
@@ -4140,6 +4140,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml
index 8c64450711bb..ac3fbd6e8c17 100644
--- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml
+++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml
@@ -4737,6 +4737,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml
index 39722a50078f..7b5e606f7bd1 100644
--- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml
+++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-CustomNoUpgrade.crd.yaml
@@ -5501,6 +5501,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml
index b4d1ca982ba7..24c986526209 100644
--- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml
+++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-Default.crd.yaml
@@ -4026,6 +4026,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml
index d03d7e84424f..8ba429dabdc0 100644
--- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml
+++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml
@@ -4623,6 +4623,50 @@ spec:
ingressOperator specifies the configuration for the Ingress Operator in the hosted cluster.
This allows configuring how the default ingress controller endpoints are published.
properties:
+ defaultCertificate:
+ description: |-
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+ that contains the default certificate served by the default ingress controller.
+ When Routes don't specify their own certificate, defaultCertificate is used.
+
+ The secret must contain the following keys and data:
+ tls.crt: certificate file contents
+ tls.key: key file contents
+
+ When set, this certificate replaces the auto-generated wildcard certificate
+ that is normally created by the control plane operator. The secret is synced
+ from the HostedCluster namespace to the control plane, and then propagated
+ to the hosted cluster's openshift-ingress namespace.
+
+ When the referenced secret is updated, the new certificate data is
+ automatically propagated to the hosted cluster.
+
+ When not set, the control plane operator generates a wildcard certificate
+ signed by the cluster's root CA.
+
+ Note: a cluster-admin in the hosted cluster can override the default ingress
+ controller's certificate directly. That override takes precedence and the
+ certificate referenced here is no longer served.
+ properties:
+ name:
+ description: |-
+ name is the name of the Secret containing tls.crt and tls.key.
+ The Secret must exist in the same namespace as the HostedCluster.
+ name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ lowercase alphanumeric characters, '-' or '.', and start and end with an
+ alphanumeric character.
+ maxLength: 253
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: 'name must be a valid DNS subdomain name: contain
+ no more than 253 characters, contain only lowercase
+ alphanumeric characters, ''-'' or ''.'', and start
+ and end with an alphanumeric character'
+ rule: self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')
+ required:
+ - name
+ type: object
endpointPublishingStrategy:
description: |-
endpointPublishingStrategy is used to publish the default ingress controller endpoints.
diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_conditions.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_conditions.go
index 2a00effd35d0..c1d3add9148a 100644
--- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_conditions.go
+++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_conditions.go
@@ -266,6 +266,18 @@ const (
// cluster's shared ingress. Status reflects observed state: True means
// public endpoints are reachable, False means they are not.
PublicEndpointExposed ConditionType = "PublicEndpointExposed"
+
+ // IngressDefaultCertificateSynced indicates whether the user-provided default
+ // ingress certificate referenced by
+ // spec.operatorConfiguration.ingressOperator.defaultCertificate has been
+ // synced from the HostedCluster namespace into the control plane namespace.
+ // **True** means the referenced Secret was found, contains tls.crt and tls.key,
+ // and its data was synced.
+ // **False** means the referenced Secret is missing or malformed; in that case
+ // the previously synced certificate (or the auto-generated wildcard certificate)
+ // keeps serving and the HostedCluster does not become degraded.
+ // The condition is absent when no defaultCertificate is configured.
+ IngressDefaultCertificateSynced ConditionType = "IngressDefaultCertificateSynced"
)
// Reasons for PublicEndpointExposed condition.
@@ -340,6 +352,16 @@ const (
ReconcileErrorReason = "ReconcileError"
+ // IngressDefaultCertificateInvalidReason is used when the referenced default
+ // ingress certificate Secret exists but does not contain the required tls.crt
+ // and tls.key entries.
+ IngressDefaultCertificateInvalidReason = "InvalidCertificateSecret"
+
+ // IngressDefaultCertificatePlatformNotSupportedReason is used when a default
+ // ingress certificate is configured on a platform whose ingress controller does
+ // not consume it (e.g. IBM Cloud), so the certificate is intentionally not synced.
+ IngressDefaultCertificatePlatformNotSupportedReason = "PlatformNotSupported"
+
CloudResourcesCleanupSkippedReason = "CloudResourcesCleanupSkipped"
CloudResourcesDeletionTimedOutReason = "CloudResourcesDeletionTimedOut"
diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/operator.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/operator.go
index 5c4160fb257a..3d62df567350 100644
--- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/operator.go
+++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/operator.go
@@ -283,4 +283,46 @@ type IngressOperatorSpec struct {
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:Type=object
EndpointPublishingStrategy *operatorv1.EndpointPublishingStrategy `json:"endpointPublishingStrategy,omitempty"`
+
+ // defaultCertificate is a reference to a secret in the HostedCluster namespace
+ // that contains the default certificate served by the default ingress controller.
+ // When Routes don't specify their own certificate, defaultCertificate is used.
+ //
+ // The secret must contain the following keys and data:
+ // tls.crt: certificate file contents
+ // tls.key: key file contents
+ //
+ // When set, this certificate replaces the auto-generated wildcard certificate
+ // that is normally created by the control plane operator. The secret is synced
+ // from the HostedCluster namespace to the control plane, and then propagated
+ // to the hosted cluster's openshift-ingress namespace.
+ //
+ // When the referenced secret is updated, the new certificate data is
+ // automatically propagated to the hosted cluster.
+ //
+ // When not set, the control plane operator generates a wildcard certificate
+ // signed by the cluster's root CA.
+ //
+ // Note: a cluster-admin in the hosted cluster can override the default ingress
+ // controller's certificate directly. That override takes precedence and the
+ // certificate referenced here is no longer served.
+ //
+ // +optional
+ DefaultCertificate IngressDefaultCertificateReference `json:"defaultCertificate,omitzero"`
+}
+
+// IngressDefaultCertificateReference contains a reference to a TLS Secret
+// in the HostedCluster namespace used as the default serving certificate
+// for the ingress controller.
+type IngressDefaultCertificateReference struct {
+ // name is the name of the Secret containing tls.crt and tls.key.
+ // The Secret must exist in the same namespace as the HostedCluster.
+ // name must be a valid DNS subdomain name (RFC 1123): it must contain only
+ // lowercase alphanumeric characters, '-' or '.', and start and end with an
+ // alphanumeric character.
+ // +required
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=253
+ // +kubebuilder:validation:XValidation:rule="self.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?([.][a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')",message="name must be a valid DNS subdomain name: contain no more than 253 characters, contain only lowercase alphanumeric characters, '-' or '.', and start and end with an alphanumeric character"
+ Name string `json:"name,omitempty"`
}
diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go
index 202d09220b3e..95411870c24c 100644
--- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go
+++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go
@@ -2992,6 +2992,21 @@ func (in *InPlaceUpgrade) DeepCopy() *InPlaceUpgrade {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IngressDefaultCertificateReference) DeepCopyInto(out *IngressDefaultCertificateReference) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressDefaultCertificateReference.
+func (in *IngressDefaultCertificateReference) DeepCopy() *IngressDefaultCertificateReference {
+ if in == nil {
+ return nil
+ }
+ out := new(IngressDefaultCertificateReference)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressOperatorSpec) DeepCopyInto(out *IngressOperatorSpec) {
*out = *in
@@ -3000,6 +3015,7 @@ func (in *IngressOperatorSpec) DeepCopyInto(out *IngressOperatorSpec) {
*out = new(operatorv1.EndpointPublishingStrategy)
(*in).DeepCopyInto(*out)
}
+ out.DefaultCertificate = in.DefaultCertificate
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressOperatorSpec.
From 8e73abdef17ea25f5d583cae095061f7772a4d56 Mon Sep 17 00:00:00 2001
From: David Eads
Date: Fri, 4 Sep 2026 11:12:24 -0400
Subject: [PATCH 3/7] feat(hypershift-operator): sync ingress default
certificate and report status
Sync the user-provided ingress default certificate secret from the HostedCluster
namespace into the control plane namespace and surface the outcome:
- Report the IngressDefaultCertificateSynced condition via optimistic-lock
status patching; degrade gracefully (no reconcile failure) on a missing or
malformed source secret, preserving the previously synced certificate.
- Skip the sync on IBM Cloud (PlatformNotSupported) and when the ingress
capability is disabled, since no ingress controller consumes the certificate.
- When the certificate is unset, clean up the stale synced secret and condition,
short-circuiting the common path before any re-Get/DeepCopy.
- Annotate the referenced secret before validation and index HostedClusters by
the referenced secret name so a delete+recreate re-enqueues the owner promptly.
- Also invoke the sync from the legacy reconcile path.
- Add unit tests for the sync and the secret-referencing enqueue mapper.
Signed-off-by: David Eads
Commit-Message-Assisted-by: Claude (via Claude Code)
Co-Authored-By: Claude Opus 4.8 (1M context)
(cherry picked from commit d346163f6082cb480634fba7a50214791dcf28cd)
---
.../hostedcluster/enqueue_secret_test.go | 80 +++++
.../hostedcluster/hostedcluster_controller.go | 173 +++++++++-
.../ingress_default_cert_sync_test.go | 300 ++++++++++++++++++
.../hostedcluster/reconcile_legacy.go | 4 +
4 files changed, 555 insertions(+), 2 deletions(-)
create mode 100644 hypershift-operator/controllers/hostedcluster/enqueue_secret_test.go
create mode 100644 hypershift-operator/controllers/hostedcluster/ingress_default_cert_sync_test.go
diff --git a/hypershift-operator/controllers/hostedcluster/enqueue_secret_test.go b/hypershift-operator/controllers/hostedcluster/enqueue_secret_test.go
new file mode 100644
index 000000000000..ce9e9fc61bf5
--- /dev/null
+++ b/hypershift-operator/controllers/hostedcluster/enqueue_secret_test.go
@@ -0,0 +1,80 @@
+package hostedcluster
+
+import (
+ "context"
+ "testing"
+
+ . "github.com/onsi/gomega"
+
+ hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
+ "github.com/openshift/hypershift/support/api"
+ "github.com/openshift/hypershift/support/metrics"
+
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+// TestEnqueueHostedClustersForReferencedSecret ensures a referenced Secret maps
+// back to its owning HostedCluster even when the Secret carries no
+// referenced-resource annotation, which is the delete+recreate case: the
+// recreated Secret is a fresh object with no annotation, so it must be matched by
+// the HostedCluster spec reference instead.
+func TestEnqueueHostedClustersForReferencedSecret(t *testing.T) {
+ ctx := context.Background()
+
+ hc := &hyperv1.HostedCluster{
+ ObjectMeta: metav1.ObjectMeta{Name: "hc", Namespace: "clusters"},
+ Spec: hyperv1.HostedClusterSpec{
+ OperatorConfiguration: &hyperv1.OperatorConfiguration{
+ IngressOperator: &hyperv1.IngressOperatorSpec{
+ DefaultCertificate: hyperv1.IngressDefaultCertificateReference{Name: "my-cert"},
+ },
+ },
+ },
+ }
+
+ tests := []struct {
+ name string
+ secretName string
+ expected []reconcile.Request
+ }{
+ {
+ name: "When a recreated referenced secret has no annotation, it should enqueue the owning HostedCluster",
+ secretName: "my-cert",
+ expected: []reconcile.Request{
+ {NamespacedName: types.NamespacedName{Namespace: "clusters", Name: "hc"}},
+ },
+ },
+ {
+ name: "When a secret is not referenced by any HostedCluster, it should enqueue nothing",
+ secretName: "unrelated",
+ expected: nil,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewGomegaWithT(t)
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(api.Scheme).
+ WithIndex(&hyperv1.HostedCluster{}, hostedClusterIngressDefaultCertSecretIndex, indexHostedClusterByIngressDefaultCertSecret).
+ WithObjects(hc).
+ Build()
+ mapFn := enqueueHostedClustersFunc(metrics.MetricsSetTelemetry, "hypershift", fakeClient)
+
+ // A freshly recreated secret carries no referenced-resource annotation.
+ secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: tt.secretName, Namespace: "clusters"}}
+ requests := mapFn(ctx, secret)
+
+ if len(tt.expected) == 0 {
+ g.Expect(requests).To(BeEmpty())
+ return
+ }
+ g.Expect(requests).To(ConsistOf(tt.expected))
+ })
+ }
+}
diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go
index 32b22d1d8cc6..a2501f84a228 100644
--- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go
+++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go
@@ -24,6 +24,7 @@ import (
"net/netip"
"os"
"reflect"
+ "slices"
"strconv"
"strings"
"time"
@@ -42,7 +43,7 @@ import (
platformaws "github.com/openshift/hypershift/hypershift-operator/controllers/hostedcluster/internal/platform/aws"
"github.com/openshift/hypershift/hypershift-operator/controllers/hostedcluster/internal/proxy"
hcmetrics "github.com/openshift/hypershift/hypershift-operator/controllers/hostedcluster/metrics"
- validations "github.com/openshift/hypershift/hypershift-operator/controllers/hostedcluster/validations"
+ "github.com/openshift/hypershift/hypershift-operator/controllers/hostedcluster/validations"
"github.com/openshift/hypershift/hypershift-operator/controllers/manifests"
"github.com/openshift/hypershift/hypershift-operator/controllers/manifests/clusterapi"
"github.com/openshift/hypershift/hypershift-operator/controllers/manifests/controlplaneoperator"
@@ -67,6 +68,7 @@ import (
"github.com/openshift/hypershift/support/podspec"
"github.com/openshift/hypershift/support/releaseinfo"
"github.com/openshift/hypershift/support/secretproviderclass"
+ "github.com/openshift/hypershift/support/statuspatching"
"github.com/openshift/hypershift/support/supportedversion"
"github.com/openshift/hypershift/support/upsert"
hyperutil "github.com/openshift/hypershift/support/util"
@@ -240,6 +242,13 @@ func (r *HostedClusterReconciler) SetupWithManager(mgr ctrl.Manager, createOrUpd
r.now = metav1.Now
}
r.createOrUpdate = createOrUpdateWithAnnotationFactory(createOrUpdate)
+
+ // Index HostedClusters by the referenced ingress default certificate Secret name
+ // so the Secret watch can map a recreated (unannotated) Secret back to its owner.
+ if err := mgr.GetFieldIndexer().IndexField(context.Background(), &hyperv1.HostedCluster{}, hostedClusterIngressDefaultCertSecretIndex, indexHostedClusterByIngressDefaultCertSecret); err != nil {
+ return fmt.Errorf("failed to set up ingress default certificate secret index: %w", err)
+ }
+
// Set up watches for resource types the controller manages. The list basically
// tracks types of the resources in the clusterapi, controlplaneoperator, and
// ignitionserver manifests packages. Since we're receiving watch events across
@@ -1445,6 +1454,10 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques
return r.reconcileSSHKeySync(ctx, hcluster, createOrUpdate, controlPlaneNamespace.Name)
})
+ report.execute("IngressDefaultCertSync", nonCritical, func() error {
+ return r.reconcileIngressDefaultCertSync(ctx, hcluster, createOrUpdate, controlPlaneNamespace.Name)
+ })
+
report.execute("AdditionalTrustBundle", nonCritical, func() error {
return r.reconcileAdditionalTrustBundle(ctx, hcluster, createOrUpdate, controlPlaneNamespace.Name)
})
@@ -2099,6 +2112,114 @@ func (r *HostedClusterReconciler) reconcileSSHKeySync(
return err
}
+// reconcileIngressDefaultCertSync syncs the user-provided ingress default
+// certificate secret from the HostedCluster namespace to the control plane
+// namespace and reports the outcome via the IngressDefaultCertificateSynced
+// condition on the HostedCluster status.
+//
+// A missing or malformed source secret is user-correctable: it is surfaced
+// through the condition (set to False) without failing reconciliation, so the
+// previously synced certificate — or the auto-generated wildcard certificate —
+// keeps serving and the HostedCluster does not become degraded.
+func (r *HostedClusterReconciler) reconcileIngressDefaultCertSync(
+ ctx context.Context, hcluster *hyperv1.HostedCluster, createOrUpdate upsert.CreateOrUpdateFN,
+ controlPlaneNamespace string,
+) error {
+ log := ctrl.LoggerFrom(ctx)
+
+ // Skip when the ingress capability is disabled — no ingress controller will be
+ // created to consume the certificate — or when no certificate is configured.
+ if !capabilities.IsIngressCapabilityEnabled(hcluster.Spec.Capabilities) ||
+ hcluster.Spec.OperatorConfiguration == nil ||
+ hcluster.Spec.OperatorConfiguration.IngressOperator == nil ||
+ len(hcluster.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate.Name) == 0 {
+ // Fast path: with no synced condition the feature was never active for this
+ // HostedCluster, so there is nothing to clean up. This is the common case
+ // and runs on every reconcile, so avoid the re-Get/DeepCopy that PatchStatus
+ // would otherwise do.
+ if meta.FindStatusCondition(hcluster.Status.Conditions, string(hyperv1.IngressDefaultCertificateSynced)) == nil {
+ return nil
+ }
+ // The feature was active before; clean up the stale synced secret and drop
+ // the condition.
+ staleSecret := cpomanifests.ServiceProviderDefaultIngressServingCert(controlPlaneNamespace)
+ if _, err := k8sutil.DeleteIfNeeded(ctx, r.Client, staleSecret); err != nil {
+ return fmt.Errorf("failed to delete stale ingress default certificate secret: %w", err)
+ }
+ return statuspatching.PatchStatus(ctx, r.Client, hcluster, func() error {
+ meta.RemoveStatusCondition(&hcluster.Status.Conditions, string(hyperv1.IngressDefaultCertificateSynced))
+ return nil
+ })
+ }
+
+ // setSyncedCondition records the outcome on the HostedCluster status using an
+ // optimistic-lock patch. It only returns an error if persisting the status fails.
+ setSyncedCondition := func(status metav1.ConditionStatus, reason, message string) error {
+ return statuspatching.PatchStatusCondition(ctx, r.Client, hcluster, &hcluster.Status.Conditions, metav1.Condition{
+ Type: string(hyperv1.IngressDefaultCertificateSynced),
+ Status: status,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: hcluster.Generation,
+ })
+ }
+
+ // The IBM Cloud ingress controller does not consume a user-provided default
+ // certificate: ReconcileDefaultIngressController skips spec.defaultCertificate
+ // for IBM Cloud, so nothing in the guest cluster would use the synced secret.
+ // Skip the sync entirely and surface it, rather than reporting a successful
+ // sync that has no effect.
+ if hcluster.Spec.Platform.Type == hyperv1.IBMCloudPlatform {
+ log.Info("ingress default certificate is not supported on the IBM Cloud platform; skipping sync")
+ return setSyncedCondition(metav1.ConditionFalse, hyperv1.IngressDefaultCertificatePlatformNotSupportedReason,
+ "ingress default certificate is not supported on the IBM Cloud platform")
+ }
+
+ sourceSecretName := hcluster.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate.Name
+ var src corev1.Secret
+ if err := r.Client.Get(ctx, client.ObjectKey{Namespace: hcluster.Namespace, Name: sourceSecretName}, &src); err != nil {
+ if apierrors.IsNotFound(err) {
+ log.Info("ingress default certificate secret not found; keeping the previously synced certificate", "secret", sourceSecretName)
+ return setSyncedCondition(metav1.ConditionFalse, hyperv1.SecretNotFoundReason,
+ fmt.Sprintf("ingress default certificate secret %q not found in namespace %q", sourceSecretName, hcluster.Namespace))
+ }
+ return fmt.Errorf("failed to get ingress default certificate secret %s: %w", sourceSecretName, err)
+ }
+
+ // Annotate the referenced secret before validating it so that a later
+ // correction to a malformed secret (or any content change) enqueues the
+ // HostedCluster for reconciliation via the secret watch.
+ if err := ensureReferencedResourceAnnotation(ctx, r.Client, hcluster.Name, &src); err != nil {
+ return fmt.Errorf("failed to set referenced resource annotation: %w", err)
+ }
+
+ // Validate the source before touching the destination so a malformed secret
+ // leaves the previously synced certificate in place.
+ for _, key := range []string{corev1.TLSCertKey, corev1.TLSPrivateKeyKey} {
+ if _, ok := src.Data[key]; !ok {
+ log.Info("ingress default certificate secret is missing a required key; keeping the previously synced certificate", "secret", sourceSecretName, "key", key)
+ return setSyncedCondition(metav1.ConditionFalse, hyperv1.IngressDefaultCertificateInvalidReason,
+ fmt.Sprintf("ingress default certificate secret %q must contain a %s key", sourceSecretName, key))
+ }
+ }
+
+ dest := cpomanifests.ServiceProviderDefaultIngressServingCert(controlPlaneNamespace)
+ if _, err := createOrUpdate(ctx, r.Client, dest, func() error {
+ dest.Type = corev1.SecretTypeTLS
+ if dest.Data == nil {
+ dest.Data = map[string][]byte{}
+ }
+ dest.Data[corev1.TLSCertKey] = src.Data[corev1.TLSCertKey]
+ dest.Data[corev1.TLSPrivateKeyKey] = src.Data[corev1.TLSPrivateKeyKey]
+ return nil
+ }); err != nil {
+ return fmt.Errorf("failed to sync ingress default certificate to control plane namespace: %w", err)
+ }
+
+ return setSyncedCondition(metav1.ConditionTrue, hyperv1.AsExpectedReason,
+ fmt.Sprintf("ingress default certificate synced from secret %q", sourceSecretName))
+}
+
// reconcileUnmanagedEtcdMTLSSync syncs the unmanaged etcd client MTLS secret
// from the HostedCluster namespace to the control plane namespace.
func (r *HostedClusterReconciler) reconcileUnmanagedEtcdMTLSSync(
@@ -3957,6 +4078,51 @@ func (r *HostedClusterReconciler) delete(ctx context.Context, hc *hyperv1.Hosted
return true, nil
}
+// hostedClusterIngressDefaultCertSecretIndex indexes HostedClusters by the name of
+// the Secret referenced via spec.operatorConfiguration.ingressOperator.defaultCertificate.
+// It lets the Secret watch map a referenced Secret back to its owner with an
+// O(matches) cache lookup, independent of the referenced-resource annotation, which
+// a freshly recreated Secret would not yet carry.
+const hostedClusterIngressDefaultCertSecretIndex = "spec.operatorConfiguration.ingressOperator.defaultCertificate.name"
+
+func indexHostedClusterByIngressDefaultCertSecret(o client.Object) []string {
+ hc, ok := o.(*hyperv1.HostedCluster)
+ if !ok {
+ return nil
+ }
+ if hc.Spec.OperatorConfiguration != nil &&
+ hc.Spec.OperatorConfiguration.IngressOperator != nil &&
+ hc.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate.Name != "" {
+ return []string{hc.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate.Name}
+ }
+ return nil
+}
+
+// hostedClustersReferencingSecret appends reconcile requests for HostedClusters
+// that reference the given Secret by name through spec (via the field index),
+// skipping any already present in requests. Unlike the referenced-resource
+// annotation, a spec reference survives delete+recreate of the Secret (the
+// recreated object carries no annotation yet). The field index keeps this an
+// O(matches) cache lookup that returns nothing for unrelated Secrets.
+func hostedClustersReferencingSecret(ctx context.Context, c client.Client, secret *corev1.Secret, requests []reconcile.Request) []reconcile.Request {
+ hcList := &hyperv1.HostedClusterList{}
+ if err := c.List(ctx, hcList,
+ client.InNamespace(secret.Namespace),
+ client.MatchingFields{hostedClusterIngressDefaultCertSecretIndex: secret.Name},
+ ); err != nil {
+ ctrllog.Log.Error(err, "failed to list hosted clusters referencing secret", "secret", client.ObjectKeyFromObject(secret).String())
+ return requests
+ }
+ for i := range hcList.Items {
+ hc := &hcList.Items[i]
+ req := reconcile.Request{NamespacedName: types.NamespacedName{Namespace: hc.Namespace, Name: hc.Name}}
+ if !slices.Contains(requests, req) {
+ requests = append(requests, req)
+ }
+ }
+ return requests
+}
+
func enqueueHostedClustersFunc(metricsSet metrics.MetricsSet, operatorNamespace string, c client.Client) handler.MapFunc {
return func(ctx context.Context, obj client.Object) []reconcile.Request {
log := ctrllog.Log
@@ -4052,7 +4218,10 @@ func enqueueHostedClustersFunc(metricsSet metrics.MetricsSet, operatorNamespace
}
}
}
- return handleDefault(typedObj)
+ // Enqueue via the referenced-resource annotation, and also via the spec
+ // field index so a delete+recreate of a referenced Secret (which drops the
+ // annotation) still re-enqueues its owner.
+ return hostedClustersReferencingSecret(ctx, c, typedObj, handleDefault(typedObj))
default:
return handleDefault(typedObj)
}
diff --git a/hypershift-operator/controllers/hostedcluster/ingress_default_cert_sync_test.go b/hypershift-operator/controllers/hostedcluster/ingress_default_cert_sync_test.go
new file mode 100644
index 000000000000..88bacdfa5931
--- /dev/null
+++ b/hypershift-operator/controllers/hostedcluster/ingress_default_cert_sync_test.go
@@ -0,0 +1,300 @@
+package hostedcluster
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ . "github.com/onsi/gomega"
+
+ hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
+ cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests"
+ "github.com/openshift/hypershift/support/api"
+ "github.com/openshift/hypershift/support/upsert"
+
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+ "sigs.k8s.io/controller-runtime/pkg/client/interceptor"
+)
+
+func hostedClusterWithDefaultCert(name string) *hyperv1.HostedCluster {
+ return &hyperv1.HostedCluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cluster",
+ Namespace: "clusters",
+ },
+ Spec: hyperv1.HostedClusterSpec{
+ OperatorConfiguration: &hyperv1.OperatorConfiguration{
+ IngressOperator: &hyperv1.IngressOperatorSpec{
+ DefaultCertificate: hyperv1.IngressDefaultCertificateReference{
+ Name: name,
+ },
+ },
+ },
+ },
+ }
+}
+
+func TestReconcileIngressDefaultCertSync(t *testing.T) {
+ tests := []struct {
+ name string
+ hcluster *hyperv1.HostedCluster
+ existingSecret *corev1.Secret
+ // staleDestSecret seeds a previously synced secret in the control plane
+ // namespace to verify it is cleaned up when the feature is no longer in use.
+ staleDestSecret bool
+ // failGetSecret makes the source secret Get return a non-NotFound error to
+ // exercise the transient-failure path.
+ failGetSecret bool
+ // expectError is only for unexpected/transient failures that should retry.
+ expectError bool
+ // expectSync asserts the destination secret was written.
+ expectSync bool
+ // expectCondition is the expected status of the IngressDefaultCertificateSynced
+ // condition. Empty means the condition must be absent.
+ expectCondition metav1.ConditionStatus
+ expectReason string
+ }{
+ {
+ name: "When DefaultCertificate is not set, it should be a no-op with no condition",
+ hcluster: &hyperv1.HostedCluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cluster",
+ Namespace: "clusters",
+ },
+ },
+ expectSync: false,
+ },
+ {
+ name: "When OperatorConfiguration is nil, it should be a no-op with no condition",
+ hcluster: &hyperv1.HostedCluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cluster",
+ Namespace: "clusters",
+ },
+ Spec: hyperv1.HostedClusterSpec{
+ OperatorConfiguration: nil,
+ },
+ },
+ expectSync: false,
+ },
+ {
+ name: "When DefaultCertificate is set with valid TLS secret, it should sync and set the condition True",
+ hcluster: hostedClusterWithDefaultCert("my-tls-cert"),
+ existingSecret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-tls-cert",
+ Namespace: "clusters",
+ },
+ Type: corev1.SecretTypeTLS,
+ Data: map[string][]byte{
+ corev1.TLSCertKey: []byte("cert-data"),
+ corev1.TLSPrivateKeyKey: []byte("key-data"),
+ },
+ },
+ expectSync: true,
+ expectCondition: metav1.ConditionTrue,
+ expectReason: hyperv1.AsExpectedReason,
+ },
+ {
+ name: "When the ingress capability is disabled, it should skip the sync and set no condition",
+ hcluster: func() *hyperv1.HostedCluster {
+ hc := hostedClusterWithDefaultCert("my-tls-cert")
+ hc.Spec.Capabilities = &hyperv1.Capabilities{
+ Disabled: []hyperv1.OptionalCapability{hyperv1.IngressCapability},
+ }
+ return hc
+ }(),
+ existingSecret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-tls-cert",
+ Namespace: "clusters",
+ },
+ Type: corev1.SecretTypeTLS,
+ Data: map[string][]byte{
+ corev1.TLSCertKey: []byte("cert-data"),
+ corev1.TLSPrivateKeyKey: []byte("key-data"),
+ },
+ },
+ expectSync: false,
+ },
+ {
+ name: "When platform is IBM Cloud, it should skip the sync and set the condition False",
+ hcluster: func() *hyperv1.HostedCluster {
+ hc := hostedClusterWithDefaultCert("my-tls-cert")
+ hc.Spec.Platform.Type = hyperv1.IBMCloudPlatform
+ return hc
+ }(),
+ existingSecret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "my-tls-cert",
+ Namespace: "clusters",
+ },
+ Type: corev1.SecretTypeTLS,
+ Data: map[string][]byte{
+ corev1.TLSCertKey: []byte("cert-data"),
+ corev1.TLSPrivateKeyKey: []byte("key-data"),
+ },
+ },
+ expectSync: false,
+ expectCondition: metav1.ConditionFalse,
+ expectReason: hyperv1.IngressDefaultCertificatePlatformNotSupportedReason,
+ },
+ {
+ name: "When source secret is missing tls.crt, it should not fail and set the condition False",
+ hcluster: hostedClusterWithDefaultCert("bad-cert"),
+ existingSecret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "bad-cert",
+ Namespace: "clusters",
+ },
+ Data: map[string][]byte{
+ corev1.TLSPrivateKeyKey: []byte("key-data"),
+ },
+ },
+ expectSync: false,
+ expectCondition: metav1.ConditionFalse,
+ expectReason: hyperv1.IngressDefaultCertificateInvalidReason,
+ },
+ {
+ name: "When source secret is missing tls.key, it should not fail and set the condition False",
+ hcluster: hostedClusterWithDefaultCert("bad-cert"),
+ existingSecret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "bad-cert",
+ Namespace: "clusters",
+ },
+ Data: map[string][]byte{
+ corev1.TLSCertKey: []byte("cert-data"),
+ },
+ },
+ expectSync: false,
+ expectCondition: metav1.ConditionFalse,
+ expectReason: hyperv1.IngressDefaultCertificateInvalidReason,
+ },
+ {
+ name: "When source secret does not exist, it should not fail and set the condition False",
+ hcluster: hostedClusterWithDefaultCert("nonexistent-cert"),
+ expectSync: false,
+ expectCondition: metav1.ConditionFalse,
+ expectReason: hyperv1.SecretNotFoundReason,
+ },
+ {
+ name: "When getting the source secret fails unexpectedly, it should return an error",
+ hcluster: hostedClusterWithDefaultCert("boom-cert"),
+ failGetSecret: true,
+ expectError: true,
+ },
+ {
+ name: "When DefaultCertificate is cleared, the stale condition and synced secret should be removed",
+ hcluster: func() *hyperv1.HostedCluster {
+ hc := &hyperv1.HostedCluster{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-cluster",
+ Namespace: "clusters",
+ },
+ }
+ meta.SetStatusCondition(&hc.Status.Conditions, metav1.Condition{
+ Type: string(hyperv1.IngressDefaultCertificateSynced),
+ Status: metav1.ConditionTrue,
+ Reason: hyperv1.AsExpectedReason,
+ })
+ return hc
+ }(),
+ staleDestSecret: true,
+ expectSync: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ g := NewGomegaWithT(t)
+ ctx := context.Background()
+
+ controlPlaneNamespace := "clusters-test-cluster"
+ objs := []client.Object{tt.hcluster}
+ if tt.existingSecret != nil {
+ objs = append(objs, tt.existingSecret)
+ }
+ if tt.staleDestSecret {
+ stale := cpomanifests.ServiceProviderDefaultIngressServingCert(controlPlaneNamespace)
+ stale.Type = corev1.SecretTypeTLS
+ stale.Data = map[string][]byte{
+ corev1.TLSCertKey: []byte("stale-cert"),
+ corev1.TLSPrivateKeyKey: []byte("stale-key"),
+ }
+ objs = append(objs, stale)
+ }
+
+ builder := fake.NewClientBuilder().
+ WithScheme(api.Scheme).
+ WithObjects(objs...).
+ WithStatusSubresource(tt.hcluster)
+ if tt.failGetSecret {
+ secretName := tt.hcluster.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate.Name
+ builder = builder.WithInterceptorFuncs(interceptor.Funcs{
+ Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
+ if _, ok := obj.(*corev1.Secret); ok && key.Name == secretName {
+ return apierrors.NewInternalError(fmt.Errorf("simulated get failure"))
+ }
+ return c.Get(ctx, key, obj, opts...)
+ },
+ })
+ }
+ fakeClient := builder.Build()
+
+ r := &HostedClusterReconciler{
+ Client: fakeClient,
+ now: metav1.Now,
+ }
+
+ // Guard against a vacuous pass: when a case pre-seeds the condition and
+ // then expects it removed, confirm the fake client actually persisted it
+ // before reconciliation so the case proves removal.
+ if tt.expectCondition == "" && meta.FindStatusCondition(tt.hcluster.Status.Conditions, string(hyperv1.IngressDefaultCertificateSynced)) != nil {
+ seeded := &hyperv1.HostedCluster{}
+ g.Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(tt.hcluster), seeded)).To(Succeed())
+ g.Expect(meta.FindStatusCondition(seeded.Status.Conditions, string(hyperv1.IngressDefaultCertificateSynced))).ToNot(BeNil(),
+ "pre-seeded condition should be persisted before reconciliation")
+ }
+
+ err := r.reconcileIngressDefaultCertSync(ctx, tt.hcluster, upsert.New(false).CreateOrUpdate, controlPlaneNamespace)
+
+ if tt.expectError {
+ g.Expect(err).To(HaveOccurred())
+ return
+ }
+ g.Expect(err).ToNot(HaveOccurred())
+
+ expected := cpomanifests.ServiceProviderDefaultIngressServingCert(controlPlaneNamespace)
+ if tt.expectSync {
+ dest := &corev1.Secret{}
+ err := fakeClient.Get(ctx, client.ObjectKeyFromObject(expected), dest)
+ g.Expect(err).ToNot(HaveOccurred())
+ g.Expect(dest.Type).To(Equal(corev1.SecretTypeTLS))
+ g.Expect(dest.Data[corev1.TLSCertKey]).To(Equal(tt.existingSecret.Data[corev1.TLSCertKey]))
+ g.Expect(dest.Data[corev1.TLSPrivateKeyKey]).To(Equal(tt.existingSecret.Data[corev1.TLSPrivateKeyKey]))
+ } else {
+ err := fakeClient.Get(ctx, client.ObjectKeyFromObject(expected), &corev1.Secret{})
+ g.Expect(apierrors.IsNotFound(err)).To(BeTrue(), "destination secret should not be created when the sync is skipped")
+ }
+
+ // Read back the persisted status to assert on the condition.
+ updated := &hyperv1.HostedCluster{}
+ g.Expect(fakeClient.Get(ctx, client.ObjectKeyFromObject(tt.hcluster), updated)).To(Succeed())
+ condition := meta.FindStatusCondition(updated.Status.Conditions, string(hyperv1.IngressDefaultCertificateSynced))
+ if tt.expectCondition == "" {
+ g.Expect(condition).To(BeNil(), "IngressDefaultCertificateSynced condition should be absent")
+ return
+ }
+ g.Expect(condition).ToNot(BeNil(), "IngressDefaultCertificateSynced condition should be set")
+ g.Expect(condition.Status).To(Equal(tt.expectCondition))
+ g.Expect(condition.Reason).To(Equal(tt.expectReason))
+ })
+ }
+}
diff --git a/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go b/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go
index 9e36c7d392a9..c17631166237 100644
--- a/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go
+++ b/hypershift-operator/controllers/hostedcluster/reconcile_legacy.go
@@ -1361,6 +1361,10 @@ func (r *HostedClusterReconciler) reconcileLegacy(ctx context.Context, req ctrl.
}
}
+ if err := r.reconcileIngressDefaultCertSync(ctx, hcluster, createOrUpdate, controlPlaneNamespace.Name); err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed to reconcile ingress default certificate: %w", err)
+ }
+
// Reconcile the HostedControlPlane AdditionalTrustBundle ConfigMap by resolving the source reference
// from the HostedCluster and syncing the CM in the control plane namespace.
if err := r.reconcileAdditionalTrustBundle(ctx, hcluster, createOrUpdate, controlPlaneNamespace.Name); err != nil {
From 698537933537abbd80eb1f78f81082f34519afb5 Mon Sep 17 00:00:00 2001
From: David Eads
Date: Fri, 4 Sep 2026 11:12:31 -0400
Subject: [PATCH 4/7] feat(control-plane-operator): use user-provided ingress
default certificate in HCCO
Propagate the user-provided default certificate into the hosted cluster:
- When a custom certificate is configured, HCCO sources default-ingress-cert
from the synced service-provider secret; otherwise it falls back to the
CPO-generated wildcard.
- Do not fail reconciliation (do not degrade the HostedCluster) when the synced
secret has not landed yet, and treat IBM Cloud as having no custom certificate.
- Add the ServiceProviderDefaultIngressServingCert manifest and thread
DefaultCertificate through the ingress params.
- Add unit tests for certificate-source selection and params.
Signed-off-by: David Eads
Commit-Message-Assisted-by: Claude (via Claude Code)
Co-Authored-By: Claude Opus 4.8 (1M context)
(cherry picked from commit 408113ba188a22d63f86d885b98236109f34081d)
---
.../hostedcontrolplane/manifests/ingress.go | 9 ++
.../controllers/resources/ingress/params.go | 6 +-
.../resources/ingress/params_test.go | 27 +++++
.../controllers/resources/resources.go | 33 +++++-
.../controllers/resources/resources_test.go | 106 ++++++++++++++++++
5 files changed, 177 insertions(+), 4 deletions(-)
diff --git a/control-plane-operator/controllers/hostedcontrolplane/manifests/ingress.go b/control-plane-operator/controllers/hostedcontrolplane/manifests/ingress.go
index 593cd7a9c8fe..333b5b6c97f4 100644
--- a/control-plane-operator/controllers/hostedcontrolplane/manifests/ingress.go
+++ b/control-plane-operator/controllers/hostedcontrolplane/manifests/ingress.go
@@ -101,6 +101,15 @@ func IngressDefaultIngressControllerCert() *corev1.Secret {
}
}
+func ServiceProviderDefaultIngressServingCert(ns string) *corev1.Secret {
+ return &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "service-provider-default-ingress-serving-cert",
+ Namespace: ns,
+ },
+ }
+}
+
func IngressObservedDefaultIngressCertCA(ns string) *corev1.ConfigMap {
return &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/ingress/params.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/ingress/params.go
index 2163e7eb195c..0b231279c6d6 100644
--- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/ingress/params.go
+++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/ingress/params.go
@@ -19,6 +19,7 @@ type IngressParams struct {
LoadBalancerScope v1.LoadBalancerScope
LoadBalancerIP string
EndpointPublishingStrategy *v1.EndpointPublishingStrategy
+ DefaultCertificate hyperv1.IngressDefaultCertificateReference
}
func NewIngressParams(hcp *hyperv1.HostedControlPlane) *IngressParams {
@@ -29,6 +30,7 @@ func NewIngressParams(hcp *hyperv1.HostedControlPlane) *IngressParams {
var loadBalancerIP string
loadBalancerScope := v1.ExternalLoadBalancer
var endpointPublishingStrategy *v1.EndpointPublishingStrategy
+ var defaultCertificate hyperv1.IngressDefaultCertificateReference
if hcp.Spec.Platform.IBMCloud != nil && hcp.Spec.Platform.IBMCloud.ProviderType == configv1.IBMCloudProviderTypeUPI {
ibmCloudUPI = true
@@ -62,9 +64,10 @@ func NewIngressParams(hcp *hyperv1.HostedControlPlane) *IngressParams {
loadBalancerIP = hcp.Spec.Platform.OpenStack.IngressFloatingIP
}
- // Extract endpointPublishingStrategy from OperatorConfiguration if configured
+ // Extract endpointPublishingStrategy and defaultCertificate from OperatorConfiguration if configured
if hcp.Spec.OperatorConfiguration != nil && hcp.Spec.OperatorConfiguration.IngressOperator != nil {
endpointPublishingStrategy = hcp.Spec.OperatorConfiguration.IngressOperator.EndpointPublishingStrategy
+ defaultCertificate = hcp.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate
}
return &IngressParams{
@@ -77,5 +80,6 @@ func NewIngressParams(hcp *hyperv1.HostedControlPlane) *IngressParams {
LoadBalancerScope: loadBalancerScope,
LoadBalancerIP: loadBalancerIP,
EndpointPublishingStrategy: endpointPublishingStrategy,
+ DefaultCertificate: defaultCertificate,
}
}
diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/ingress/params_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/ingress/params_test.go
index 9d2b0c266b27..2a09c63fa4ae 100644
--- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/ingress/params_test.go
+++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/ingress/params_test.go
@@ -317,6 +317,33 @@ func TestNewIngressParams(t *testing.T) {
LoadBalancerScope: v1.ExternalLoadBalancer,
},
},
+ {
+ name: "When DefaultCertificate is set via OperatorConfiguration, it should propagate it to ingress parameters",
+ args: args{
+ hcp: &hyperv1.HostedControlPlane{
+ Spec: hyperv1.HostedControlPlaneSpec{
+ OperatorConfiguration: &hyperv1.OperatorConfiguration{
+ IngressOperator: &hyperv1.IngressOperatorSpec{
+ DefaultCertificate: hyperv1.IngressDefaultCertificateReference{
+ Name: "my-custom-cert",
+ },
+ },
+ },
+ },
+ },
+ },
+ want: &IngressParams{
+ IngressSubdomain: "apps.",
+ Replicas: 1,
+ IsPrivate: false,
+ IBMCloudUPI: false,
+ AWSNLB: false,
+ LoadBalancerScope: v1.ExternalLoadBalancer,
+ DefaultCertificate: hyperv1.IngressDefaultCertificateReference{
+ Name: "my-custom-cert",
+ },
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go
index 033f75a70b62..4a3d76f79534 100644
--- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go
+++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources.go
@@ -1417,6 +1417,7 @@ func (r *reconciler) reconcileRBAC(ctx context.Context, hcp *hyperv1.HostedContr
}
func (r *reconciler) reconcileIngressController(ctx context.Context, hcp *hyperv1.HostedControlPlane) error {
+ log := ctrl.LoggerFrom(ctx)
var errs []error
p := ingress.NewIngressParams(hcp)
ingressController := manifests.IngressDefaultIngressController()
@@ -1426,10 +1427,36 @@ func (r *reconciler) reconcileIngressController(ctx context.Context, hcp *hyperv
errs = append(errs, fmt.Errorf("failed to reconcile default ingress controller: %w", err))
}
- sourceCert := cpomanifests.IngressCert(hcp.Namespace)
- if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(sourceCert), sourceCert); err != nil {
- errs = append(errs, fmt.Errorf("failed to get ingress cert (%s/%s) from control plane: %w", sourceCert.Namespace, sourceCert.Name, err))
+ // When the user provides a custom default certificate, use the secret synced
+ // into the control plane namespace as the source. Otherwise, fall back to the
+ // CPO-generated wildcard cert. IBM Cloud's ingress controller does not consume a
+ // user-provided default certificate (ReconcileDefaultIngressController skips
+ // spec.defaultCertificate for IBM Cloud), so the custom secret is never synced
+ // there; keep sourcing the generated wildcard so this loop does not chase a
+ // secret that will never appear.
+ usingCustomCert := len(p.DefaultCertificate.Name) > 0 && p.PlatformType != hyperv1.IBMCloudPlatform
+ var sourceCert *corev1.Secret
+ if usingCustomCert {
+ sourceCert = cpomanifests.ServiceProviderDefaultIngressServingCert(hcp.Namespace)
} else {
+ sourceCert = cpomanifests.IngressCert(hcp.Namespace)
+ }
+ if err := r.cpClient.Get(ctx, client.ObjectKeyFromObject(sourceCert), sourceCert); err != nil {
+ if usingCustomCert {
+ // The custom certificate is synced independently by the HyperShift
+ // Operator and may not have landed in the control plane namespace yet
+ // (or the source may be missing). Do not fail reconciliation: leave the
+ // previously synced certificate in place so the HostedCluster does not
+ // become degraded. The IngressDefaultCertificateSynced condition on the
+ // HostedCluster surfaces the underlying problem.
+ log.Info("user-provided ingress default certificate not yet available in the control plane namespace; keeping the existing certificate",
+ "secret", client.ObjectKeyFromObject(sourceCert).String(), "error", err.Error())
+ } else {
+ errs = append(errs, fmt.Errorf("failed to get ingress cert (%s/%s) from control plane: %w", sourceCert.Namespace, sourceCert.Name, err))
+ }
+ sourceCert = nil
+ }
+ if sourceCert != nil {
ingressControllerCert := manifests.IngressDefaultIngressControllerCert()
if _, err := r.CreateOrUpdate(ctx, r.client, ingressControllerCert, func() error {
return ingress.ReconcileDefaultIngressControllerCertSecret(ingressControllerCert, sourceCert)
diff --git a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources_test.go b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources_test.go
index 4dd059fb0a94..12c6be5987af 100644
--- a/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources_test.go
+++ b/control-plane-operator/hostedclusterconfigoperator/controllers/resources/resources_test.go
@@ -365,6 +365,112 @@ func (*simpleCreateOrUpdater) CreateOrUpdate(ctx context.Context, c client.Clien
return controllerutil.CreateOrUpdate(ctx, c, obj, f)
}
+func TestReconcileIngressControllerCertSource(t *testing.T) {
+ ctx := t.Context()
+
+ customData := map[string][]byte{
+ corev1.TLSCertKey: []byte("custom-cert"),
+ corev1.TLSPrivateKeyKey: []byte("custom-key"),
+ }
+ wildcardData := map[string][]byte{
+ corev1.TLSCertKey: []byte("wildcard-cert"),
+ corev1.TLSPrivateKeyKey: []byte("wildcard-key"),
+ }
+
+ newHCP := func(withCustom bool) *hyperv1.HostedControlPlane {
+ hcp := fakeHCP()
+ hcp.Spec.Platform.Type = hyperv1.NonePlatform
+ hcp.Spec.DNS.BaseDomain = "example.com"
+ if withCustom {
+ hcp.Spec.OperatorConfiguration = &hyperv1.OperatorConfiguration{
+ IngressOperator: &hyperv1.IngressOperatorSpec{
+ DefaultCertificate: hyperv1.IngressDefaultCertificateReference{Name: "my-cert"},
+ },
+ }
+ }
+ return hcp
+ }
+
+ wildcardSource := func() *corev1.Secret {
+ s := cpomanifests.IngressCert("bar")
+ s.Type = corev1.SecretTypeTLS
+ s.Data = wildcardData
+ return s
+ }
+ customSource := func() *corev1.Secret {
+ s := cpomanifests.ServiceProviderDefaultIngressServingCert("bar")
+ s.Type = corev1.SecretTypeTLS
+ s.Data = customData
+ return s
+ }
+ existingGuestCert := func() *corev1.Secret {
+ s := manifests.IngressDefaultIngressControllerCert()
+ s.Type = corev1.SecretTypeTLS
+ s.Data = wildcardData
+ return s
+ }
+
+ testCases := []struct {
+ name string
+ hcp *hyperv1.HostedControlPlane
+ cpObjects []client.Object
+ guestObjects []client.Object
+ // expectCert is the tls.crt expected in the guest default-ingress-cert secret.
+ expectCert []byte
+ }{
+ {
+ name: "When no custom certificate is configured, it should sync the generated wildcard certificate",
+ hcp: newHCP(false),
+ cpObjects: []client.Object{wildcardSource()},
+ expectCert: wildcardData[corev1.TLSCertKey],
+ },
+ {
+ name: "When a custom certificate is configured and synced, it should use the custom certificate",
+ hcp: newHCP(true),
+ cpObjects: []client.Object{customSource()},
+ expectCert: customData[corev1.TLSCertKey],
+ },
+ {
+ name: "When a custom certificate is configured but not yet synced, it should not error and preserve the existing certificate",
+ hcp: newHCP(true),
+ cpObjects: []client.Object{},
+ guestObjects: []client.Object{existingGuestCert()},
+ expectCert: wildcardData[corev1.TLSCertKey],
+ },
+ {
+ name: "When a custom certificate is configured on IBM Cloud, it should ignore it and use the generated wildcard certificate",
+ hcp: func() *hyperv1.HostedControlPlane {
+ hcp := newHCP(true)
+ hcp.Spec.Platform.Type = hyperv1.IBMCloudPlatform
+ return hcp
+ }(),
+ cpObjects: []client.Object{wildcardSource(), customSource()},
+ expectCert: wildcardData[corev1.TLSCertKey],
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ g := NewWithT(t)
+ cpClient := fake.NewClientBuilder().WithScheme(api.Scheme).WithObjects(append(tc.cpObjects, tc.hcp)...).Build()
+ guestClient := fake.NewClientBuilder().WithScheme(api.Scheme).WithObjects(tc.guestObjects...).Build()
+ r := &reconciler{
+ client: guestClient,
+ cpClient: cpClient,
+ CreateOrUpdateProvider: &simpleCreateOrUpdater{},
+ }
+
+ // A missing custom certificate must not fail reconciliation, so the
+ // HostedCluster does not become degraded.
+ g.Expect(r.reconcileIngressController(ctx, tc.hcp)).To(Succeed())
+
+ guestCert := manifests.IngressDefaultIngressControllerCert()
+ g.Expect(guestClient.Get(ctx, client.ObjectKeyFromObject(guestCert), guestCert)).To(Succeed())
+ g.Expect(guestCert.Data[corev1.TLSCertKey]).To(Equal(tc.expectCert))
+ })
+ }
+}
+
func fakeHCP() *hyperv1.HostedControlPlane {
hcp := manifests.HostedControlPlane("bar", "foo")
hcp.Status.ControlPlaneEndpoint.Host = "server"
From 9b1a56ecdbd986b207a46d66d0c9b8ab09068c77 Mon Sep 17 00:00:00 2001
From: David Eads
Date: Fri, 4 Sep 2026 11:12:40 -0400
Subject: [PATCH 5/7] test(e2e): add ingress default certificate coverage
- Add a v2 lifecycle test covering propagation, rotation, TLS handshake, the
IngressDefaultCertificateSynced condition, source-secret deletion, revert to
the wildcard, and a malformed-source negative case. Wire it into the Azure
public group and skip it on Azure until the ingress endpoint is reachable from
the build farm.
- Add envtest cases for the defaultCertificate name (CEL, MinLength, MaxLength).
- Add an IngressOperatorSpec serialization round-trip test and move
GenerateCustomCertificate to test/e2e/v2/util to satisfy the e2eutilallowlist.
Signed-off-by: David Eads
Commit-Message-Assisted-by: Claude (via Claude Code)
Co-Authored-By: Claude Opus 4.8 (1M context)
(cherry picked from commit 004ae9b408d7879365fffdcb55167f528700de44)
---
api/hypershift/v1beta1/operator_test.go | 91 ++++
...able.hostedclusters.ingress.testsuite.yaml | 291 +++++++++++
test/e2e/util/util.go | 29 +-
test/e2e/util/util_test.go | 3 +-
test/e2e/v2/lifecycle/azure.go | 2 +-
.../v2/tests/hosted_cluster_ingress_test.go | 457 ++++++++++++++++++
test/e2e/v2/util/certs.go | 33 ++
7 files changed, 877 insertions(+), 29 deletions(-)
create mode 100644 api/hypershift/v1beta1/operator_test.go
create mode 100644 cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/stable.hostedclusters.ingress.testsuite.yaml
create mode 100644 test/e2e/v2/util/certs.go
diff --git a/api/hypershift/v1beta1/operator_test.go b/api/hypershift/v1beta1/operator_test.go
new file mode 100644
index 000000000000..dac9f7e74a06
--- /dev/null
+++ b/api/hypershift/v1beta1/operator_test.go
@@ -0,0 +1,91 @@
+package v1beta1
+
+import (
+ "encoding/json"
+ "testing"
+
+ operatorv1 "github.com/openshift/api/operator/v1"
+)
+
+// ingressOperatorSpecNMinus1 represents the previous version of IngressOperatorSpec
+// without the DefaultCertificate field.
+type ingressOperatorSpecNMinus1 struct {
+ EndpointPublishingStrategy json.RawMessage `json:"endpointPublishingStrategy,omitempty"` //nolint:kubeapilinter
+}
+
+func TestIngressOperatorSpecSerializationCompatibility(t *testing.T) {
+ tests := []struct {
+ name string
+ current IngressOperatorSpec
+ expectedJSON string
+ nMinus1Result ingressOperatorSpecNMinus1
+ }{
+ {
+ name: "When DefaultCertificate is zero it should be omitted and N-1 should deserialize cleanly",
+ current: IngressOperatorSpec{},
+ expectedJSON: `{}`,
+ nMinus1Result: ingressOperatorSpecNMinus1{},
+ },
+ {
+ name: "When DefaultCertificate is set it should serialize and N-1 should ignore it",
+ current: IngressOperatorSpec{
+ DefaultCertificate: IngressDefaultCertificateReference{
+ Name: "my-cert",
+ },
+ },
+ expectedJSON: `{"defaultCertificate":{"name":"my-cert"}}`,
+ nMinus1Result: ingressOperatorSpecNMinus1{},
+ },
+ {
+ name: "When N-1 data carries EndpointPublishingStrategy it should survive the round-trip into N",
+ current: IngressOperatorSpec{},
+ expectedJSON: `{}`,
+ nMinus1Result: ingressOperatorSpecNMinus1{
+ EndpointPublishingStrategy: json.RawMessage(`{"type":"LoadBalancerService"}`),
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ data, err := json.Marshal(tt.current)
+ if err != nil {
+ t.Fatalf("failed to marshal current struct: %v", err)
+ }
+ if string(data) != tt.expectedJSON {
+ t.Errorf("unexpected JSON output: got %s, want %s", string(data), tt.expectedJSON)
+ }
+
+ // N -> N-1: old code should ignore the unknown DefaultCertificate field
+ var nMinus1 ingressOperatorSpecNMinus1
+ if err := json.Unmarshal(data, &nMinus1); err != nil {
+ t.Fatalf("N-1 failed to unmarshal JSON from N: %v", err)
+ }
+
+ // N-1 -> N: data from old code should deserialize into new struct with zero DefaultCertificate
+ nMinus1Data, err := json.Marshal(tt.nMinus1Result)
+ if err != nil {
+ t.Fatalf("failed to marshal N-1 struct: %v", err)
+ }
+ var roundTrip IngressOperatorSpec
+ if err := json.Unmarshal(nMinus1Data, &roundTrip); err != nil {
+ t.Fatalf("N failed to unmarshal JSON from N-1: %v", err)
+ }
+ if roundTrip.DefaultCertificate.Name != "" {
+ t.Errorf("expected DefaultCertificate to be zero after N-1 round-trip, got %+v", roundTrip.DefaultCertificate)
+ }
+
+ // Sibling fields written by N-1 must survive into N unchanged; otherwise
+ // the round-trip would silently drop data the enhancement requires to be
+ // preserved.
+ if len(tt.nMinus1Result.EndpointPublishingStrategy) > 0 {
+ if roundTrip.EndpointPublishingStrategy == nil {
+ t.Errorf("expected EndpointPublishingStrategy to survive N-1 -> N round-trip, got nil")
+ } else if roundTrip.EndpointPublishingStrategy.Type != operatorv1.LoadBalancerServiceStrategyType {
+ t.Errorf("expected EndpointPublishingStrategy.Type %q to survive N-1 -> N round-trip, got %q",
+ operatorv1.LoadBalancerServiceStrategyType, roundTrip.EndpointPublishingStrategy.Type)
+ }
+ }
+ })
+ }
+}
diff --git a/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/stable.hostedclusters.ingress.testsuite.yaml b/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/stable.hostedclusters.ingress.testsuite.yaml
new file mode 100644
index 000000000000..54961991dee7
--- /dev/null
+++ b/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/stable.hostedclusters.ingress.testsuite.yaml
@@ -0,0 +1,291 @@
+apiVersion: apiextensions.k8s.io/v1
+name: "HostedCluster ingress validation"
+crdName: hostedclusters.hypershift.openshift.io
+version: v1beta1
+tests:
+ onCreate:
+ # --- operatorConfiguration.ingressOperator.defaultCertificate.name validation ---
+ - name: When defaultCertificate name is a valid DNS subdomain it should pass
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ operatorConfiguration:
+ ingressOperator:
+ defaultCertificate:
+ name: my-custom-ingress-cert
+ platform:
+ type: AWS
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ - name: When defaultCertificate name is a valid multi-segment DNS subdomain it should pass
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ operatorConfiguration:
+ ingressOperator:
+ defaultCertificate:
+ name: my-cert.openshift-ingress.svc
+ platform:
+ type: AWS
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ - name: When defaultCertificate name contains an underscore it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ operatorConfiguration:
+ ingressOperator:
+ defaultCertificate:
+ name: my_custom_cert
+ platform:
+ type: AWS
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "name must be a valid DNS subdomain name"
+
+ - name: When defaultCertificate name contains uppercase characters it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ operatorConfiguration:
+ ingressOperator:
+ defaultCertificate:
+ name: MyCustomCert
+ platform:
+ type: AWS
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "name must be a valid DNS subdomain name"
+
+ - name: When defaultCertificate name starts with a dash it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ operatorConfiguration:
+ ingressOperator:
+ defaultCertificate:
+ name: -my-cert
+ platform:
+ type: AWS
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "name must be a valid DNS subdomain name"
+
+ - name: When defaultCertificate name is empty it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ operatorConfiguration:
+ ingressOperator:
+ defaultCertificate:
+ name: ""
+ platform:
+ type: AWS
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "name in body should be at least 1 chars long"
+
+ - name: When defaultCertificate name exceeds 253 characters it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ operatorConfiguration:
+ ingressOperator:
+ defaultCertificate:
+ name: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ platform:
+ type: AWS
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "Too long"
diff --git a/test/e2e/util/util.go b/test/e2e/util/util.go
index e7bc4cd83ddb..56ab2085121f 100644
--- a/test/e2e/util/util.go
+++ b/test/e2e/util/util.go
@@ -4,8 +4,6 @@ import (
"bufio"
"bytes"
"context"
- "crypto/x509"
- "crypto/x509/pkix"
"encoding/json"
"errors"
"fmt"
@@ -33,13 +31,13 @@ import (
"github.com/openshift/hypershift/hypershift-operator/controllers/manifests"
controlplaneoperatoroverrides "github.com/openshift/hypershift/hypershift-operator/controlplaneoperator-overrides"
"github.com/openshift/hypershift/support/azureutil"
- "github.com/openshift/hypershift/support/certs"
"github.com/openshift/hypershift/support/conditions"
suppconfig "github.com/openshift/hypershift/support/config"
"github.com/openshift/hypershift/support/netutil"
"github.com/openshift/hypershift/support/podspec"
"github.com/openshift/hypershift/support/releaseinfo"
hyperutil "github.com/openshift/hypershift/support/util"
+ v2util "github.com/openshift/hypershift/test/e2e/v2/util"
configv1 "github.com/openshift/api/config/v1"
operatorv1 "github.com/openshift/api/operator/v1"
@@ -2396,7 +2394,7 @@ func EnsureKubeAPIDNSNameCustomCert(t *testing.T, ctx context.Context, mgmtClien
// Generate a custom certificate for the KAS
t.Log("Generating custom certificate with DNS name", customApiServerHost)
- customCert, customKey, err := GenerateCustomCertificate([]string{customApiServerHost}, 24*time.Hour)
+ customCert, customKey, err := v2util.GenerateCustomCertificate([]string{customApiServerHost}, 24*time.Hour)
g.Expect(err).NotTo(HaveOccurred(), "failed to generate custom certificate")
// Create secret with the custom certificate
@@ -3884,29 +3882,6 @@ func EnsureImageRegistryCapabilityDisabled(ctx context.Context, t *testing.T, g
})
}
-// GenerateCustomCertificate generates a self-signed certificate for the given DNS names
-func GenerateCustomCertificate(dnsNames []string, validity time.Duration) ([]byte, []byte, error) {
- if len(dnsNames) == 0 {
- return nil, nil, fmt.Errorf("no DNS names provided")
- }
-
- cfg := &certs.CertCfg{
- Subject: pkix.Name{CommonName: dnsNames[0], Organization: []string{"kubernetes"}, OrganizationalUnit: []string{"test"}},
- KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
- ExtKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
- Validity: validity,
- DNSNames: dnsNames,
- IsCA: false,
- }
-
- key, crt, err := certs.GenerateSelfSignedCertificate(cfg)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to generate self-signed certificate: %w", err)
- }
-
- return certs.CertToPem(crt), certs.PrivateKeyToPem(key), nil
-}
-
// EnsureOpenshiftSamplesCapabilityDisabled validates the expectations for when OpenShiftSamplesCapability is Disabled
func EnsureOpenshiftSamplesCapabilityDisabled(ctx context.Context, t *testing.T, g Gomega, clients *GuestClients) {
t.Run("EnsureOpenshiftSamplesCapabilityDisabled", func(t *testing.T) {
diff --git a/test/e2e/util/util_test.go b/test/e2e/util/util_test.go
index 320849b384ac..1cc6f4bfa771 100644
--- a/test/e2e/util/util_test.go
+++ b/test/e2e/util/util_test.go
@@ -10,6 +10,7 @@ import (
hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
"github.com/openshift/hypershift/support/azureutil"
"github.com/openshift/hypershift/support/certs"
+ v2util "github.com/openshift/hypershift/test/e2e/v2/util"
"k8s.io/utils/ptr"
)
@@ -164,7 +165,7 @@ func TestGenerateCustomCertificate(t *testing.T) {
for _, tc := range testsCases {
t.Run(tc.name, func(t *testing.T) {
g := NewWithT(t)
- certPEM, keyPEM, err := GenerateCustomCertificate(tc.dnsNames, tc.duration)
+ certPEM, keyPEM, err := v2util.GenerateCustomCertificate(tc.dnsNames, tc.duration)
if tc.wantErr {
g.Expect(err).To(HaveOccurred())
diff --git a/test/e2e/v2/lifecycle/azure.go b/test/e2e/v2/lifecycle/azure.go
index 4c8ac18ded01..7d2b9681f6b9 100644
--- a/test/e2e/v2/lifecycle/azure.go
+++ b/test/e2e/v2/lifecycle/azure.go
@@ -327,7 +327,7 @@ func (a *AzurePlatformConfig) TestMatrix(releaseImage string) TestMatrix {
{
Name: "public",
Variant: "public",
- LabelFilter: "self-managed-azure-public || nodepool-lifecycle || secret-encryption || control-plane-workloads || hosted-cluster-security || nodepool-osimagestream",
+ LabelFilter: "self-managed-azure-public || nodepool-lifecycle || secret-encryption || control-plane-workloads || hosted-cluster-security || nodepool-osimagestream || hosted-cluster-ingress",
Skip: "KAS allowed CIDRs",
JUnitFile: "junit_self_managed_azure_public.xml",
},
diff --git a/test/e2e/v2/tests/hosted_cluster_ingress_test.go b/test/e2e/v2/tests/hosted_cluster_ingress_test.go
index a9cc9b820418..c81b4638fe38 100644
--- a/test/e2e/v2/tests/hosted_cluster_ingress_test.go
+++ b/test/e2e/v2/tests/hosted_cluster_ingress_test.go
@@ -17,21 +17,81 @@ limitations under the License.
package tests
import (
+ "bytes"
+ "crypto/tls"
+ "crypto/x509"
"fmt"
+ "net/http"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
operatorv1 "github.com/openshift/api/operator/v1"
+ hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
+ cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests"
+ "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/controllers/resources/manifests"
e2eutil "github.com/openshift/hypershift/test/e2e/util"
"github.com/openshift/hypershift/test/e2e/v2/internal"
+ v2util "github.com/openshift/hypershift/test/e2e/v2/util"
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
+
+ crclient "sigs.k8s.io/controller-runtime/pkg/client"
)
+// canaryURL returns the health-check URL for the openshift-ingress canary route
+// on the given ingress domain.
+func canaryURL(ingressDomain string) string {
+ return fmt.Sprintf("https://canary-openshift-ingress-canary.%s/healthz", ingressDomain)
+}
+
+// newTLSClient builds an HTTP client that trusts only the provided CA bundle.
+func newTLSClient(caBundle []byte) (*http.Client, error) {
+ certPool := x509.NewCertPool()
+ if !certPool.AppendCertsFromPEM(caBundle) {
+ return nil, fmt.Errorf("failed to parse CA bundle")
+ }
+ return &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ RootCAs: certPool,
+ MinVersion: tls.VersionTLS12,
+ },
+ },
+ Timeout: 30 * time.Second,
+ }, nil
+}
+
+// ingressDomainForHostedCluster derives the apps ingress domain for the hosted
+// cluster, honoring an explicitly configured ingress domain (AppsDomain over
+// Domain, matching globalconfig.IngressDomain) and falling back to
+// apps. when neither is configured.
+func ingressDomainForHostedCluster(hc *hyperv1.HostedCluster) string {
+ if hc.Spec.Configuration != nil && hc.Spec.Configuration.Ingress != nil {
+ if len(hc.Spec.Configuration.Ingress.AppsDomain) > 0 {
+ return hc.Spec.Configuration.Ingress.AppsDomain
+ }
+ if len(hc.Spec.Configuration.Ingress.Domain) > 0 {
+ return hc.Spec.Configuration.Ingress.Domain
+ }
+ }
+ if hc.Spec.DNS.BaseDomainPrefix != nil && *hc.Spec.DNS.BaseDomainPrefix != "" {
+ return fmt.Sprintf("apps.%s.%s", *hc.Spec.DNS.BaseDomainPrefix, hc.Spec.DNS.BaseDomain)
+ }
+ if hc.Spec.DNS.BaseDomainPrefix != nil && *hc.Spec.DNS.BaseDomainPrefix == "" {
+ return fmt.Sprintf("apps.%s", hc.Spec.DNS.BaseDomain)
+ }
+ return fmt.Sprintf("apps.%s.%s", hc.Name, hc.Spec.DNS.BaseDomain)
+}
+
func RegisterHostedClusterIngressTests(getTestCtx internal.TestContextGetter) {
ValidateIngressOperatorConfigurationTest(getTestCtx)
+ ServiceProviderDefaultIngressServingCertificateLifecycleTest(getTestCtx)
}
func ValidateIngressOperatorConfigurationTest(getTestCtx internal.TestContextGetter) {
@@ -75,6 +135,403 @@ func ValidateIngressOperatorConfigurationTest(getTestCtx internal.TestContextGet
})
}
+func ServiceProviderDefaultIngressServingCertificateLifecycleTest(getTestCtx internal.TestContextGetter) {
+ When("a custom default ingress certificate is configured", Ordered, func() {
+ const certSecretName = "e2e-custom-ingress-cert"
+
+ var tc *internal.TestContext
+ var hcClient crclient.Client
+ var ingressDomain string
+ var certPEM, keyPEM []byte
+ var originalDefaultCert hyperv1.IngressDefaultCertificateReference
+
+ BeforeAll(func() {
+ tc = getTestCtx()
+
+ hc, err := tc.GetHostedCluster()
+ Expect(err).NotTo(HaveOccurred(), "failed to get HostedCluster")
+
+ // TODO: Remove this skip once the default ingress endpoint is reachable
+ // from the build farm during testing. The certificate propagation checks
+ // work over the guest API, but the TLS handshake steps dial the ingress
+ // canary route directly, which is not routable from CI on Azure today.
+ if hc.Spec.Platform.Type == hyperv1.AzurePlatform {
+ Skip("skipped on Azure until the default ingress endpoint is reachable from the build farm during testing")
+ }
+
+ // Capture the original defaultCertificate so AfterAll can restore it
+ // rather than unconditionally clearing a value the cluster arrived with.
+ if hc.Spec.OperatorConfiguration != nil && hc.Spec.OperatorConfiguration.IngressOperator != nil {
+ originalDefaultCert = hc.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate
+ }
+
+ hcClient, err = tc.GetHostedClusterClient(hc)
+ Expect(err).NotTo(HaveOccurred(), "failed to get hosted cluster client")
+
+ ingressDomain = ingressDomainForHostedCluster(hc)
+
+ certPEM, keyPEM, err = v2util.GenerateCustomCertificate(
+ []string{fmt.Sprintf("*.%s", ingressDomain)},
+ 24*time.Hour,
+ )
+ Expect(err).NotTo(HaveOccurred(), "failed to generate custom ingress certificate")
+ })
+
+ AfterAll(func() {
+ if tc == nil {
+ return
+ }
+ By("Restoring the original defaultCertificate on the HostedCluster")
+ hc, err := tc.GetHostedCluster()
+ if err != nil {
+ GinkgoWriter.Printf("WARNING: failed to get HostedCluster for cleanup: %v\n", err)
+ } else {
+ err = e2eutil.UpdateObject(GinkgoTB(), tc.Context, tc.MgmtClient, hc, func(obj *hyperv1.HostedCluster) {
+ if originalDefaultCert.Name != "" {
+ if obj.Spec.OperatorConfiguration == nil {
+ obj.Spec.OperatorConfiguration = &hyperv1.OperatorConfiguration{}
+ }
+ if obj.Spec.OperatorConfiguration.IngressOperator == nil {
+ obj.Spec.OperatorConfiguration.IngressOperator = &hyperv1.IngressOperatorSpec{}
+ }
+ obj.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate = originalDefaultCert
+ } else if obj.Spec.OperatorConfiguration != nil && obj.Spec.OperatorConfiguration.IngressOperator != nil {
+ obj.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate = hyperv1.IngressDefaultCertificateReference{}
+ }
+ })
+ if err != nil && !apierrors.IsNotFound(err) {
+ GinkgoWriter.Printf("WARNING: failed to restore defaultCertificate: %v\n", err)
+ }
+ }
+
+ By("Deleting the custom cert secret")
+ certSecret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: certSecretName,
+ Namespace: tc.ClusterNamespace,
+ },
+ }
+ err = tc.MgmtClient.Delete(tc.Context, certSecret)
+ if err != nil && !apierrors.IsNotFound(err) {
+ GinkgoWriter.Printf("WARNING: failed to delete cert secret: %v\n", err)
+ }
+ })
+
+ It("should create the cert secret and set defaultCertificate on the HostedCluster", Label(internal.InformingLabel), func() {
+ By("Creating the TLS secret in the HostedCluster namespace")
+ certSecret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: certSecretName,
+ Namespace: tc.ClusterNamespace,
+ },
+ Type: corev1.SecretTypeTLS,
+ Data: map[string][]byte{
+ corev1.TLSCertKey: certPEM,
+ corev1.TLSPrivateKeyKey: keyPEM,
+ },
+ }
+ err := tc.MgmtClient.Create(tc.Context, certSecret)
+ if apierrors.IsAlreadyExists(err) {
+ existing := &corev1.Secret{}
+ Expect(tc.MgmtClient.Get(tc.Context, crclient.ObjectKeyFromObject(certSecret), existing)).To(Succeed())
+ existing.Data = certSecret.Data
+ existing.Type = certSecret.Type
+ Expect(tc.MgmtClient.Update(tc.Context, existing)).To(Succeed())
+ } else {
+ Expect(err).NotTo(HaveOccurred(), "failed to create custom cert secret")
+ }
+
+ By("Setting defaultCertificate on the HostedCluster")
+ hc, err := tc.GetHostedCluster()
+ Expect(err).NotTo(HaveOccurred(), "failed to get HostedCluster")
+ Expect(e2eutil.UpdateObject(GinkgoTB(), tc.Context, tc.MgmtClient, hc, func(obj *hyperv1.HostedCluster) {
+ if obj.Spec.OperatorConfiguration == nil {
+ obj.Spec.OperatorConfiguration = &hyperv1.OperatorConfiguration{}
+ }
+ if obj.Spec.OperatorConfiguration.IngressOperator == nil {
+ obj.Spec.OperatorConfiguration.IngressOperator = &hyperv1.IngressOperatorSpec{}
+ }
+ obj.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate = hyperv1.IngressDefaultCertificateReference{
+ Name: certSecretName,
+ }
+ })).To(Succeed(), "failed to set defaultCertificate on HostedCluster")
+ })
+
+ It("should propagate the custom cert data to the hosted cluster's default-ingress-cert secret", Label(internal.InformingLabel), func() {
+ Eventually(func(g Gomega) {
+ hostedClusterSecret := &corev1.Secret{}
+ ref := manifests.IngressDefaultIngressControllerCert()
+ g.Expect(hcClient.Get(tc.Context, types.NamespacedName{
+ Namespace: ref.Namespace,
+ Name: ref.Name,
+ }, hostedClusterSecret)).To(Succeed())
+
+ g.Expect(hostedClusterSecret.Data[corev1.TLSCertKey]).To(Equal(certPEM),
+ "hosted cluster cert should match the user-provided cert")
+ g.Expect(hostedClusterSecret.Data[corev1.TLSPrivateKeyKey]).To(Equal(keyPEM),
+ "hosted cluster key should match the user-provided key")
+ }, 5*time.Minute, 10*time.Second).Should(Succeed())
+ })
+
+ It("should report the IngressDefaultCertificateSynced condition as True on the HostedCluster", Label(internal.InformingLabel), func() {
+ Eventually(func(g Gomega) {
+ hc, err := tc.GetHostedCluster()
+ g.Expect(err).NotTo(HaveOccurred())
+ cond := meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.IngressDefaultCertificateSynced))
+ g.Expect(cond).NotTo(BeNil(), "IngressDefaultCertificateSynced condition should be set")
+ g.Expect(cond.Status).To(Equal(metav1.ConditionTrue),
+ fmt.Sprintf("expected IngressDefaultCertificateSynced=True, got %s (%s: %s)", cond.Status, cond.Reason, cond.Message))
+ g.Expect(cond.Reason).To(Equal(hyperv1.AsExpectedReason))
+ }, 5*time.Minute, 10*time.Second).Should(Succeed())
+ })
+
+ It("should populate the observed-default-ingress-cert ConfigMap in the control plane namespace with the custom cert's CA", Label(internal.InformingLabel), func() {
+ Eventually(func(g Gomega) {
+ cm := cpomanifests.IngressObservedDefaultIngressCertCA(tc.ControlPlaneNamespace)
+ g.Expect(tc.MgmtClient.Get(tc.Context, crclient.ObjectKeyFromObject(cm), cm)).To(Succeed(), "observed-default-ingress-cert ConfigMap should exist in control plane namespace")
+
+ caData, ok := cm.Data["ca.crt"]
+ g.Expect(ok).To(BeTrue(), "observed-default-ingress-cert should have ca.crt key")
+ g.Expect(caData).NotTo(BeEmpty(), "ca.crt should not be empty")
+
+ certPool := x509.NewCertPool()
+ g.Expect(certPool.AppendCertsFromPEM([]byte(caData))).To(BeTrue(),
+ "ca.crt should contain valid PEM certificate data")
+ }, 10*time.Minute, 15*time.Second).Should(Succeed())
+ })
+
+ It("should serve a route with the custom cert verifiable by the CA from the management cluster", Label(internal.InformingLabel), func() {
+ By("Reading the observed CA from the management cluster")
+ var caBundle []byte
+ Eventually(func(g Gomega) {
+ cm := cpomanifests.IngressObservedDefaultIngressCertCA(tc.ControlPlaneNamespace)
+ g.Expect(tc.MgmtClient.Get(tc.Context, crclient.ObjectKeyFromObject(cm), cm)).To(Succeed())
+ caData, ok := cm.Data["ca.crt"]
+ g.Expect(ok).To(BeTrue())
+ caBundle = []byte(caData)
+ }, 2*time.Minute, 10*time.Second).Should(Succeed())
+
+ httpClient, err := newTLSClient(caBundle)
+ Expect(err).NotTo(HaveOccurred(), "failed to parse observed CA bundle")
+
+ url := canaryURL(ingressDomain)
+ By("Verifying TLS handshake against " + url)
+ Eventually(func(g Gomega) {
+ req, err := http.NewRequestWithContext(tc.Context, http.MethodGet, url, nil)
+ g.Expect(err).NotTo(HaveOccurred())
+ resp, err := httpClient.Do(req)
+ g.Expect(err).NotTo(HaveOccurred(), "TLS handshake should succeed using the observed CA from the management cluster")
+ defer resp.Body.Close()
+ g.Expect(resp.StatusCode).To(Equal(http.StatusOK))
+ }, 5*time.Minute, 10*time.Second).Should(Succeed())
+ })
+
+ It("should propagate rotated certificate data when the source secret is updated", Label(internal.InformingLabel), func() {
+ By("Generating a new certificate for rotation")
+ newCertPEM, newKeyPEM, err := v2util.GenerateCustomCertificate(
+ []string{fmt.Sprintf("*.%s", ingressDomain)},
+ 24*time.Hour,
+ )
+ Expect(err).NotTo(HaveOccurred(), "failed to generate rotated certificate")
+ Expect(newCertPEM).NotTo(Equal(certPEM), "rotated cert should differ from original")
+
+ By("Updating the source secret in the HostedCluster namespace")
+ Expect(e2eutil.UpdateObject(GinkgoTB(), tc.Context, tc.MgmtClient, &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: certSecretName,
+ Namespace: tc.ClusterNamespace,
+ },
+ }, func(obj *corev1.Secret) {
+ obj.Data[corev1.TLSCertKey] = newCertPEM
+ obj.Data[corev1.TLSPrivateKeyKey] = newKeyPEM
+ })).To(Succeed(), "failed to update cert secret for rotation")
+
+ By("Verifying the rotated cert appears in the hosted cluster")
+ Eventually(func(g Gomega) {
+ hostedClusterSecret := &corev1.Secret{}
+ ref := manifests.IngressDefaultIngressControllerCert()
+ g.Expect(hcClient.Get(tc.Context, types.NamespacedName{
+ Namespace: ref.Namespace,
+ Name: ref.Name,
+ }, hostedClusterSecret)).To(Succeed())
+
+ g.Expect(bytes.Equal(hostedClusterSecret.Data[corev1.TLSCertKey], newCertPEM)).To(BeTrue(),
+ "hosted cluster cert should match the rotated cert")
+ g.Expect(bytes.Equal(hostedClusterSecret.Data[corev1.TLSPrivateKeyKey], newKeyPEM)).To(BeTrue(),
+ "hosted cluster key should match the rotated key")
+ }, 5*time.Minute, 10*time.Second).Should(Succeed())
+
+ certPEM = newCertPEM
+ keyPEM = newKeyPEM
+
+ By("Verifying the observed CA in the management cluster updates for the rotated cert")
+ var rotatedCABundle []byte
+ Eventually(func(g Gomega) {
+ cm := cpomanifests.IngressObservedDefaultIngressCertCA(tc.ControlPlaneNamespace)
+ g.Expect(tc.MgmtClient.Get(tc.Context, crclient.ObjectKeyFromObject(cm), cm)).To(Succeed())
+ caData, ok := cm.Data["ca.crt"]
+ g.Expect(ok).To(BeTrue(), "observed-default-ingress-cert should have ca.crt key")
+ g.Expect(caData).NotTo(BeEmpty())
+
+ certPool := x509.NewCertPool()
+ g.Expect(certPool.AppendCertsFromPEM([]byte(caData))).To(BeTrue(),
+ "ca.crt should contain valid PEM certificate data")
+ rotatedCABundle = []byte(caData)
+ }, 10*time.Minute, 15*time.Second).Should(Succeed())
+
+ By("Verifying TLS handshake succeeds with the rotated CA from the management cluster")
+ httpClient, err := newTLSClient(rotatedCABundle)
+ Expect(err).NotTo(HaveOccurred(), "failed to parse rotated CA bundle")
+ url := canaryURL(ingressDomain)
+ Eventually(func(g Gomega) {
+ req, err := http.NewRequestWithContext(tc.Context, http.MethodGet, url, nil)
+ g.Expect(err).NotTo(HaveOccurred())
+ resp, err := httpClient.Do(req)
+ g.Expect(err).NotTo(HaveOccurred(), "TLS handshake should succeed with rotated CA from management cluster")
+ defer resp.Body.Close()
+ g.Expect(resp.StatusCode).To(Equal(http.StatusOK))
+ }, 5*time.Minute, 10*time.Second).Should(Succeed())
+ })
+
+ It("should report InvalidCertificateSecret and preserve the served certificate when the source secret is missing tls.key", Label(internal.InformingLabel), func() {
+ const badSecretName = "e2e-custom-ingress-cert-invalid"
+ ref := manifests.IngressDefaultIngressControllerCert()
+
+ By("Capturing the certificate currently served in the hosted cluster")
+ var servedCert []byte
+ Eventually(func(g Gomega) {
+ hostedClusterSecret := &corev1.Secret{}
+ g.Expect(hcClient.Get(tc.Context, types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, hostedClusterSecret)).To(Succeed())
+ g.Expect(hostedClusterSecret.Data[corev1.TLSCertKey]).NotTo(BeEmpty())
+ servedCert = append([]byte(nil), hostedClusterSecret.Data[corev1.TLSCertKey]...)
+ }, 2*time.Minute, 10*time.Second).Should(Succeed())
+
+ By("Creating a malformed Opaque source secret that is missing tls.key")
+ badSecret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: badSecretName, Namespace: tc.ClusterNamespace},
+ Type: corev1.SecretTypeOpaque,
+ Data: map[string][]byte{corev1.TLSCertKey: certPEM},
+ }
+ Expect(tc.MgmtClient.Create(tc.Context, badSecret)).To(Succeed(), "failed to create malformed source secret")
+ DeferCleanup(func() {
+ if err := tc.MgmtClient.Delete(tc.Context, badSecret); err != nil && !apierrors.IsNotFound(err) {
+ GinkgoWriter.Printf("WARNING: failed to delete malformed source secret: %v\n", err)
+ }
+ })
+
+ By("Pointing defaultCertificate at the malformed secret")
+ hc, err := tc.GetHostedCluster()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(e2eutil.UpdateObject(GinkgoTB(), tc.Context, tc.MgmtClient, hc, func(obj *hyperv1.HostedCluster) {
+ obj.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate = hyperv1.IngressDefaultCertificateReference{Name: badSecretName}
+ })).To(Succeed())
+
+ By("Verifying the HostedCluster reports IngressDefaultCertificateSynced=False with reason InvalidCertificateSecret")
+ Eventually(func(g Gomega) {
+ hc, err := tc.GetHostedCluster()
+ g.Expect(err).NotTo(HaveOccurred())
+ cond := meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.IngressDefaultCertificateSynced))
+ g.Expect(cond).NotTo(BeNil(), "IngressDefaultCertificateSynced condition should be set")
+ g.Expect(cond.Status).To(Equal(metav1.ConditionFalse),
+ fmt.Sprintf("expected IngressDefaultCertificateSynced=False, got %s (%s: %s)", cond.Status, cond.Reason, cond.Message))
+ g.Expect(cond.Reason).To(Equal(hyperv1.IngressDefaultCertificateInvalidReason))
+ }, 5*time.Minute, 10*time.Second).Should(Succeed())
+
+ By("Verifying the previously served certificate is preserved while the source is invalid")
+ Consistently(func(g Gomega) {
+ hostedClusterSecret := &corev1.Secret{}
+ g.Expect(hcClient.Get(tc.Context, types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, hostedClusterSecret)).To(Succeed())
+ g.Expect(bytes.Equal(hostedClusterSecret.Data[corev1.TLSCertKey], servedCert)).To(BeTrue(),
+ "the previously served certificate should remain in place while the source secret is invalid")
+ }, 1*time.Minute, 10*time.Second).Should(Succeed())
+
+ By("Restoring defaultCertificate to the valid source secret")
+ hc, err = tc.GetHostedCluster()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(e2eutil.UpdateObject(GinkgoTB(), tc.Context, tc.MgmtClient, hc, func(obj *hyperv1.HostedCluster) {
+ obj.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate = hyperv1.IngressDefaultCertificateReference{Name: certSecretName}
+ })).To(Succeed())
+ })
+
+ It("should preserve the last synced certificate and report SecretNotFound when the source secret is deleted", Label(internal.InformingLabel), func() {
+ ref := manifests.IngressDefaultIngressControllerCert()
+
+ By("Capturing the certificate currently served in the hosted cluster")
+ var lastCert []byte
+ Eventually(func(g Gomega) {
+ hostedClusterSecret := &corev1.Secret{}
+ g.Expect(hcClient.Get(tc.Context, types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, hostedClusterSecret)).To(Succeed())
+ g.Expect(hostedClusterSecret.Data[corev1.TLSCertKey]).NotTo(BeEmpty())
+ lastCert = append([]byte(nil), hostedClusterSecret.Data[corev1.TLSCertKey]...)
+ }, 2*time.Minute, 10*time.Second).Should(Succeed())
+
+ By("Deleting the source secret while defaultCertificate is still set")
+ Expect(tc.MgmtClient.Delete(tc.Context, &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: certSecretName, Namespace: tc.ClusterNamespace},
+ })).To(Succeed(), "failed to delete source cert secret")
+
+ By("Verifying the HostedCluster reports IngressDefaultCertificateSynced=False with reason SecretNotFound")
+ Eventually(func(g Gomega) {
+ hc, err := tc.GetHostedCluster()
+ g.Expect(err).NotTo(HaveOccurred())
+ cond := meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.IngressDefaultCertificateSynced))
+ g.Expect(cond).NotTo(BeNil(), "IngressDefaultCertificateSynced condition should be set")
+ g.Expect(cond.Status).To(Equal(metav1.ConditionFalse),
+ fmt.Sprintf("expected IngressDefaultCertificateSynced=False, got %s (%s: %s)", cond.Status, cond.Reason, cond.Message))
+ g.Expect(cond.Reason).To(Equal(hyperv1.SecretNotFoundReason))
+ }, 5*time.Minute, 10*time.Second).Should(Succeed())
+
+ By("Verifying the previously synced certificate remains in place in the hosted cluster")
+ Consistently(func(g Gomega) {
+ hostedClusterSecret := &corev1.Secret{}
+ g.Expect(hcClient.Get(tc.Context, types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, hostedClusterSecret)).To(Succeed())
+ g.Expect(bytes.Equal(hostedClusterSecret.Data[corev1.TLSCertKey], lastCert)).To(BeTrue(),
+ "the last synced certificate should remain in place after the source secret is deleted")
+ }, 1*time.Minute, 10*time.Second).Should(Succeed())
+ })
+
+ It("should revert to the generated wildcard certificate when defaultCertificate is cleared", Label(internal.InformingLabel), func() {
+ ref := manifests.IngressDefaultIngressControllerCert()
+
+ By("Capturing the custom certificate currently served")
+ var customCert []byte
+ Eventually(func(g Gomega) {
+ hostedClusterSecret := &corev1.Secret{}
+ g.Expect(hcClient.Get(tc.Context, types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, hostedClusterSecret)).To(Succeed())
+ g.Expect(hostedClusterSecret.Data[corev1.TLSCertKey]).NotTo(BeEmpty())
+ customCert = append([]byte(nil), hostedClusterSecret.Data[corev1.TLSCertKey]...)
+ }, 2*time.Minute, 10*time.Second).Should(Succeed())
+
+ By("Clearing defaultCertificate on the HostedCluster")
+ hc, err := tc.GetHostedCluster()
+ Expect(err).NotTo(HaveOccurred())
+ Expect(e2eutil.UpdateObject(GinkgoTB(), tc.Context, tc.MgmtClient, hc, func(obj *hyperv1.HostedCluster) {
+ if obj.Spec.OperatorConfiguration != nil && obj.Spec.OperatorConfiguration.IngressOperator != nil {
+ obj.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate = hyperv1.IngressDefaultCertificateReference{}
+ }
+ })).To(Succeed(), "failed to clear defaultCertificate on HostedCluster")
+
+ By("Verifying the hosted cluster reverts to the generated wildcard certificate")
+ Eventually(func(g Gomega) {
+ hostedClusterSecret := &corev1.Secret{}
+ g.Expect(hcClient.Get(tc.Context, types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, hostedClusterSecret)).To(Succeed())
+ g.Expect(hostedClusterSecret.Data[corev1.TLSCertKey]).NotTo(BeEmpty(),
+ "a generated wildcard certificate should be present after clearing defaultCertificate")
+ g.Expect(bytes.Equal(hostedClusterSecret.Data[corev1.TLSCertKey], customCert)).To(BeFalse(),
+ "default-ingress-cert should no longer contain the custom certificate after clearing defaultCertificate")
+ }, 10*time.Minute, 15*time.Second).Should(Succeed())
+
+ By("Verifying the IngressDefaultCertificateSynced condition is cleared")
+ Eventually(func(g Gomega) {
+ hc, err := tc.GetHostedCluster()
+ g.Expect(err).NotTo(HaveOccurred())
+ cond := meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.IngressDefaultCertificateSynced))
+ g.Expect(cond).To(BeNil(), "IngressDefaultCertificateSynced condition should be removed once defaultCertificate is cleared")
+ }, 5*time.Minute, 10*time.Second).Should(Succeed())
+ })
+ })
+}
+
var _ = Describe("[sig-hypershift][Jira:Hypershift][Feature:Ingress] Hosted Cluster Ingress", Label("hosted-cluster-ingress"), func() {
var testCtx *internal.TestContext
diff --git a/test/e2e/v2/util/certs.go b/test/e2e/v2/util/certs.go
new file mode 100644
index 000000000000..afaa66549f05
--- /dev/null
+++ b/test/e2e/v2/util/certs.go
@@ -0,0 +1,33 @@
+package util
+
+import (
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "fmt"
+ "time"
+
+ "github.com/openshift/hypershift/support/certs"
+)
+
+// GenerateCustomCertificate generates a self-signed certificate for the given DNS names.
+func GenerateCustomCertificate(dnsNames []string, validity time.Duration) ([]byte, []byte, error) {
+ if len(dnsNames) == 0 {
+ return nil, nil, fmt.Errorf("no DNS names provided")
+ }
+
+ cfg := &certs.CertCfg{
+ Subject: pkix.Name{CommonName: dnsNames[0], Organization: []string{"kubernetes"}, OrganizationalUnit: []string{"test"}},
+ KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+ ExtKeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ Validity: validity,
+ DNSNames: dnsNames,
+ IsCA: false,
+ }
+
+ key, crt, err := certs.GenerateSelfSignedCertificate(cfg)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to generate self-signed certificate: %w", err)
+ }
+
+ return certs.CertToPem(crt), certs.PrivateKeyToPem(key), nil
+}
From 3a1eba85a7c3d5032bc82eec2b3da0f3ea572732 Mon Sep 17 00:00:00 2001
From: David Eads
Date: Fri, 4 Sep 2026 11:12:46 -0400
Subject: [PATCH 6/7] docs: regenerate API reference for ingress default
certificate
Regenerate the API reference and aggregated docs for the defaultCertificate
field and the IngressDefaultCertificateSynced condition.
Signed-off-by: David Eads
Commit-Message-Assisted-by: Claude (via Claude Code)
Co-Authored-By: Claude Opus 4.8 (1M context)
(cherry picked from commit 01dee99f9b9c0142b306727685851755dbf7c395)
---
docs/content/reference/aggregated-docs.md | 77 +++++++++++++++++++++++
docs/content/reference/api.md | 77 +++++++++++++++++++++++
2 files changed, 154 insertions(+)
diff --git a/docs/content/reference/aggregated-docs.md b/docs/content/reference/aggregated-docs.md
index 35841437169d..a97152da7959 100644
--- a/docs/content/reference/aggregated-docs.md
+++ b/docs/content/reference/aggregated-docs.md
@@ -45396,6 +45396,18 @@ and reports missing images if any.
e.g. load balancers were created successfully.
A failure here may require external user intervention to resolve. E.g. hitting quotas on the cloud provider.
+"IngressDefaultCertificateSynced" |
+IngressDefaultCertificateSynced indicates whether the user-provided default
+ingress certificate referenced by
+spec.operatorConfiguration.ingressOperator.defaultCertificate has been
+synced from the HostedCluster namespace into the control plane namespace.
+True means the referenced Secret was found, contains tls.crt and tls.key,
+and its data was synced.
+False means the referenced Secret is missing or malformed; in that case
+the previously synced certificate (or the auto-generated wildcard certificate)
+keeps serving and the HostedCluster does not become degraded.
+The condition is absent when no defaultCertificate is configured.
+ |
"KubeAPIServerAvailable" |
KubeAPIServerAvailable bubbles up the same condition from HCP. It signals if the kube API server is available.
A failure here often means a software bug or a non-stable cluster.
@@ -50542,6 +50554,41 @@ the update is at least 70% of desired nodes.
|
+###IngressDefaultCertificateReference { #hypershift.openshift.io/v1beta1.IngressDefaultCertificateReference }
+
+(Appears on:
+IngressOperatorSpec)
+
+
+
IngressDefaultCertificateReference contains a reference to a TLS Secret
+in the HostedCluster namespace used as the default serving certificate
+for the ingress controller.
+
+
+
+
+| Field |
+Description |
+
+
+
+
+
+name
+
+string
+
+ |
+
+ name is the name of the Secret containing tls.crt and tls.key.
+The Secret must exist in the same namespace as the HostedCluster.
+name must be a valid DNS subdomain name (RFC 1123): it must contain only
+lowercase alphanumeric characters, ‘-’ or ‘.’, and start and end with an
+alphanumeric character.
+ |
+
+
+
###IngressOperatorSpec { #hypershift.openshift.io/v1beta1.IngressOperatorSpec }
(Appears on:
@@ -50589,6 +50636,36 @@ LoadBalancerService with External scope
https://github.com/openshift/api/blob/master/operator/v1/types_ingress.go
+
+
+defaultCertificate,omitzero
+
+
+IngressDefaultCertificateReference
+
+
+ |
+
+(Optional)
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+that contains the default certificate served by the default ingress controller.
+When Routes don’t specify their own certificate, defaultCertificate is used.
+The secret must contain the following keys and data:
+tls.crt: certificate file contents
+tls.key: key file contents
+When set, this certificate replaces the auto-generated wildcard certificate
+that is normally created by the control plane operator. The secret is synced
+from the HostedCluster namespace to the control plane, and then propagated
+to the hosted cluster’s openshift-ingress namespace.
+When the referenced secret is updated, the new certificate data is
+automatically propagated to the hosted cluster.
+When not set, the control plane operator generates a wildcard certificate
+signed by the cluster’s root CA.
+Note: a cluster-admin in the hosted cluster can override the default ingress
+controller’s certificate directly. That override takes precedence and the
+certificate referenced here is no longer served.
+ |
+
###InstanceType { #hypershift.openshift.io/v1beta1.InstanceType }
diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md
index e799ffb6d914..f1a962775a97 100644
--- a/docs/content/reference/api.md
+++ b/docs/content/reference/api.md
@@ -6283,6 +6283,18 @@ and reports missing images if any.
e.g. load balancers were created successfully.
A failure here may require external user intervention to resolve. E.g. hitting quotas on the cloud provider.
+"IngressDefaultCertificateSynced" |
+IngressDefaultCertificateSynced indicates whether the user-provided default
+ingress certificate referenced by
+spec.operatorConfiguration.ingressOperator.defaultCertificate has been
+synced from the HostedCluster namespace into the control plane namespace.
+True means the referenced Secret was found, contains tls.crt and tls.key,
+and its data was synced.
+False means the referenced Secret is missing or malformed; in that case
+the previously synced certificate (or the auto-generated wildcard certificate)
+keeps serving and the HostedCluster does not become degraded.
+The condition is absent when no defaultCertificate is configured.
+ |
"KubeAPIServerAvailable" |
KubeAPIServerAvailable bubbles up the same condition from HCP. It signals if the kube API server is available.
A failure here often means a software bug or a non-stable cluster.
@@ -11429,6 +11441,41 @@ the update is at least 70% of desired nodes.
|
+###IngressDefaultCertificateReference { #hypershift.openshift.io/v1beta1.IngressDefaultCertificateReference }
+
+(Appears on:
+IngressOperatorSpec)
+
+
+
IngressDefaultCertificateReference contains a reference to a TLS Secret
+in the HostedCluster namespace used as the default serving certificate
+for the ingress controller.
+
+
+
+
+| Field |
+Description |
+
+
+
+
+
+name
+
+string
+
+ |
+
+ name is the name of the Secret containing tls.crt and tls.key.
+The Secret must exist in the same namespace as the HostedCluster.
+name must be a valid DNS subdomain name (RFC 1123): it must contain only
+lowercase alphanumeric characters, ‘-’ or ‘.’, and start and end with an
+alphanumeric character.
+ |
+
+
+
###IngressOperatorSpec { #hypershift.openshift.io/v1beta1.IngressOperatorSpec }
(Appears on:
@@ -11476,6 +11523,36 @@ LoadBalancerService with External scope
https://github.com/openshift/api/blob/master/operator/v1/types_ingress.go
+
+
+defaultCertificate,omitzero
+
+
+IngressDefaultCertificateReference
+
+
+ |
+
+(Optional)
+ defaultCertificate is a reference to a secret in the HostedCluster namespace
+that contains the default certificate served by the default ingress controller.
+When Routes don’t specify their own certificate, defaultCertificate is used.
+The secret must contain the following keys and data:
+tls.crt: certificate file contents
+tls.key: key file contents
+When set, this certificate replaces the auto-generated wildcard certificate
+that is normally created by the control plane operator. The secret is synced
+from the HostedCluster namespace to the control plane, and then propagated
+to the hosted cluster’s openshift-ingress namespace.
+When the referenced secret is updated, the new certificate data is
+automatically propagated to the hosted cluster.
+When not set, the control plane operator generates a wildcard certificate
+signed by the cluster’s root CA.
+Note: a cluster-admin in the hosted cluster can override the default ingress
+controller’s certificate directly. That override takes precedence and the
+certificate referenced here is no longer served.
+ |
+
###InstanceType { #hypershift.openshift.io/v1beta1.InstanceType }
From 39891d84ab9b953bb63e8848ef7d47f089e2a481 Mon Sep 17 00:00:00 2001
From: David Eads
Date: Tue, 8 Sep 2026 09:06:21 -0400
Subject: [PATCH 7/7] test(e2e): verify ingress certificates are not copied
when ingress is disabled
Assert that both destination secrets and the synced condition remain absent
after configuring a valid source certificate. Skip the enabled-ingress
lifecycle tests safely when ingress is disabled.
Signed-off-by: David Eads
(cherry picked from commit 57221be4ab833298fee23d1dd7510529d8283a8a)
---
.../v2/tests/hosted_cluster_ingress_test.go | 84 ++++++++++++++++++-
1 file changed, 82 insertions(+), 2 deletions(-)
diff --git a/test/e2e/v2/tests/hosted_cluster_ingress_test.go b/test/e2e/v2/tests/hosted_cluster_ingress_test.go
index c81b4638fe38..ce3686607ce1 100644
--- a/test/e2e/v2/tests/hosted_cluster_ingress_test.go
+++ b/test/e2e/v2/tests/hosted_cluster_ingress_test.go
@@ -31,6 +31,7 @@ import (
hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests"
"github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/controllers/resources/manifests"
+ "github.com/openshift/hypershift/support/capabilities"
e2eutil "github.com/openshift/hypershift/test/e2e/util"
"github.com/openshift/hypershift/test/e2e/v2/internal"
v2util "github.com/openshift/hypershift/test/e2e/v2/util"
@@ -136,6 +137,78 @@ func ValidateIngressOperatorConfigurationTest(getTestCtx internal.TestContextGet
}
func ServiceProviderDefaultIngressServingCertificateLifecycleTest(getTestCtx internal.TestContextGetter) {
+ When("the ingress capability is disabled", func() {
+ It("should not copy a configured default ingress certificate to the control plane or hosted cluster", Label(internal.InformingLabel), func() {
+ tc := getTestCtx()
+ hc, err := tc.GetHostedCluster()
+ Expect(err).NotTo(HaveOccurred(), "failed to get HostedCluster")
+ if capabilities.IsIngressCapabilityEnabled(hc.Spec.Capabilities) {
+ Skip("Ingress capability must be disabled on the HostedCluster")
+ }
+ hcClient, err := tc.GetHostedClusterClient(hc)
+ Expect(err).NotTo(HaveOccurred(), "failed to get hosted cluster client")
+
+ By("Creating a valid source certificate while ingress is disabled")
+ certPEM, keyPEM, err := v2util.GenerateCustomCertificate(
+ []string{fmt.Sprintf("*.%s", ingressDomainForHostedCluster(hc))}, 24*time.Hour)
+ Expect(err).NotTo(HaveOccurred(), "failed to generate custom ingress certificate")
+ secret := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{GenerateName: "e2e-disabled-ingress-cert-", Namespace: tc.ClusterNamespace},
+ Type: corev1.SecretTypeTLS,
+ Data: map[string][]byte{corev1.TLSCertKey: certPEM, corev1.TLSPrivateKeyKey: keyPEM},
+ }
+ Expect(tc.MgmtClient.Create(tc.Context, secret)).To(Succeed())
+ DeferCleanup(func() {
+ if err := tc.MgmtClient.Delete(tc.Context, secret); !apierrors.IsNotFound(err) {
+ Expect(err).NotTo(HaveOccurred(), "cleanup: failed to delete source certificate")
+ }
+ })
+
+ originalOperatorConfig := hc.Spec.OperatorConfiguration.DeepCopy()
+ DeferCleanup(func() {
+ Expect(e2eutil.UpdateObject(GinkgoTB(), tc.Context, tc.MgmtClient, hc, func(obj *hyperv1.HostedCluster) {
+ obj.Spec.OperatorConfiguration = originalOperatorConfig
+ })).To(Succeed(), "cleanup: failed to restore operator configuration")
+ })
+ By("Referencing the source certificate on the HostedCluster")
+ Expect(e2eutil.UpdateObject(GinkgoTB(), tc.Context, tc.MgmtClient, hc, func(obj *hyperv1.HostedCluster) {
+ if obj.Spec.OperatorConfiguration == nil {
+ obj.Spec.OperatorConfiguration = &hyperv1.OperatorConfiguration{}
+ }
+ if obj.Spec.OperatorConfiguration.IngressOperator == nil {
+ obj.Spec.OperatorConfiguration.IngressOperator = &hyperv1.IngressOperatorSpec{}
+ }
+ obj.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate = hyperv1.IngressDefaultCertificateReference{Name: secret.Name}
+ })).To(Succeed())
+
+ By("Waiting for the certificate reference to reach the HostedControlPlane")
+ Eventually(func(g Gomega) {
+ hcp := &hyperv1.HostedControlPlane{}
+ g.Expect(tc.MgmtClient.Get(tc.Context, types.NamespacedName{Namespace: tc.ControlPlaneNamespace, Name: hc.Name}, hcp)).To(Succeed())
+ g.Expect(capabilities.IsIngressCapabilityEnabled(hcp.Spec.Capabilities)).To(BeFalse())
+ g.Expect(hcp.Spec.OperatorConfiguration).NotTo(BeNil())
+ g.Expect(hcp.Spec.OperatorConfiguration.IngressOperator).NotTo(BeNil())
+ g.Expect(hcp.Spec.OperatorConfiguration.IngressOperator.DefaultCertificate.Name).To(Equal(secret.Name))
+ }, 5*time.Minute, 10*time.Second).Should(Succeed())
+
+ By("Verifying neither controller copies the certificate or reports it synced")
+ Consistently(func(g Gomega) {
+ controlPlaneSecret := cpomanifests.ServiceProviderDefaultIngressServingCert(tc.ControlPlaneNamespace)
+ err := tc.MgmtClient.Get(tc.Context, crclient.ObjectKeyFromObject(controlPlaneSecret), controlPlaneSecret)
+ g.Expect(apierrors.IsNotFound(err)).To(BeTrue(), "control plane certificate secret %s must remain absent, got: %v", crclient.ObjectKeyFromObject(controlPlaneSecret), err)
+
+ hostedClusterSecret := manifests.IngressDefaultIngressControllerCert()
+ err = hcClient.Get(tc.Context, crclient.ObjectKeyFromObject(hostedClusterSecret), hostedClusterSecret)
+ g.Expect(apierrors.IsNotFound(err)).To(BeTrue(), "hosted cluster certificate secret %s must remain absent, got: %v", crclient.ObjectKeyFromObject(hostedClusterSecret), err)
+
+ currentHC, err := tc.GetHostedCluster()
+ g.Expect(err).NotTo(HaveOccurred())
+ g.Expect(meta.FindStatusCondition(currentHC.Status.Conditions, string(hyperv1.IngressDefaultCertificateSynced))).To(BeNil(),
+ "IngressDefaultCertificateSynced condition must remain absent while ingress is disabled")
+ }, 1*time.Minute, 10*time.Second).Should(Succeed())
+ })
+ })
+
When("a custom default ingress certificate is configured", Ordered, func() {
const certSecretName = "e2e-custom-ingress-cert"
@@ -146,11 +219,15 @@ func ServiceProviderDefaultIngressServingCertificateLifecycleTest(getTestCtx int
var originalDefaultCert hyperv1.IngressDefaultCertificateReference
BeforeAll(func() {
- tc = getTestCtx()
+ candidateTC := getTestCtx()
- hc, err := tc.GetHostedCluster()
+ hc, err := candidateTC.GetHostedCluster()
Expect(err).NotTo(HaveOccurred(), "failed to get HostedCluster")
+ if !capabilities.IsIngressCapabilityEnabled(hc.Spec.Capabilities) {
+ Skip("Ingress capability is disabled on the HostedCluster")
+ }
+
// TODO: Remove this skip once the default ingress endpoint is reachable
// from the build farm during testing. The certificate propagation checks
// work over the guest API, but the TLS handshake steps dial the ingress
@@ -159,6 +236,9 @@ func ServiceProviderDefaultIngressServingCertificateLifecycleTest(getTestCtx int
Skip("skipped on Azure until the default ingress endpoint is reachable from the build farm during testing")
}
+ // Enable cleanup only after the capability and platform guards pass.
+ tc = candidateTC
+
// Capture the original defaultCertificate so AfterAll can restore it
// rather than unconditionally clearing a value the cluster arrived with.
if hc.Spec.OperatorConfiguration != nil && hc.Spec.OperatorConfiguration.IngressOperator != nil {