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.

+

+ + + + + + + + + + + + + +
FieldDescription
+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.

+

+ + + + + + + + + + + + + +
FieldDescription
+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 {