diff --git a/Dockerfile.control-plane b/Dockerfile.control-plane index 6c83ab781edc..0de5fe62e55f 100644 --- a/Dockerfile.control-plane +++ b/Dockerfile.control-plane @@ -26,3 +26,4 @@ LABEL io.openshift.hypershift.control-plane-operator-applies-management-kas-netw LABEL io.openshift.hypershift.restricted-psa=true LABEL io.openshift.hypershift.control-plane-pki-operator-signs-csrs=true LABEL io.openshift.hypershift.hosted-cluster-config-operator-reports-node-count=true +LABEL io.openshift.hypershift.control-plane-operator-supports-kas-custom-kubeconfig=true diff --git a/Dockerfile.dev b/Dockerfile.dev index 8690df5a4304..54dd99fb9d49 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -36,4 +36,5 @@ LABEL io.openshift.hypershift.control-plane-operator-creates-aws-sg=true LABEL io.openshift.hypershift.control-plane-operator-applies-management-kas-network-policy-label=true LABEL io.openshift.hypershift.restricted-psa=true LABEL io.openshift.hypershift.control-plane-pki-operator-signs-csrs=true -LABEL io.openshift.hypershift.hosted-cluster-config-operator-reports-node-count=true \ No newline at end of file +LABEL io.openshift.hypershift.hosted-cluster-config-operator-reports-node-count=true +LABEL io.openshift.hypershift.control-plane-operator-supports-kas-custom-kubeconfig=true \ No newline at end of file diff --git a/api/hypershift/v1beta1/hosted_controlplane.go b/api/hypershift/v1beta1/hosted_controlplane.go index e227344d0c75..035c6f758701 100644 --- a/api/hypershift/v1beta1/hosted_controlplane.go +++ b/api/hypershift/v1beta1/hosted_controlplane.go @@ -115,6 +115,20 @@ type HostedControlPlaneSpec struct { // +optional KubeConfig *KubeconfigSecretRef `json:"kubeconfig,omitempty"` + // kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + // When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + // If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + // The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + // This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + // access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + // for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + // + // +kubebuilder:validation:XValidation:rule=`self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$')`,message="kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com)" + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:example: "api.example.com" + // +optional + KubeAPIServerDNSName string `json:"kubeAPIServerDNSName,omitempty"` + // Services defines metadata about how control plane services are published // in the management cluster. // +kubebuilder:validation:MaxItems=6 @@ -314,6 +328,15 @@ type HostedControlPlaneStatus struct { // for this control plane. KubeConfig *KubeconfigSecretRef `json:"kubeConfig,omitempty"` + // customKubeconfig references an external custom kubeconfig secret. + // This field is populated in the status when a custom kubeconfig secret has been generated + // for the hosted cluster. It contains the name and key of the secret located in the + // hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + // If this field is removed during a day 2 operation, the referenced secret will be deleted + // and this field will be removed from the hostedCluster status. + // +optional + CustomKubeconfig *KubeconfigSecretRef `json:"customKubeconfig,omitempty"` + // KubeadminPassword is a reference to the secret containing the initial kubeadmin password // for the guest cluster. // +optional diff --git a/api/hypershift/v1beta1/hostedcluster_types.go b/api/hypershift/v1beta1/hostedcluster_types.go index a018bb26f91a..f4fb6eb8ba80 100644 --- a/api/hypershift/v1beta1/hostedcluster_types.go +++ b/api/hypershift/v1beta1/hostedcluster_types.go @@ -471,6 +471,20 @@ type HostedClusterSpec struct { // +required Platform PlatformSpec `json:"platform"` + // kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + // When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + // If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + // The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + // This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + // access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + // for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + // This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + // +kubebuilder:validation:XValidation:rule=`self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$')`,message="kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com)" + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:example: "api.example.com" + // +optional + KubeAPIServerDNSName string `json:"kubeAPIServerDNSName,omitempty"` + // controllerAvailabilityPolicy specifies the availability policy applied to critical control plane components like the Kube API Server. // Possible values are HighlyAvailable and SingleReplica. The default value is HighlyAvailable. // This field is immutable. @@ -1478,6 +1492,11 @@ type HostedClusterStatus struct { // +optional KubeConfig *corev1.LocalObjectReference `json:"kubeconfig,omitempty"` + // CustomKubeconfig is a local secret reference to the external custom kubeconfig. + // Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + // +optional + CustomKubeconfig *corev1.LocalObjectReference `json:"customKubeconfig,omitempty"` + // KubeadminPassword is a reference to the secret that contains the initial // kubeadmin user password for the guest cluster. // +optional diff --git a/api/hypershift/v1beta1/zz_generated.deepcopy.go b/api/hypershift/v1beta1/zz_generated.deepcopy.go index eb752dddb349..7f27701145d2 100644 --- a/api/hypershift/v1beta1/zz_generated.deepcopy.go +++ b/api/hypershift/v1beta1/zz_generated.deepcopy.go @@ -1505,6 +1505,11 @@ func (in *HostedClusterStatus) DeepCopyInto(out *HostedClusterStatus) { *out = new(corev1.LocalObjectReference) **out = **in } + if in.CustomKubeconfig != nil { + in, out := &in.CustomKubeconfig, &out.CustomKubeconfig + *out = new(corev1.LocalObjectReference) + **out = **in + } if in.KubeadminPassword != nil { in, out := &in.KubeadminPassword, &out.KubeadminPassword *out = new(corev1.LocalObjectReference) @@ -1729,6 +1734,11 @@ func (in *HostedControlPlaneStatus) DeepCopyInto(out *HostedControlPlaneStatus) *out = new(KubeconfigSecretRef) **out = **in } + if in.CustomKubeconfig != nil { + in, out := &in.CustomKubeconfig, &out.CustomKubeconfig + *out = new(KubeconfigSecretRef) + **out = **in + } if in.KubeadminPassword != nil { in, out := &in.KubeadminPassword, &out.KubeadminPassword *out = new(corev1.LocalObjectReference) 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 79a0d993e25e..c522eb7c9b5c 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 @@ -2393,6 +2393,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -4833,6 +4848,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AutoNodeKarpenter.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AutoNodeKarpenter.yaml index 8d919c0110e1..51f2e3edbcea 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AutoNodeKarpenter.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/AutoNodeKarpenter.yaml @@ -2434,6 +2434,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -4866,6 +4881,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. 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 cb207c1a9968..f410e1e635cc 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 @@ -2389,6 +2389,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -4846,6 +4861,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/DynamicResourceAllocation.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/DynamicResourceAllocation.yaml index ad1ed2ee42a8..d1c31d5ff763 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/DynamicResourceAllocation.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/DynamicResourceAllocation.yaml @@ -2410,6 +2410,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -4842,6 +4857,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. 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 93d23eafde45..df2e48092cc5 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 @@ -2631,6 +2631,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -5063,6 +5078,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. 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 961fb8ab1199..fead335c08ef 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 @@ -2407,6 +2407,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -4839,6 +4854,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/NetworkDiagnosticsConfig.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/NetworkDiagnosticsConfig.yaml index 855c1246ee1f..c68ec8904d40 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/NetworkDiagnosticsConfig.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/NetworkDiagnosticsConfig.yaml @@ -2541,6 +2541,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -4973,6 +4988,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. 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 3bf69795dc9b..6af246a0a33c 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 @@ -2389,6 +2389,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -5309,6 +5324,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. 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 d9e4daf592c4..b6de43012e04 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 @@ -2287,6 +2287,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -4654,6 +4668,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AutoNodeKarpenter.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AutoNodeKarpenter.yaml index 8b790f19b563..f20162a834bc 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AutoNodeKarpenter.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/AutoNodeKarpenter.yaml @@ -2328,6 +2328,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -4687,6 +4701,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- 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 1f0bfe505592..730339732fe6 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 @@ -2283,6 +2283,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -4667,6 +4681,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/DynamicResourceAllocation.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/DynamicResourceAllocation.yaml index 3b356dffe91a..f07a4dc3c22e 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/DynamicResourceAllocation.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/DynamicResourceAllocation.yaml @@ -2304,6 +2304,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -4663,6 +4677,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- 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 8e8d4e98187e..000e8517cf06 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 @@ -2525,6 +2525,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -4884,6 +4898,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- 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 316c98503fa9..63554fbd6a7c 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 @@ -2301,6 +2301,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -4660,6 +4674,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/NetworkDiagnosticsConfig.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/NetworkDiagnosticsConfig.yaml index 01b740bc315b..cff71effe5d5 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/NetworkDiagnosticsConfig.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/NetworkDiagnosticsConfig.yaml @@ -2435,6 +2435,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -4794,6 +4808,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- 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 64e882e3651b..578b34d95dbf 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 @@ -2283,6 +2283,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -5130,6 +5144,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- diff --git a/client/applyconfiguration/hypershift/v1beta1/hostedclusterspec.go b/client/applyconfiguration/hypershift/v1beta1/hostedclusterspec.go index 9f37a60d8df4..7b6c8525a0b2 100644 --- a/client/applyconfiguration/hypershift/v1beta1/hostedclusterspec.go +++ b/client/applyconfiguration/hypershift/v1beta1/hostedclusterspec.go @@ -33,6 +33,7 @@ type HostedClusterSpecApplyConfiguration struct { UpdateService *v1.URL `json:"updateService,omitempty"` Channel *string `json:"channel,omitempty"` Platform *PlatformSpecApplyConfiguration `json:"platform,omitempty"` + KubeAPIServerDNSName *string `json:"kubeAPIServerDNSName,omitempty"` ControllerAvailabilityPolicy *hypershiftv1beta1.AvailabilityPolicy `json:"controllerAvailabilityPolicy,omitempty"` InfrastructureAvailabilityPolicy *hypershiftv1beta1.AvailabilityPolicy `json:"infrastructureAvailabilityPolicy,omitempty"` DNS *DNSSpecApplyConfiguration `json:"dns,omitempty"` @@ -122,6 +123,14 @@ func (b *HostedClusterSpecApplyConfiguration) WithPlatform(value *PlatformSpecAp return b } +// WithKubeAPIServerDNSName sets the KubeAPIServerDNSName 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 KubeAPIServerDNSName field is set to the value of the last call. +func (b *HostedClusterSpecApplyConfiguration) WithKubeAPIServerDNSName(value string) *HostedClusterSpecApplyConfiguration { + b.KubeAPIServerDNSName = &value + return b +} + // WithControllerAvailabilityPolicy sets the ControllerAvailabilityPolicy 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 ControllerAvailabilityPolicy field is set to the value of the last call. diff --git a/client/applyconfiguration/hypershift/v1beta1/hostedclusterstatus.go b/client/applyconfiguration/hypershift/v1beta1/hostedclusterstatus.go index 433804746c8c..01c66e5dbc15 100644 --- a/client/applyconfiguration/hypershift/v1beta1/hostedclusterstatus.go +++ b/client/applyconfiguration/hypershift/v1beta1/hostedclusterstatus.go @@ -28,6 +28,7 @@ import ( type HostedClusterStatusApplyConfiguration struct { Version *ClusterVersionStatusApplyConfiguration `json:"version,omitempty"` KubeConfig *v1.LocalObjectReference `json:"kubeconfig,omitempty"` + CustomKubeconfig *v1.LocalObjectReference `json:"customKubeconfig,omitempty"` KubeadminPassword *v1.LocalObjectReference `json:"kubeadminPassword,omitempty"` IgnitionEndpoint *string `json:"ignitionEndpoint,omitempty"` ControlPlaneEndpoint *APIEndpointApplyConfiguration `json:"controlPlaneEndpoint,omitempty"` @@ -59,6 +60,14 @@ func (b *HostedClusterStatusApplyConfiguration) WithKubeConfig(value v1.LocalObj return b } +// WithCustomKubeconfig sets the CustomKubeconfig 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 CustomKubeconfig field is set to the value of the last call. +func (b *HostedClusterStatusApplyConfiguration) WithCustomKubeconfig(value v1.LocalObjectReference) *HostedClusterStatusApplyConfiguration { + b.CustomKubeconfig = &value + return b +} + // WithKubeadminPassword sets the KubeadminPassword 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 KubeadminPassword field is set to the value of the last call. diff --git a/client/applyconfiguration/hypershift/v1beta1/hostedcontrolplanespec.go b/client/applyconfiguration/hypershift/v1beta1/hostedcontrolplanespec.go index 3c5bdef84251..037c8de4f214 100644 --- a/client/applyconfiguration/hypershift/v1beta1/hostedcontrolplanespec.go +++ b/client/applyconfiguration/hypershift/v1beta1/hostedcontrolplanespec.go @@ -43,6 +43,7 @@ type HostedControlPlaneSpecApplyConfiguration struct { InfrastructureAvailabilityPolicy *hypershiftv1beta1.AvailabilityPolicy `json:"infrastructureAvailabilityPolicy,omitempty"` FIPS *bool `json:"fips,omitempty"` KubeConfig *KubeconfigSecretRefApplyConfiguration `json:"kubeconfig,omitempty"` + KubeAPIServerDNSName *string `json:"kubeAPIServerDNSName,omitempty"` Services []ServicePublishingStrategyMappingApplyConfiguration `json:"services,omitempty"` AuditWebhook *corev1.LocalObjectReference `json:"auditWebhook,omitempty"` Etcd *EtcdSpecApplyConfiguration `json:"etcd,omitempty"` @@ -203,6 +204,14 @@ func (b *HostedControlPlaneSpecApplyConfiguration) WithKubeConfig(value *Kubecon return b } +// WithKubeAPIServerDNSName sets the KubeAPIServerDNSName 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 KubeAPIServerDNSName field is set to the value of the last call. +func (b *HostedControlPlaneSpecApplyConfiguration) WithKubeAPIServerDNSName(value string) *HostedControlPlaneSpecApplyConfiguration { + b.KubeAPIServerDNSName = &value + return b +} + // WithServices adds the given value to the Services field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Services field. diff --git a/client/applyconfiguration/hypershift/v1beta1/hostedcontrolplanestatus.go b/client/applyconfiguration/hypershift/v1beta1/hostedcontrolplanestatus.go index bd78944b3ca2..2e73f1fab2eb 100644 --- a/client/applyconfiguration/hypershift/v1beta1/hostedcontrolplanestatus.go +++ b/client/applyconfiguration/hypershift/v1beta1/hostedcontrolplanestatus.go @@ -36,6 +36,7 @@ type HostedControlPlaneStatusApplyConfiguration struct { ReleaseImage *string `json:"releaseImage,omitempty"` LastReleaseImageTransitionTime *v1.Time `json:"lastReleaseImageTransitionTime,omitempty"` KubeConfig *KubeconfigSecretRefApplyConfiguration `json:"kubeConfig,omitempty"` + CustomKubeconfig *KubeconfigSecretRefApplyConfiguration `json:"customKubeconfig,omitempty"` KubeadminPassword *corev1.LocalObjectReference `json:"kubeadminPassword,omitempty"` Conditions []metav1.ConditionApplyConfiguration `json:"conditions,omitempty"` Platform *PlatformStatusApplyConfiguration `json:"platform,omitempty"` @@ -128,6 +129,14 @@ func (b *HostedControlPlaneStatusApplyConfiguration) WithKubeConfig(value *Kubec return b } +// WithCustomKubeconfig sets the CustomKubeconfig 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 CustomKubeconfig field is set to the value of the last call. +func (b *HostedControlPlaneStatusApplyConfiguration) WithCustomKubeconfig(value *KubeconfigSecretRefApplyConfiguration) *HostedControlPlaneStatusApplyConfiguration { + b.CustomKubeconfig = value + return b +} + // WithKubeadminPassword sets the KubeadminPassword 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 KubeadminPassword field is set to the value of the last call. diff --git a/cmd/cluster/core/dump.go b/cmd/cluster/core/dump.go index 8aa23fddbc44..b1a0958ec116 100644 --- a/cmd/cluster/core/dump.go +++ b/cmd/cluster/core/dump.go @@ -20,6 +20,7 @@ import ( "github.com/openshift/hypershift/hypershift-operator/controllers/sharedingress" kvinfra "github.com/openshift/hypershift/kubevirtexternalinfra" hyperapi "github.com/openshift/hypershift/support/api" + supportforwarder "github.com/openshift/hypershift/support/forwarder" supportutil "github.com/openshift/hypershift/support/util" configv1 "github.com/openshift/api/config/v1" @@ -183,21 +184,11 @@ func dumpGuestCluster(ctx context.Context, opts *DumpOptions) error { target := opts.ArtifactDir + "/hostedcluster-" + opts.Name - kubeAPIServerPodList := &corev1.PodList{} - if err := c.List(ctx, kubeAPIServerPodList, client.InNamespace(cpNamespace), client.MatchingLabels{"app": "kube-apiserver", hyperv1.ControlPlaneComponentLabel: "kube-apiserver"}); err != nil { - return fmt.Errorf("failed to list kube-apiserver pods in control plane namespace: %w", err) - } - var podToForward *corev1.Pod - for i := range kubeAPIServerPodList.Items { - pod := &kubeAPIServerPodList.Items[i] - if pod.Status.Phase == corev1.PodRunning { - podToForward = pod - break - } - } - if podToForward == nil { - return fmt.Errorf("did not find running kube-apiserver pod for guest cluster") + podToForward, err := supportforwarder.GetRunningKubeAPIServerPod(ctx, c, cpNamespace) + if err != nil { + return fmt.Errorf("failed to get running kube-apiserver pod for guest cluster: %w", err) } + restConfig, err := util.GetConfig() if err != nil { return fmt.Errorf("failed to get a config for management cluster: %w", err) @@ -214,7 +205,7 @@ func dumpGuestCluster(ctx context.Context, opts *DumpOptions) error { return fmt.Errorf("failed to get a kubernetes client: %w", err) } forwarderOutput := &bytes.Buffer{} - forwarder := portForwarder{ + forwarder := supportforwarder.PortForwarder{ Namespace: podToForward.Namespace, PodName: podToForward.Name, Config: restConfig, diff --git a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-CustomNoUpgrade.crd.yaml b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-CustomNoUpgrade.crd.yaml index 6523b27f6dda..b010e64723c6 100644 --- a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-CustomNoUpgrade.crd.yaml +++ b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-CustomNoUpgrade.crd.yaml @@ -2870,6 +2870,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -5815,6 +5830,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. diff --git a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Default.crd.yaml b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Default.crd.yaml index 4ba94a0b9301..f3e986780f4a 100644 --- a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Default.crd.yaml +++ b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Default.crd.yaml @@ -2807,6 +2807,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -5247,6 +5262,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. diff --git a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-TechPreviewNoUpgrade.crd.yaml b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-TechPreviewNoUpgrade.crd.yaml index 05d4b930220a..c543cda9e206 100644 --- a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-TechPreviewNoUpgrade.crd.yaml +++ b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedclusters-TechPreviewNoUpgrade.crd.yaml @@ -2852,6 +2852,21 @@ spec: rule: self == oldSelf - message: issuerURL must be a valid absolute URL rule: isURL(self) + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') labels: additionalProperties: type: string @@ -5797,6 +5812,22 @@ spec: - host - port type: object + customKubeconfig: + description: |- + CustomKubeconfig is a local secret reference to the external custom kubeconfig. + Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic ignitionEndpoint: description: |- IgnitionEndpoint is the endpoint injected in the ign config userdata. diff --git a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-CustomNoUpgrade.crd.yaml b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-CustomNoUpgrade.crd.yaml index 6c0f8f8754c1..84225dd7c77d 100644 --- a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-CustomNoUpgrade.crd.yaml +++ b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-CustomNoUpgrade.crd.yaml @@ -2764,6 +2764,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -5636,6 +5650,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- diff --git a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Default.crd.yaml b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Default.crd.yaml index 15ee203b52df..14a434df5478 100644 --- a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Default.crd.yaml +++ b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Default.crd.yaml @@ -2701,6 +2701,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -5068,6 +5082,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- diff --git a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-TechPreviewNoUpgrade.crd.yaml b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-TechPreviewNoUpgrade.crd.yaml index f608e73b030f..8ac37ca8542b 100644 --- a/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-TechPreviewNoUpgrade.crd.yaml +++ b/cmd/install/assets/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-TechPreviewNoUpgrade.crd.yaml @@ -2746,6 +2746,20 @@ spec: default value is kubernetes.default.svc, which only works for in-cluster validation. type: string + kubeAPIServerDNSName: + description: |- + kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. + When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. + If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. + The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. + This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable + access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates + for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. + maxLength: 253 + type: string + x-kubernetes-validations: + - message: kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com) + rule: self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$') kubeconfig: description: KubeConfig specifies the name and key for the kubeconfig secret @@ -5618,6 +5632,23 @@ spec: - host - port type: object + customKubeconfig: + description: |- + customKubeconfig references an external custom kubeconfig secret. + This field is populated in the status when a custom kubeconfig secret has been generated + for the hosted cluster. It contains the name and key of the secret located in the + hostedCluster namespace. This field is only populated when kubeApiExternalName is set. + If this field is removed during a day 2 operation, the referenced secret will be deleted + and this field will be removed from the hostedCluster status. + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object externalManagedControlPlane: default: true description: |- diff --git a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go index 91ee569a7c0c..eb1320cb2aa3 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go +++ b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go @@ -739,7 +739,8 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R meta.SetStatusCondition(&hostedControlPlane.Status.Conditions, condition) } - kubeconfig := manifests.KASExternalKubeconfigSecret(hostedControlPlane.Namespace, hostedControlPlane.Spec.KubeConfig) + // Admin Kubeconfig + kubeconfig := manifests.KASAdminKubeconfigSecret(hostedControlPlane.Namespace, hostedControlPlane.Spec.KubeConfig) if err := r.Get(ctx, client.ObjectKeyFromObject(kubeconfig), kubeconfig); err != nil { if !apierrors.IsNotFound(err) { return reconcile.Result{}, err @@ -749,11 +750,16 @@ func (r *HostedControlPlaneReconciler) Reconcile(ctx context.Context, req ctrl.R Name: kubeconfig.Name, Key: DefaultAdminKubeconfigKey, } + if hostedControlPlane.Spec.KubeConfig != nil { hostedControlPlane.Status.KubeConfig.Key = hostedControlPlane.Spec.KubeConfig.Key } } + if err := setKASCustomKubeconfigStatus(ctx, hostedControlPlane, r.Client); err != nil { + return reconcile.Result{}, err + } + explicitOauthConfig := hostedControlPlane.Spec.Configuration != nil && hostedControlPlane.Spec.Configuration.OAuth != nil if explicitOauthConfig { hostedControlPlane.Status.KubeadminPassword = nil @@ -3008,12 +3014,33 @@ func (r *HostedControlPlaneReconciler) reconcileKubeAPIServer(ctx context.Contex return fmt.Errorf("failed to reconcile localhost kubeconfig secret: %w", err) } - externalKubeconfigSecret := manifests.KASExternalKubeconfigSecret(hcp.Namespace, hcp.Spec.KubeConfig) - if _, err := createOrUpdate(ctx, r, externalKubeconfigSecret, func() error { + // Generate the new KASCustomKubeconfig secret if the KubeAPIServerDNSName is set + kasCustomKubeconfigSecret := manifests.KASCustomKubeconfigSecret(hcp.Namespace, nil) + if len(hcp.Spec.KubeAPIServerDNSName) > 0 { + newRootCA, err := includeServingCertificates(ctx, r.Client, hcp, rootCA) + if err != nil { + return fmt.Errorf("failed to include serving certificates: %w", err) + } + + if _, err := createOrUpdate(ctx, r, kasCustomKubeconfigSecret, func() error { + return kas.ReconcileKASCustomKubeconfigSecret(kasCustomKubeconfigSecret, clientCertSecret, newRootCA, p.OwnerRef, p.CustomExternalURL(), p.KASCustomKubeconfigKey()) + }); err != nil { + return fmt.Errorf("failed to reconcile custom external kubeconfig secret: %w", err) + } + } else { + // Cleanup the new kasCustomKubeconfigSecret secret if the KubeAPIServerDNSName is removed + if _, err := util.DeleteIfNeeded(ctx, r.Client, kasCustomKubeconfigSecret); err != nil { + return fmt.Errorf("failed to delete customKubeconfig status from HCP object: %w", err) + } + } + + // Renamed the old externalKubeconfigSecret to adminKubeconfigSecret + adminKubeconfigSecret := manifests.KASAdminKubeconfigSecret(hcp.Namespace, hcp.Spec.KubeConfig) + if _, err := createOrUpdate(ctx, r, adminKubeconfigSecret, func() error { if !util.IsPublicHCP(hcp) && !util.IsRouteKAS(hcp) { - return kas.ReconcileExternalKubeconfigSecret(externalKubeconfigSecret, clientCertSecret, rootCA, p.OwnerRef, p.InternalURL(), p.ExternalKubeconfigKey()) + return kas.ReconcileKASCustomKubeconfigSecret(adminKubeconfigSecret, clientCertSecret, rootCA, p.OwnerRef, p.InternalURL(), p.ExternalKubeconfigKey()) } - return kas.ReconcileExternalKubeconfigSecret(externalKubeconfigSecret, clientCertSecret, rootCA, p.OwnerRef, p.ExternalURL(), p.ExternalKubeconfigKey()) + return kas.ReconcileKASCustomKubeconfigSecret(adminKubeconfigSecret, clientCertSecret, rootCA, p.OwnerRef, p.ExternalURL(), p.ExternalKubeconfigKey()) }); err != nil { return fmt.Errorf("failed to reconcile external kubeconfig secret: %w", err) } @@ -5571,7 +5598,7 @@ func (r *HostedControlPlaneReconciler) validateAzureKMSConfig(ctx context.Contex } func (r *HostedControlPlaneReconciler) GetGuestClusterClient(ctx context.Context, hcp *hyperv1.HostedControlPlane) (*kubernetes.Clientset, error) { - kubeconfigSecret := manifests.KASExternalKubeconfigSecret(hcp.Namespace, hcp.Spec.KubeConfig) + kubeconfigSecret := manifests.KASAdminKubeconfigSecret(hcp.Namespace, hcp.Spec.KubeConfig) if err := r.Get(ctx, client.ObjectKeyFromObject(kubeconfigSecret), kubeconfigSecret); err != nil { return nil, err } @@ -5676,3 +5703,54 @@ func (r *HostedControlPlaneReconciler) verifyResourceGroupLocationsMatch(ctx con } return nil } + +func setKASCustomKubeconfigStatus(ctx context.Context, hcp *hyperv1.HostedControlPlane, c client.Client) error { + customKubeconfig := manifests.KASCustomKubeconfigSecret(hcp.Namespace, nil) + if err := c.Get(ctx, client.ObjectKeyFromObject(customKubeconfig), customKubeconfig); err != nil { + if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to get custom kubeconfig secret: %w", err) + } + } + + if len(hcp.Spec.KubeAPIServerDNSName) > 0 { + // Reconcile custom kubeconfig status + hcp.Status.CustomKubeconfig = &hyperv1.KubeconfigSecretRef{ + Name: customKubeconfig.Name, + Key: DefaultAdminKubeconfigKey, + } + } else { + // Cleanning up custom kubeconfig status + hcp.Status.CustomKubeconfig = nil + } + + return nil +} + +// includeServingCertificates includes additional serving certificates into the provided root CA ConfigMap. +// It retrieves the named certificates specified in the HostedControlPlane's APIServer configuration and appends +// their contents to the "ca.crt" entry in the root CA ConfigMap. +func includeServingCertificates(ctx context.Context, c client.Client, hcp *hyperv1.HostedControlPlane, rootCA *corev1.ConfigMap) (*corev1.ConfigMap, error) { + var tlsCRT string + newRootCA := rootCA.DeepCopy() + + if hcp.Spec.Configuration != nil && hcp.Spec.Configuration.APIServer != nil && len(hcp.Spec.Configuration.APIServer.ServingCerts.NamedCertificates) > 0 { + for _, servingCert := range hcp.Spec.Configuration.APIServer.ServingCerts.NamedCertificates { + newCRT := &corev1.Secret{} + if err := c.Get(ctx, client.ObjectKey{Namespace: hcp.Namespace, Name: servingCert.ServingCertificate.Name}, newCRT); err != nil { + return nil, fmt.Errorf("failed to get serving certificate secret: %w", err) + } + + if len(tlsCRT) <= 0 { + tlsCRT = newRootCA.Data["ca.crt"] + } + + tlsCRT = fmt.Sprintf("%s\n%s", tlsCRT, string(newCRT.Data["tls.crt"])) + } + + if len(tlsCRT) > 0 { + newRootCA.Data["ca.crt"] = tlsCRT + } + } + + return newRootCA, nil +} diff --git a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go index 51fe8cc0a3ff..98d9b736724e 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go +++ b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go @@ -1512,6 +1512,172 @@ func TestReconcileHCPRouterServices(t *testing.T) { } } +func TestSetKASCustomKubeconfigStatus(t *testing.T) { + hcp := sampleHCP(t) + pullSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: hcp.Namespace, Name: "pull-secret"}} + c := fake.NewClientBuilder().WithScheme(api.Scheme).WithObjects(hcp, pullSecret).WithStatusSubresource(&hyperv1.HostedControlPlane{}).Build() + ctx := ctrl.LoggerInto(context.Background(), zapr.NewLogger(zaptest.NewLogger(t))) + + tests := []struct { + name string + KubeAPIServerDNSName string + expectedStatus *hyperv1.KubeconfigSecretRef + }{ + { + name: "KubeAPIServerDNSName is empty", + KubeAPIServerDNSName: "", + expectedStatus: nil, + }, + { + name: "KubeAPIServerDNSName has a valid value", + KubeAPIServerDNSName: "testapi.example.com", + expectedStatus: &hyperv1.KubeconfigSecretRef{ + Name: "custom-admin-kubeconfig", + Key: "kubeconfig", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + hcp.Spec.KubeAPIServerDNSName = tc.KubeAPIServerDNSName + + err := setKASCustomKubeconfigStatus(ctx, hcp, c) + g.Expect(err).To(BeNil(), fmt.Errorf("error setting custom kubeconfig status failed: %v", err)) + g.Expect(hcp.Status.CustomKubeconfig).To(Equal(tc.expectedStatus)) + }) + } +} + +func TestIncludeServingCertificates(t *testing.T) { + ctx := context.Background() + hcp := sampleHCP(t) + rootCA := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "root-ca", + Namespace: hcp.Namespace, + }, + Data: map[string]string{ + "ca.crt": "root-ca-cert", + }, + } + + tests := []struct { + name string + servingCerts *configv1.APIServerServingCerts + servingSecrets []*corev1.Secret + expectedCert string + expectError bool + }{ + { + name: "APIServer servingCerts is nil", + servingCerts: &configv1.APIServerServingCerts{}, + expectedCert: "root-ca-cert", + }, + { + name: "APIServer servingCerts configuration with one named certificates", + servingCerts: &configv1.APIServerServingCerts{ + NamedCertificates: []configv1.APIServerNamedServingCert{ + { + ServingCertificate: configv1.SecretNameReference{ + Name: "serving-cert-1", + }, + }, + }, + }, + servingSecrets: []*corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "serving-cert-1", + Namespace: hcp.Namespace, + }, + Data: map[string][]byte{ + "tls.crt": []byte("cert-1"), + }, + }, + }, + expectedCert: "root-ca-cert\ncert-1", + }, + { + name: "APIServer servingCerts configuration with multiple named certificates", + servingCerts: &configv1.APIServerServingCerts{ + NamedCertificates: []configv1.APIServerNamedServingCert{ + { + ServingCertificate: configv1.SecretNameReference{ + Name: "serving-cert-1", + }, + }, + { + ServingCertificate: configv1.SecretNameReference{ + Name: "serving-cert-2", + }, + }, + }, + }, + servingSecrets: []*corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "serving-cert-1", + Namespace: hcp.Namespace, + }, + Data: map[string][]byte{ + "tls.crt": []byte("cert-1"), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "serving-cert-2", + Namespace: hcp.Namespace, + }, + Data: map[string][]byte{ + "tls.crt": []byte("cert-2"), + }, + }, + }, + expectedCert: "root-ca-cert\ncert-1\ncert-2", + }, + { + name: "APIServer servingCerts configuration with missing named certificate", + servingCerts: &configv1.APIServerServingCerts{ + NamedCertificates: []configv1.APIServerNamedServingCert{ + { + ServingCertificate: configv1.SecretNameReference{ + Name: "missing-cert", + }, + }, + }, + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + hcp.Spec.Configuration = &hyperv1.ClusterConfiguration{ + APIServer: &configv1.APIServerSpec{ + ServingCerts: *tc.servingCerts, + }, + } + + fakeClient := fake.NewClientBuilder().WithObjects(rootCA).Build() + for _, secret := range tc.servingSecrets { + _ = fakeClient.Create(ctx, secret) + } + + newRootCA, err := includeServingCertificates(ctx, fakeClient, hcp, rootCA) + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(newRootCA.Data["ca.crt"]).To(Equal(tc.expectedCert)) + } + }) + } +} + type fakeMessageCollector struct { msg string } diff --git a/control-plane-operator/controllers/hostedcontrolplane/kas/kubeconfig.go b/control-plane-operator/controllers/hostedcontrolplane/kas/kubeconfig.go index 619aa3967e14..b1791d960ef5 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/kas/kubeconfig.go +++ b/control-plane-operator/controllers/hostedcontrolplane/kas/kubeconfig.go @@ -52,7 +52,7 @@ func ReconcileLocalhostKubeconfigSecret(secret, cert *corev1.Secret, ca *corev1. return pki.ReconcileKubeConfig(secret, cert, ca, localhostURL, "", manifests.KubeconfigScopeLocal, ownerRef) } -func ReconcileExternalKubeconfigSecret(secret, cert *corev1.Secret, ca *corev1.ConfigMap, ownerRef config.OwnerRef, externalURL, secretKey string) error { +func ReconcileKASCustomKubeconfigSecret(secret, cert *corev1.Secret, ca *corev1.ConfigMap, ownerRef config.OwnerRef, externalURL, secretKey string) error { return pki.ReconcileKubeConfig(secret, cert, ca, externalURL, secretKey, manifests.KubeconfigScopeExternal, ownerRef) } diff --git a/control-plane-operator/controllers/hostedcontrolplane/kas/params.go b/control-plane-operator/controllers/hostedcontrolplane/kas/params.go index f686be7092eb..bdae0ea1cd33 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/kas/params.go +++ b/control-plane-operator/controllers/hostedcontrolplane/kas/params.go @@ -51,21 +51,24 @@ type KubeAPIServerParams struct { ServiceCIDRs []string `json:"serviceCIDRs"` ClusterCIDRs []string `json:"clusterCIDRs"` AdvertiseAddress string `json:"advertiseAddress"` - ExternalAddress string `json:"externalAddress"` + // CustomizedExternalAddress is the external address that is customized by the user using the field KubeAPIServerDNSName. + CustomizedExternalAddress string `json:"customizedExternalAddress"` + ExternalAddress string `json:"externalAddress"` // ExternalPort is the port coming from the status of the SVC which is exposing the KAS, e.g. common router LB, dedicated private/public/ LB... // This is used to build kas urls for generated internal kubeconfigs for example. ExternalPort int32 `json:"externalPort"` InternalAddress string `json:"internalAddress"` // KASPodPort is the port to expose in the KAS Pod. - KASPodPort int32 `json:"apiServerPort"` - ExternalOAuthAddress string `json:"externalOAuthAddress"` - ExternalOAuthPort int32 `json:"externalOAuthPort"` - OIDCCAConfigMap *corev1.LocalObjectReference `json:"oidcCAConfigMap"` - EtcdURL string `json:"etcdAddress"` - KubeConfigRef *hyperv1.KubeconfigSecretRef `json:"kubeConfigRef"` - AuditWebhookRef *corev1.LocalObjectReference `json:"auditWebhookRef"` - ConsolePublicURL string `json:"consolePublicURL"` - DisableProfiling bool `json:"disableProfiling"` + KASPodPort int32 `json:"apiServerPort"` + ExternalOAuthAddress string `json:"externalOAuthAddress"` + ExternalOAuthPort int32 `json:"externalOAuthPort"` + OIDCCAConfigMap *corev1.LocalObjectReference `json:"oidcCAConfigMap"` + EtcdURL string `json:"etcdAddress"` + KubeConfigRef *hyperv1.KubeconfigSecretRef `json:"kubeConfigRef"` + KASCustomKubeconfigRef *hyperv1.KubeconfigSecretRef `json:"kasCustomKubeconfigRef"` + AuditWebhookRef *corev1.LocalObjectReference `json:"auditWebhookRef"` + ConsolePublicURL string `json:"consolePublicURL"` + DisableProfiling bool `json:"disableProfiling"` config.DeploymentConfig config.OwnerRef @@ -122,6 +125,11 @@ func NewKubeAPIServerParams(ctx context.Context, hcp *hyperv1.HostedControlPlane MaxRequestsInflight: fmt.Sprint(defaultMaxRequestsInflight), MaxMutatingRequestsInflight: fmt.Sprint(defaultMaxMutatingRequestsInflight), } + + if len(hcp.Spec.KubeAPIServerDNSName) > 0 { + params.CustomizedExternalAddress = hcp.Spec.KubeAPIServerDNSName + } + if hcp.Spec.Configuration != nil { params.APIServer = hcp.Spec.Configuration.APIServer params.Authentication = hcp.Spec.Configuration.Authentication @@ -336,6 +344,10 @@ func (p *KubeAPIServerParams) AuditPolicyConfig() configv1.Audit { } } +func (p *KubeAPIServerParams) CustomExternalURL() string { + return fmt.Sprintf("https://%s:%d", pki.AddBracketsIfIPv6(p.CustomizedExternalAddress), p.ExternalPort) +} + func (p *KubeAPIServerParams) ExternalURL() string { return fmt.Sprintf("https://%s:%d", pki.AddBracketsIfIPv6(p.ExternalAddress), p.ExternalPort) } @@ -345,6 +357,13 @@ func (p *KubeAPIServerParams) InternalURL() string { return fmt.Sprintf("https://%s:%d", pki.AddBracketsIfIPv6(p.InternalAddress), p.ExternalPort) } +func (p *KubeAPIServerParams) KASCustomKubeconfigKey() string { + if p.KASCustomKubeconfigRef == nil { + return "" + } + return p.KASCustomKubeconfigRef.Key +} + func (p *KubeAPIServerParams) ExternalKubeconfigKey() string { if p.KubeConfigRef == nil { return "" diff --git a/control-plane-operator/controllers/hostedcontrolplane/manifests/kas.go b/control-plane-operator/controllers/hostedcontrolplane/manifests/kas.go index d066f2fd55d9..22dbdcb72927 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/manifests/kas.go +++ b/control-plane-operator/controllers/hostedcontrolplane/manifests/kas.go @@ -72,7 +72,20 @@ func HCCOKubeconfigSecret(controlPlaneNamespace string) *corev1.Secret { } } -func KASExternalKubeconfigSecret(controlPlaneNamespace string, ref *hyperv1.KubeconfigSecretRef) *corev1.Secret { +func KASCustomKubeconfigSecret(controlPlaneNamespace string, ref *hyperv1.KubeconfigSecretRef) *corev1.Secret { + s := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "custom-admin-kubeconfig", + Namespace: controlPlaneNamespace, + }, + } + if ref != nil { + s.Name = ref.Name + } + return s +} + +func KASAdminKubeconfigSecret(controlPlaneNamespace string, ref *hyperv1.KubeconfigSecretRef) *corev1.Secret { s := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "admin-kubeconfig", diff --git a/control-plane-operator/controllers/hostedcontrolplane/manifests/pki.go b/control-plane-operator/controllers/hostedcontrolplane/manifests/pki.go index e59266575a1a..482d2b75af5f 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/manifests/pki.go +++ b/control-plane-operator/controllers/hostedcontrolplane/manifests/pki.go @@ -20,6 +20,15 @@ func RootCASecret(ns string) *corev1.Secret { return secretFor(ns, "root-ca") } func CSRSignerCASecret(ns string) *corev1.Secret { return secretFor(ns, "cluster-signer-ca") } +func KASExternalCAConfigMap(name string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "openshift-config", + }, + } +} + func RootCAConfigMap(ns string) *corev1.ConfigMap { return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/assets/kube-apiserver/custom-admin-kubeconfig.yaml b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/kube-apiserver/custom-admin-kubeconfig.yaml new file mode 100644 index 000000000000..ec3d3ecbcf50 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/kube-apiserver/custom-admin-kubeconfig.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + labels: + hypershift.openshift.io/kubeconfig: external + name: custom-admin-kubeconfig + namespace: HCP_NAMESPACE +type: Opaque diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/component.go b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/component.go index 23f52a65ed7b..58ee10059662 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/component.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/component.go @@ -6,6 +6,7 @@ import ( "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/v2/kas/kms" "github.com/openshift/hypershift/support/azureutil" component "github.com/openshift/hypershift/support/controlplane-component" + hyperutils "github.com/openshift/hypershift/support/util" ) const ( @@ -52,6 +53,11 @@ func NewComponent() component.ControlPlaneComponent { "local-kubeconfig.yaml", component.WithAdaptFunction(adaptLocalhostKubeconfigSecret), ). + WithManifestAdapter( + "custom-admin-kubeconfig.yaml", + component.WithAdaptFunction(adaptCustomAdminKubeconfigSecret), + component.WithPredicate(enableIfCustomKubeconfig), + ). WithManifestAdapter( "external-admin-kubeconfig.yaml", component.WithAdaptFunction(adapExternalAdminKubeconfigSecret), @@ -117,3 +123,8 @@ func enableAzureKMSSecretProvider(cpContext component.WorkloadContext) bool { } return false } + +// enableIfCustomKubeconfig is a helper predicate for the common use case of enabling a resource when a KubeAPICustomKubeconfig is specified. +func enableIfCustomKubeconfig(cpContext component.WorkloadContext) bool { + return hyperutils.EnableIfCustomKubeconfig(cpContext.HCP) +} diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/kubeconfig.go b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/kubeconfig.go index 6e6d1643fe46..ba14ce00f323 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/kas/kubeconfig.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/kas/kubeconfig.go @@ -101,6 +101,7 @@ func adapExternalAdminKubeconfigSecret(cpContext component.WorkloadContext, secr } url := externalURL(cpContext.InfraStatus) + if !util.IsPublicHCP(cpContext.HCP) && !util.IsRouteKAS(cpContext.HCP) { url = internalURL(cpContext.InfraStatus, cpContext.HCP.Name) } @@ -116,7 +117,26 @@ func adapExternalAdminKubeconfigSecret(cpContext component.WorkloadContext, secr return nil } +func adaptCustomAdminKubeconfigSecret(cpContext component.WorkloadContext, secret *corev1.Secret) error { + hcp := cpContext.HCP + apiServerPort := util.KASPodPort(hcp) + url := customExternalURL(hcp.Spec.KubeAPIServerDNSName, apiServerPort) + kubeconfig, err := GenerateKubeConfig(cpContext, manifests.SystemAdminClientCertSecret(hcp.Namespace), url) + if err != nil { + return fmt.Errorf("failed to generate kubeconfig: %w", err) + } + + if secret.Data == nil { + secret.Data = map[string][]byte{} + } + secret.Data[externalKubeconfigKey(hcp)] = kubeconfig + + return nil + +} + func adaptBootstrapKubeconfigSecret(cpContext component.WorkloadContext, secret *corev1.Secret) error { + url := externalURL(cpContext.InfraStatus) if util.IsPrivateHCP(cpContext.HCP) { url = internalURL(cpContext.InfraStatus, cpContext.HCP.Name) @@ -201,13 +221,17 @@ func InClusterKASURL(platformType hyperv1.PlatformType) string { return fmt.Sprintf("https://%s:%d", manifests.KubeAPIServerServiceName, config.KASSVCPort) } +func customExternalURL(address string, port int32) string { + return fmt.Sprintf("https://%s:%d", pki.AddBracketsIfIPv6(address), port) +} + func externalURL(infraStatus infra.InfrastructureStatus) string { return fmt.Sprintf("https://%s:%d", pki.AddBracketsIfIPv6(infraStatus.APIHost), infraStatus.APIPort) } func internalURL(infraStatus infra.InfrastructureStatus, hcpName string) string { internalAddress := fmt.Sprintf("api.%s.hypershift.local", hcpName) - return fmt.Sprintf("https://%s:%d", pki.AddBracketsIfIPv6(internalAddress), infraStatus.APIPort) + return fmt.Sprintf("https://%s:%d", internalAddress, infraStatus.APIPort) } func externalKubeconfigKey(hcp *hyperv1.HostedControlPlane) string { diff --git a/docs/content/how-to/aws/define-custom-kube-api-name.md b/docs/content/how-to/aws/define-custom-kube-api-name.md new file mode 100644 index 000000000000..97918783d905 --- /dev/null +++ b/docs/content/how-to/aws/define-custom-kube-api-name.md @@ -0,0 +1,35 @@ +--- +title: Define Custom KubeAPI Name +--- + +## What is this for? + +`KubeAPICustomName` is a spec field used to declare a custom Kubernetes API URI. To make this work, you simply need to define the URI (e.g., `api.example.com`) in the `HostedCluster` object. + +## How does this work? + +- This can be defined both during day-1 (initial setup) and day-2 (post-deployment updates). +- The CPO (ControlPlaneOperator) controllers will create a new kubeconfig stored in the HCP namespace. This kubeconfig will be based on certificates and named `custom-admin-kubeconfig`. +- The certificates are generated from the root CA, with their expiration and renewal managed by the `HostedControlPlane`. +- The CPO will report a new kubeconfig, called `CustomKubeconfig`, in the `HostedControlPlane`. This kubeconfig will use the new server defined in the `KubeAPICustomName` field. +- This custom kubeconfig will also be referenced in the `HostedCluster` object under the status field as `CustomKubeconfig`. +- A new secret, named `{HOSTEDCLUSTER_NAME}-custom-admin-kubeconfig`, will be created in the `HostedCluster` namespace. This secret can be used to easily access the HostedCluster API server. + +!!! NOTE + This does not directly affect the dataplane, so no rollouts are expected to occur. + +- If you remove this field from the spec, all newly generated secrets and the `CustomKubeconfig` reference will be removed from the cluster and from the status field. + +## Additional Notes + +This other field called `CustomKubeConfig` is optional and can only be used if `KubeAPICustomName` is not empty. When set, it triggers the generation of a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace. This kubeconfig will also be referenced in the `HostedCluster.status` as `customkubeconfig`. If removed during day-2 operations, all related secrets and status references will also be deleted: + +- This action will not cause a NodePool rollout, ensuring zero impact on customers. +- The `HostedControlPlane` object will receive the changes progressed by the Hypershift Operator and delete the corresponding field. +- The `.status.customkubeconfig` will be removed from both `HostedCluster` and `HostedControlPlane` objects. +- The secret in the `HostedControlPlane` namespace, named `custom-admin-kubeconfig`, will be deleted. +- The secret in the `HostedCluster` namespace, named `{HOSTEDCLUSTER_NAME}-custom-admin-kubeconfig`, will also be deleted. + + + + diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md index 31359799c816..d1f226ee2a31 100644 --- a/docs/content/reference/api.md +++ b/docs/content/reference/api.md @@ -275,6 +275,25 @@ and is used to configure platform specific behavior.
kubeAPIServerDNSName
+
+string
+
+kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. +When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. +If it’s set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. +The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. +This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable +access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates +for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. +This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op.
+controllerAvailabilityPolicy
@@ -5145,6 +5164,25 @@ and is used to configure platform specific behavior.
kubeAPIServerDNSName
+
+string
+
+kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. +When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. +If it’s set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. +The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. +This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable +access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates +for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates. +This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op.
+controllerAvailabilityPolicy
@@ -5603,6 +5641,21 @@ for the cluster.
customKubeconfig
+
+
+Kubernetes core/v1.LocalObjectReference
+
+
+CustomKubeconfig is a local secret reference to the external custom kubeconfig.
+Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the HostedCluster namespace.
kubeadminPassword
@@ -5947,6 +6000,24 @@ KubeconfigSecretRef
kubeAPIServerDNSName
+
+string
+
+kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS. +When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field. +If it’s set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted. +The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider. +This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable +access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates +for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates.
+services
@@ -6338,6 +6409,25 @@ for this control plane.
customKubeconfig
+
+
+KubeconfigSecretRef
+
+
+customKubeconfig references an external custom kubeconfig secret. +This field is populated in the status when a custom kubeconfig secret has been generated +for the hosted cluster. It contains the name and key of the secret located in the +hostedCluster namespace. This field is only populated when kubeApiExternalName is set. +If this field is removed during a day 2 operation, the referenced secret will be deleted +and this field will be removed from the hostedCluster status.
+kubeadminPassword
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 07a8cddfd2fe..ef38ce9d14ba 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -88,6 +88,7 @@ nav:
- how-to/aws/etc-backup-restore.md
- how-to/aws/disaster-recovery.md
- how-to/aws/shared-vpc.md
+ - how-to/aws/define-custom-kube-api-name.md
- 'Other SDN providers': how-to/aws/other-sdn-providers.md
- 'Troubleshooting':
- how-to/aws/troubleshooting/index.md
diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go
index 40c824343602..95fd14042dad 100644
--- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go
+++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go
@@ -127,8 +127,9 @@ const (
ImageStreamAutoscalerImage = "cluster-autoscaler"
ImageStreamClusterMachineApproverImage = "cluster-machine-approver"
- controlPlaneOperatorSubcommandsLabel = "io.openshift.hypershift.control-plane-operator-subcommands"
- ignitionServerHealthzHandlerLabel = "io.openshift.hypershift.ignition-server-healthz-handler"
+ controlPlaneOperatorSubcommandsLabel = "io.openshift.hypershift.control-plane-operator-subcommands"
+ ignitionServerHealthzHandlerLabel = "io.openshift.hypershift.ignition-server-healthz-handler"
+ controlPlaneOperatorSupportsKASCustomKubeconfigLabel = "io.openshift.hypershift.control-plane-operator-supports-kas-custom-kubeconfig"
controlplaneOperatorManagesIgnitionServerLabel = "io.openshift.hypershift.control-plane-operator-manages-ignition-server"
controlPlaneOperatorManagesMachineApprover = "io.openshift.hypershift.control-plane-operator-manages.cluster-machine-approver"
@@ -613,6 +614,36 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques
return ctrl.Result{}, err
}
+ pullSecretBytes, err := hyperutil.GetPullSecretBytes(ctx, r.Client, hcluster)
+ if err != nil {
+ return ctrl.Result{}, err
+ }
+
+ controlPlaneOperatorImage, err := hyperutil.GetControlPlaneOperatorImage(ctx, hcluster, releaseProvider, r.HypershiftOperatorImage, pullSecretBytes)
+ if err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed to get controlPlaneOperatorImage: %w", err)
+ }
+ controlPlaneOperatorImageLabels, err := hyperutil.GetControlPlaneOperatorImageLabels(ctx, hcluster, controlPlaneOperatorImage, pullSecretBytes, registryClientImageMetadataProvider)
+ if err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed to get controlPlaneOperatorImageLabels: %w", err)
+ }
+
+ _, cpoSupportsKASCustomKubeconfig := controlPlaneOperatorImageLabels[controlPlaneOperatorSupportsKASCustomKubeconfigLabel]
+
+ if cpoSupportsKASCustomKubeconfig {
+ if len(hcluster.Spec.KubeAPIServerDNSName) > 0 {
+ CustomKubeconfigSecret := manifests.KubeConfigExternalSecret(hcluster.Namespace, hcluster.Name)
+ err := r.Client.Get(ctx, client.ObjectKeyFromObject(CustomKubeconfigSecret), CustomKubeconfigSecret)
+ if err != nil {
+ if !apierrors.IsNotFound(err) {
+ return ctrl.Result{}, fmt.Errorf("failed to reconcile external kubeconfig secret: %w", err)
+ }
+ } else {
+ hcluster.Status.CustomKubeconfig = &corev1.LocalObjectReference{Name: CustomKubeconfigSecret.Name}
+ }
+ }
+ }
+
// Set kubeadminPassword status
{
explicitOauthConfig := hcluster.Spec.Configuration != nil && hcluster.Spec.Configuration.OAuth != nil
@@ -1066,10 +1097,6 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques
}
hcluster.Status.PayloadArch = payloadArch
- pullSecretBytes, err := hyperutil.GetPullSecretBytes(ctx, r.Client, hcluster)
- if err != nil {
- return ctrl.Result{}, err
- }
releaseImage, err := r.lookupReleaseImage(ctx, hcluster, releaseProvider)
if err != nil {
@@ -1184,15 +1211,6 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques
}
}
- controlPlaneOperatorImage, err := hyperutil.GetControlPlaneOperatorImage(ctx, hcluster, releaseProvider, r.HypershiftOperatorImage, pullSecretBytes)
- if err != nil {
- return ctrl.Result{}, fmt.Errorf("failed to get controlPlaneOperatorImage: %w", err)
- }
- controlPlaneOperatorImageLabels, err := hyperutil.GetControlPlaneOperatorImageLabels(ctx, hcluster, controlPlaneOperatorImage, pullSecretBytes, registryClientImageMetadataProvider)
- if err != nil {
- return ctrl.Result{}, fmt.Errorf("failed to get controlPlaneOperatorImageLabels: %w", err)
- }
-
cpoHasUtilities := false
if _, hasLabel := controlPlaneOperatorImageLabels[controlPlaneOperatorSubcommandsLabel]; hasLabel {
cpoHasUtilities = true
@@ -1681,6 +1699,62 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques
}
}
+ if cpoSupportsKASCustomKubeconfig {
+ // Reconcile the HostedControlPlane external kubeconfig if one is reported
+ if len(hcp.Spec.KubeAPIServerDNSName) > 0 {
+ if hcp.Status.CustomKubeconfig != nil {
+ src := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: hcp.Namespace,
+ Name: hcp.Status.CustomKubeconfig.Name,
+ },
+ }
+ err := r.Client.Get(ctx, client.ObjectKeyFromObject(src), src)
+ if err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed to get controlplane custom external kubeconfig secret %q: %w", client.ObjectKeyFromObject(src), err)
+ }
+ dest := manifests.KubeConfigExternalSecret(hcluster.Namespace, hcluster.Name)
+ _, err = createOrUpdate(ctx, r.Client, dest, func() error {
+ key := hcp.Status.CustomKubeconfig.Key
+ srcData, srcHasData := src.Data[key]
+ if !srcHasData {
+ return fmt.Errorf("controlplane custom external kubeconfig secret %q must have a %q key", client.ObjectKeyFromObject(src), key)
+ }
+ dest.Labels = hcluster.Labels
+ dest.Type = corev1.SecretTypeOpaque
+ if dest.Data == nil {
+ dest.Data = map[string][]byte{}
+ }
+ dest.Data["kubeconfig"] = srcData
+ dest.SetOwnerReferences([]metav1.OwnerReference{{
+ APIVersion: hyperv1.GroupVersion.String(),
+ Kind: "HostedCluster",
+ Name: hcluster.Name,
+ UID: hcluster.UID,
+ }})
+ return nil
+ })
+ if err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed to reconcile hostedcluster custom external kubeconfig secret: %w", err)
+ }
+ }
+ } else {
+ // Delete the custom external kubeconfig secret if it exists and the external name is not set
+ if hcluster.Status.CustomKubeconfig != nil {
+ customKubeconfig := &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: hcluster.Namespace,
+ Name: hcluster.Status.CustomKubeconfig.Name,
+ },
+ }
+ if _, err := hyperutil.DeleteIfNeeded(ctx, r.Client, customKubeconfig); err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed to delete custom external kubeconfig secret %q: %w", client.ObjectKeyFromObject(customKubeconfig), err)
+ }
+ hcluster.Status.CustomKubeconfig = nil
+ }
+ }
+ }
+
// Reconcile the HostedControlPlane kubeadminPassword
if hcp.Status.KubeadminPassword != nil {
src := &corev1.Secret{
@@ -2136,6 +2210,7 @@ func reconcileHostedControlPlane(hcp *hyperv1.HostedControlPlane, hcluster *hype
hcp.Spec.SecretEncryption = hcluster.Spec.SecretEncryption.DeepCopy()
}
+ hcp.Spec.KubeAPIServerDNSName = hcluster.Spec.KubeAPIServerDNSName
hcp.Spec.PausedUntil = hcluster.Spec.PausedUntil
hcp.Spec.OLMCatalogPlacement = hcluster.Spec.OLMCatalogPlacement
hcp.Spec.Autoscaling = hcluster.Spec.Autoscaling
diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go
index 9e679efc6b87..b72a94f16fef 100644
--- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go
+++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go
@@ -153,6 +153,9 @@ func TestHasBeenAvailable(t *testing.T) {
},
},
},
+ PullSecret: corev1.LocalObjectReference{
+ Name: "pull-secret",
+ },
},
}
@@ -162,8 +165,19 @@ func TestHasBeenAvailable(t *testing.T) {
hcp.Status = hyperv1.HostedControlPlaneStatus{
Conditions: tc.hcpConditions,
}
+ objects := []crclient.Object{
+ &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: "pull-secret", Namespace: "any"},
+ Data: map[string][]byte{
+ hyperv1.AESCBCKeySecretKey: {1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+ ".dockerconfigjson": []byte("{}"),
+ },
+ },
+ hcp,
+ hcluster,
+ }
- client := fake.NewClientBuilder().WithScheme(api.Scheme).WithObjects(hcluster, hcp).WithStatusSubresource(hcluster).Build()
+ client := fake.NewClientBuilder().WithScheme(api.Scheme).WithObjects(objects...).WithStatusSubresource(hcluster).Build()
clock := clocktesting.NewFakeClock(tc.timestamp)
r := &HostedClusterReconciler{
Client: client,
@@ -3618,6 +3632,9 @@ func TestKubevirtETCDEncKey(t *testing.T) {
},
},
},
+ PullSecret: corev1.LocalObjectReference{
+ Name: "kubevirt" + etcdEncKeyPostfix,
+ },
},
},
secretName: "kubevirt" + etcdEncKeyPostfix,
@@ -3627,6 +3644,7 @@ func TestKubevirtETCDEncKey(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Name: "kubevirt" + etcdEncKeyPostfix, Namespace: "test"},
Data: map[string][]byte{
hyperv1.AESCBCKeySecretKey: {1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+ ".dockerconfigjson": []byte("{}"),
},
},
},
@@ -3852,10 +3870,22 @@ func TestKubevirtETCDEncKey(t *testing.T) {
},
},
},
+ PullSecret: corev1.LocalObjectReference{
+ Name: "custom-name",
+ },
},
},
secretName: "custom-name",
secretExpected: false,
+ objects: []crclient.Object{
+ &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{Name: "custom-name", Namespace: "test"},
+ Data: map[string][]byte{
+ hyperv1.AESCBCKeySecretKey: {1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
+ ".dockerconfigjson": []byte("{}"),
+ },
+ },
+ },
},
{
name: "secret encryption not defined and secret exists with no key",
diff --git a/hypershift-operator/controllers/manifests/manifests.go b/hypershift-operator/controllers/manifests/manifests.go
index f243899c179f..34e0e3f8c831 100644
--- a/hypershift-operator/controllers/manifests/manifests.go
+++ b/hypershift-operator/controllers/manifests/manifests.go
@@ -34,6 +34,15 @@ func KubeConfigSecret(hostedClusterNamespace string, hostedClusterName string) *
}
}
+func KubeConfigExternalSecret(hostedClusterNamespace string, hostedClusterName string) *corev1.Secret {
+ return &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Namespace: hostedClusterNamespace,
+ Name: hostedClusterName + "-custom-admin-kubeconfig",
+ },
+ }
+}
+
func KubeadminPasswordSecret(hostedClusterNamespace string, hostedClusterName string) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
diff --git a/cmd/cluster/core/forwarder.go b/support/forwarder/forwarder.go
similarity index 51%
rename from cmd/cluster/core/forwarder.go
rename to support/forwarder/forwarder.go
index 96a030894de4..5ea25ce826cb 100644
--- a/cmd/cluster/core/forwarder.go
+++ b/support/forwarder/forwarder.go
@@ -1,18 +1,24 @@
-// source: https://github.com/openshift/oc/blob/bc2163c506ff27cda7ab907a715aeb1815389ead/pkg/cli/rsync/forwarder.go
-package core
+package forwarder
import (
+ "context"
+ "fmt"
"io"
"net/http"
+ hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
+
+ corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
+
+ crclient "sigs.k8s.io/controller-runtime/pkg/client"
)
// portForwarder starts port forwarding to a given pod
-type portForwarder struct {
+type PortForwarder struct {
Namespace string
PodName string
Client kubernetes.Interface
@@ -23,7 +29,7 @@ type portForwarder struct {
// ForwardPorts will forward a set of ports from a pod, the stopChan will stop the forwarding
// when it's closed or receives a struct{}
-func (f *portForwarder) ForwardPorts(ports []string, stopChan <-chan struct{}) error {
+func (f *PortForwarder) ForwardPorts(ports []string, stopChan <-chan struct{}) error {
req := f.Client.CoreV1().RESTClient().Post().
Resource("pods").
Namespace(f.Namespace).
@@ -50,3 +56,22 @@ func (f *portForwarder) ForwardPorts(ports []string, stopChan <-chan struct{}) e
return err
}
}
+
+func GetRunningKubeAPIServerPod(ctx context.Context, kbClient crclient.Client, cpNamespace string) (*corev1.Pod, error) {
+ kubeAPIServerPodList := &corev1.PodList{}
+ if err := kbClient.List(ctx, kubeAPIServerPodList, crclient.InNamespace(cpNamespace), crclient.MatchingLabels{"app": "kube-apiserver", hyperv1.ControlPlaneComponentLabel: "kube-apiserver"}); err != nil {
+ return nil, fmt.Errorf("failed to list kube-apiserver pods in control plane namespace: %w", err)
+ }
+ var podToForward *corev1.Pod
+ for i := range kubeAPIServerPodList.Items {
+ pod := &kubeAPIServerPodList.Items[i]
+ if pod.Status.Phase == corev1.PodRunning {
+ podToForward = pod
+ break
+ }
+ }
+ if podToForward == nil {
+ return nil, fmt.Errorf("did not find running kube-apiserver pod for guest cluster")
+ }
+ return podToForward, nil
+}
diff --git a/support/forwarder/forwarder_test.go b/support/forwarder/forwarder_test.go
new file mode 100644
index 000000000000..af247edaf16c
--- /dev/null
+++ b/support/forwarder/forwarder_test.go
@@ -0,0 +1,135 @@
+package forwarder
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
+
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func TestGetRunningKubeAPIServerPod(t *testing.T) {
+ scheme := runtime.NewScheme()
+ _ = corev1.AddToScheme(scheme)
+
+ tests := []struct {
+ name string
+ pods []client.Object
+ cpNamespace string
+ wantErr bool
+ errContains string
+ }{
+ {
+ name: "successfully find running kube-apiserver pod",
+ pods: []client.Object{
+ &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "kube-apiserver-1",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "kube-apiserver",
+ hyperv1.ControlPlaneComponentLabel: "kube-apiserver",
+ },
+ },
+ Status: corev1.PodStatus{
+ Phase: corev1.PodRunning,
+ },
+ },
+ },
+ cpNamespace: "test-namespace",
+ wantErr: false,
+ },
+ {
+ name: "no running kube-apiserver pod found",
+ pods: []client.Object{
+ &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "kube-apiserver-1",
+ Namespace: "test-namespace",
+ Labels: map[string]string{
+ "app": "kube-apiserver",
+ hyperv1.ControlPlaneComponentLabel: "kube-apiserver",
+ },
+ },
+ Status: corev1.PodStatus{
+ Phase: corev1.PodPending,
+ },
+ },
+ },
+ cpNamespace: "test-namespace",
+ wantErr: true,
+ errContains: "did not find running kube-apiserver pod",
+ },
+ {
+ name: "no kube-apiserver pods found",
+ pods: []client.Object{
+ &corev1.Pod{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "other-pod",
+ Namespace: "test-namespace",
+ },
+ },
+ },
+ cpNamespace: "test-namespace",
+ wantErr: true,
+ errContains: "did not find running kube-apiserver pod",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fakeClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(tt.pods...).
+ Build()
+
+ got, err := GetRunningKubeAPIServerPod(context.Background(), fakeClient, tt.cpNamespace)
+
+ if tt.wantErr {
+ if err == nil {
+ t.Error("expected error but got none")
+ }
+ if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
+ t.Errorf("expected error containing %q, got %v", tt.errContains, err)
+ }
+ return
+ }
+
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ return
+ }
+
+ if got == nil {
+ t.Error("expected pod but got nil")
+ return
+ }
+
+ if got.Status.Phase != corev1.PodRunning {
+ t.Errorf("expected pod phase to be Running, got %v", got.Status.Phase)
+ }
+
+ if got.Namespace != tt.cpNamespace {
+ t.Errorf("expected pod namespace to be %q, got %q", tt.cpNamespace, got.Namespace)
+ }
+
+ // Verify required labels
+ requiredLabels := map[string]string{
+ "app": "kube-apiserver",
+ hyperv1.ControlPlaneComponentLabel: "kube-apiserver",
+ }
+ for key, value := range requiredLabels {
+ if got.Labels[key] != value {
+ t.Errorf("expected pod to have label %q=%q, got %q", key, value, got.Labels[key])
+ }
+ }
+ })
+ }
+}
diff --git a/support/globalconfig/infrastructure.go b/support/globalconfig/infrastructure.go
index 859102a3f246..65464e3fca91 100644
--- a/support/globalconfig/infrastructure.go
+++ b/support/globalconfig/infrastructure.go
@@ -36,6 +36,9 @@ func ReconcileInfrastructure(infra *configv1.Infrastructure, hcp *hyperv1.Hosted
}
infra.Status.APIServerURL = fmt.Sprintf("https://%s:%d", apiServerAddress, apiServerPort)
+ if len(hcp.Spec.KubeAPIServerDNSName) > 0 {
+ infra.Status.APIServerURL = fmt.Sprintf("https://%s:%d", hcp.Spec.KubeAPIServerDNSName, apiServerPort)
+ }
infra.Status.EtcdDiscoveryDomain = BaseDomain(hcp)
infra.Status.InfrastructureName = hcp.Spec.InfraID
infra.Status.ControlPlaneTopology = configv1.ExternalTopologyMode
diff --git a/support/util/util.go b/support/util/util.go
index e16496448471..fe45de13a3c4 100644
--- a/support/util/util.go
+++ b/support/util/util.go
@@ -681,4 +681,10 @@ func HostFromURL(addr string) (string, error) {
return "", fmt.Errorf("missing host name in URL(%s)", addr)
}
return hostName, nil
+
+}
+
+// EnableIfCustomKubeconfig returns true if the hosted control plane has a custom kubeconfig defined
+func EnableIfCustomKubeconfig(hcp *hyperv1.HostedControlPlane) bool {
+ return len(hcp.Spec.KubeAPIServerDNSName) > 0
}
diff --git a/test/e2e/create_cluster_test.go b/test/e2e/create_cluster_test.go
index 3662b7f52d07..be456cc26d12 100644
--- a/test/e2e/create_cluster_test.go
+++ b/test/e2e/create_cluster_test.go
@@ -879,6 +879,51 @@ func TestOnCreateAPIUX(t *testing.T) {
},
},
},
+ {
+ name: "when kubeAPIServerDNSName is not valid it should fail",
+ file: "hostedcluster-base.yaml",
+ validations: []struct {
+ name string
+ mutateInput func(*hyperv1.HostedCluster)
+ expectedErrorSubstring string
+ }{
+ {
+ name: "when kubeAPIServerDNSName has invalid chars it should fail",
+ mutateInput: func(hc *hyperv1.HostedCluster) {
+ hc.Spec.KubeAPIServerDNSName = "@foo"
+ },
+ expectedErrorSubstring: "kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com)",
+ },
+ {
+ name: "when kubeAPIServerDNSName is a valid hierarchical domain with two levels it should pass",
+ mutateInput: func(hc *hyperv1.HostedCluster) {
+ hc.Spec.KubeAPIServerDNSName = "foo.bar"
+ },
+ expectedErrorSubstring: "",
+ },
+ {
+ name: "when kubeAPIServerDNSName is a valid hierarchical domain it with 3 levels should pass",
+ mutateInput: func(hc *hyperv1.HostedCluster) {
+ hc.Spec.KubeAPIServerDNSName = "123.foo.bar"
+ },
+ expectedErrorSubstring: "",
+ },
+ {
+ name: "when kubeAPIServerDNSName is a single subdomain it should pass",
+ mutateInput: func(hc *hyperv1.HostedCluster) {
+ hc.Spec.KubeAPIServerDNSName = "foo"
+ },
+ expectedErrorSubstring: "",
+ },
+ {
+ name: "when kubeAPIServerDNSName is empty it should pass",
+ mutateInput: func(hc *hyperv1.HostedCluster) {
+ hc.Spec.KubeAPIServerDNSName = ""
+ },
+ expectedErrorSubstring: "",
+ },
+ },
+ },
}
for _, tc := range testCases {
@@ -1329,6 +1374,7 @@ func TestCreateClusterCustomConfig(t *testing.T) {
// ensure image registry component is disabled
e2eutil.EnsureImageRegistryCapabilityDisabled(ctx, t, g, mgtClient, hostedCluster)
+ e2eutil.EnsureKubeAPIDNSName(t, ctx, mgtClient, hostedCluster)
}).Execute(&clusterOpts, globalOpts.Platform, globalOpts.ArtifactDir, "custom-config", globalOpts.ServiceAccountSigningKey)
}
diff --git a/test/e2e/util/util.go b/test/e2e/util/util.go
index b0071806a0e2..b8991bfe285c 100644
--- a/test/e2e/util/util.go
+++ b/test/e2e/util/util.go
@@ -15,14 +15,18 @@ import (
hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
awsinfra "github.com/openshift/hypershift/cmd/infra/aws"
awsutil "github.com/openshift/hypershift/cmd/infra/aws/util"
+ "github.com/openshift/hypershift/cmd/log"
awsprivatelink "github.com/openshift/hypershift/control-plane-operator/controllers/awsprivatelink"
+ cpomanifests "github.com/openshift/hypershift/control-plane-operator/controllers/hostedcontrolplane/manifests"
hccokasvap "github.com/openshift/hypershift/control-plane-operator/hostedclusterconfigoperator/controllers/resources/kas"
hcmetrics "github.com/openshift/hypershift/hypershift-operator/controllers/hostedcluster/metrics"
"github.com/openshift/hypershift/hypershift-operator/controllers/manifests"
"github.com/openshift/hypershift/support/conditions"
suppconfig "github.com/openshift/hypershift/support/config"
+ supportforwarder "github.com/openshift/hypershift/support/forwarder"
"github.com/openshift/hypershift/support/util"
hyperutil "github.com/openshift/hypershift/support/util"
+ "github.com/openshift/hypershift/test/integration/framework"
configv1 "github.com/openshift/api/config/v1"
routev1 "github.com/openshift/api/route/v1"
@@ -48,6 +52,7 @@ import (
"k8s.io/client-go/kubernetes"
kubeclient "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
+ "k8s.io/client-go/util/retry"
"k8s.io/utils/ptr"
capiv1 "sigs.k8s.io/cluster-api/api/v1beta1"
@@ -150,6 +155,64 @@ func DeleteNamespace(t *testing.T, ctx context.Context, client crclient.Client,
return nil
}
+func GetCustomKubeconfigClients(t *testing.T, ctx context.Context, client crclient.Client, hostedCluster *hyperv1.HostedCluster, serverAddress string) (*kubernetes.Clientset, crclient.Client) {
+ g := NewWithT(t)
+
+ customKubeconfigData := WaitForCustomKubeconfig(t, ctx, client, hostedCluster)
+ customConfig, err := clientcmd.RESTConfigFromKubeConfig(customKubeconfigData)
+ g.Expect(err).NotTo(HaveOccurred(), "couldn't load KAS custom kubeconfig")
+ // we know we're the only real clients for these test servers, so turn off client-side throttling
+ customConfig.QPS = -1
+ customConfig.Burst = -1
+ if len(serverAddress) > 0 {
+ customConfig.Host = serverAddress
+ }
+ GetKubeClientSet, err := kubernetes.NewForConfig(customConfig)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create custom kube client for guest cluster")
+ kbCrclient, err := crclient.New(customConfig, crclient.Options{Scheme: scheme})
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create custom cr client for guest cluster")
+
+ return GetKubeClientSet, kbCrclient
+}
+
+// WaitForCustomKubeconfig waits for a KAS custom kubeconfig to be published for the given HostedCluster.
+func WaitForCustomKubeconfig(t *testing.T, ctx context.Context, client crclient.Client, hostedCluster *hyperv1.HostedCluster) []byte {
+ var customKubeConfigSecretRef crclient.ObjectKey
+ EventuallyObject(t, ctx, fmt.Sprintf("KAS custom kubeconfig to be published for HostedCluster %s/%s", hostedCluster.Namespace, hostedCluster.Name),
+ func(ctx context.Context) (*hyperv1.HostedCluster, error) {
+ err := client.Get(ctx, crclient.ObjectKeyFromObject(hostedCluster), hostedCluster)
+ return hostedCluster, err
+ },
+ []Predicate[*hyperv1.HostedCluster]{
+ func(cluster *hyperv1.HostedCluster) (done bool, reasons string, err error) {
+ customKubeConfigSecretRef = crclient.ObjectKey{
+ Namespace: hostedCluster.Namespace,
+ Name: ptr.Deref(hostedCluster.Status.CustomKubeconfig, corev1.LocalObjectReference{}).Name,
+ }
+ return hostedCluster.Status.CustomKubeconfig != nil, "expected a KAS custom kubeconfig reference in status", nil
+ },
+ },
+ )
+
+ var data []byte
+ EventuallyObject(t, ctx, "KAS custom kubeconfig secret to have data",
+ func(ctx context.Context) (*corev1.Secret, error) {
+ var customKubeConfigSecret corev1.Secret
+ err := client.Get(ctx, customKubeConfigSecretRef, &customKubeConfigSecret)
+ return &customKubeConfigSecret, err
+ },
+ []Predicate[*corev1.Secret]{
+ func(secret *corev1.Secret) (done bool, reasons string, err error) {
+ var hasData bool
+ data, hasData = secret.Data["kubeconfig"]
+ return hasData, "expected secret to contain kubeconfig in data", nil
+ },
+ },
+ )
+
+ return data
+}
+
func WaitForGuestKubeConfig(t *testing.T, ctx context.Context, client crclient.Client, hostedCluster *hyperv1.HostedCluster) []byte {
var guestKubeConfigSecretRef crclient.ObjectKey
EventuallyObject(t, ctx, fmt.Sprintf("kubeconfig to be published for HostedCluster %s/%s", hostedCluster.Namespace, hostedCluster.Name),
@@ -1371,6 +1434,175 @@ func EnsureGuestWebhooksValidated(t *testing.T, ctx context.Context, guestClient
})
}
+func EnsureKubeAPIDNSName(t *testing.T, ctx context.Context, mgmtClient crclient.Client, entryHostedCluster *hyperv1.HostedCluster) {
+ AtLeast(t, Version419)
+ var (
+ hcKASCustomKubeconfigSecretName string
+ customApiServerHost = "localhost"
+ opts = framework.DefaultOptions()
+ logger = log.Log
+ hcpNamespace = manifests.HostedControlPlaneNamespace(entryHostedCluster.Namespace, entryHostedCluster.Name)
+ )
+ g := NewWithT(t)
+ if !util.IsPublicHC(entryHostedCluster) {
+ return
+ }
+
+ // Get the management cluster config
+ mgmtCfg, err := GetConfig()
+ g.Expect(err).NotTo(HaveOccurred(), "couldn't get mgmt kubernetes config")
+ // Create a kubernetes client for the management cluster
+ mgmtKubeClientset, err := kubernetes.NewForConfig(mgmtCfg)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create management kubernetes client")
+
+ forwardedLocalPort, err := framework.GetFreePort(ctx, logger, opts, t)
+ g.Expect(err).NotTo(HaveOccurred(), "couldn't fetch a free port")
+
+ // update HC with a KubeAPIDNSName
+ hc := entryHostedCluster.DeepCopy()
+ hc.Spec.KubeAPIServerDNSName = customApiServerHost
+ t.Log("Updating hosted cluster with KubeAPIDNSName")
+ err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ return mgmtClient.Update(ctx, hc)
+ })
+ g.Expect(err).NotTo(HaveOccurred(), "failed to update hosted cluster")
+
+ customApiServerURL := fmt.Sprintf("https://%s:%s", customApiServerHost, forwardedLocalPort)
+ kasCustomKubeconfigClient, kbCrclient := GetCustomKubeconfigClients(t, ctx, mgmtClient, entryHostedCluster, customApiServerURL)
+
+ // wait for the KubeAPIDNSName to be reconciled
+ t.Log("waiting for the KubeAPIDNSName to be reconciled")
+ _ = WaitForCustomKubeconfig(t, ctx, mgmtClient, entryHostedCluster)
+
+ // Get HC and HCP updated
+ err = mgmtClient.Get(ctx, client.ObjectKeyFromObject(hc), hc)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to get updated HostedCluster")
+
+ hcp := &hyperv1.HostedControlPlane{}
+ err = mgmtClient.Get(ctx, types.NamespacedName{Namespace: hcpNamespace, Name: entryHostedCluster.Name}, hcp)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to get updated HostedControlPlane")
+
+ // Forward the kube-apiserver port to the management cluster
+ podToForward, err := supportforwarder.GetRunningKubeAPIServerPod(ctx, mgmtClient, hcpNamespace)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to get running kube-apiserver pod for guest cluster")
+ forwarderOutput := &bytes.Buffer{}
+ forwarder := supportforwarder.PortForwarder{
+ Namespace: podToForward.Namespace,
+ PodName: podToForward.Name,
+ Config: mgmtCfg,
+ Client: mgmtKubeClientset,
+ Out: forwarderOutput,
+ ErrOut: forwarderOutput,
+ }
+ podPort := hyperutil.KASPodPortFromHostedCluster(hc)
+ forwarderStop := make(chan struct{})
+ g.Expect(err).NotTo(HaveOccurred(), "failed to convert port string to int: %v", err)
+ err = forwarder.ForwardPorts([]string{fmt.Sprintf("%s:%d", forwardedLocalPort, podPort)}, forwarderStop)
+ g.Expect(err).NotTo(HaveOccurred(), "cannot forward kube apiserver port: %w, output: %s", err, forwarderOutput.String())
+ defer close(forwarderStop)
+
+ t.Run("EnsureCustomAdminKubeconfigStatusExists", func(t *testing.T) {
+ g := NewWithT(t)
+ t.Log("Checking CustomAdminKubeconfigStatus are present")
+ g.Expect(hcp.Status.CustomKubeconfig).ToNot(BeNil(), "HostedControlPlaneKASCustomKubeconfigis nil")
+ g.Expect(hc.Status.CustomKubeconfig).ToNot(BeNil(), "HostedClusterKASCustomKubeconfigis nil")
+ hcKASCustomKubeconfigSecretName = hc.Status.CustomKubeconfig.Name
+ })
+ t.Run("EnsureCustomAdminKubeconfigExists", func(t *testing.T) {
+ g := NewWithT(t)
+ // Get KASCustomKubeconfig secret from HCP Namespace
+ t.Log("Checking CustomAdminKubeconfigs are present")
+ hcpKASCustomKubeconfig := cpomanifests.KASCustomKubeconfigSecret(hcpNamespace, nil)
+ err := mgmtClient.Get(ctx, client.ObjectKeyFromObject(hcpKASCustomKubeconfig), hcpKASCustomKubeconfig)
+ g.Expect(err).ToNot(HaveOccurred(), "failed to get KAS custom kubeconfig secret")
+ g.Expect(hc.Status.CustomKubeconfig).ToNot(BeNil(), "KASCustomKubeconfig is nil")
+
+ // Get KASCustomKubeconfig secret from HC Namespace
+ hcCustomKubeconfigSecret := &corev1.Secret{}
+ err = mgmtClient.Get(ctx, types.NamespacedName{Namespace: hc.Namespace, Name: hc.Status.CustomKubeconfig.Name}, hcCustomKubeconfigSecret)
+ g.Expect(err).ToNot(HaveOccurred(), "failed to get KAS custom kubeconfig secret from HC namespace")
+ })
+ t.Run("EnsureCustomAdminKubeconfigReachesTheKAS", func(t *testing.T) {
+ g := NewWithT(t)
+ t.Log("Checking CustomAdminKubeconfig reaches the KAS")
+ cv := &configv1.ClusterVersion{}
+ err := kbCrclient.Get(ctx, types.NamespacedName{Name: "version"}, cv)
+ g.Expect(err).ToNot(HaveOccurred(), "failed to get HostedCluster ClusterVersion with KAS custom kubeconfig")
+ })
+ t.Run("EnsureCustomAdminKubeconfigInfraStatusIsUpdated", func(t *testing.T) {
+ g := NewWithT(t)
+ t.Log("Checking CustomAdminKubeconfig Infrastructure status is updated")
+ EventuallyObject(t, ctx, "a successful connection to the custom DNS guest API server",
+ func(ctx context.Context) (*authenticationv1.SelfSubjectReview, error) {
+ return kasCustomKubeconfigClient.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
+ }, nil, WithTimeout(30*time.Minute),
+ )
+ infra := &configv1.Infrastructure{}
+ err := kbCrclient.Get(ctx, types.NamespacedName{Name: "cluster"}, infra)
+ g.Expect(err).ToNot(HaveOccurred(), "failed to get HostedCluster Infrastructure with KAS custom kubeconfig")
+ g.Expect(infra.Status.APIServerURL).To(ContainSubstring(hc.Spec.KubeAPIServerDNSName), "Infrastructure APIServerURL does not contains the KubeAPIServerDNSName set in the HostedCluster")
+ })
+
+ // removing KubeAPIDNSName from HC
+ hc.Spec.KubeAPIServerDNSName = ""
+ err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ return mgmtClient.Update(ctx, hc)
+ })
+ g.Expect(err).NotTo(HaveOccurred(), "failed to update hosted control plane")
+
+ EventuallyObject(t, ctx, "the KAS custom kubeconfig secret to be deleted",
+ func(ctx context.Context) (*hyperv1.HostedCluster, error) {
+ hc := &hyperv1.HostedCluster{}
+ err := mgmtClient.Get(ctx, types.NamespacedName{Name: entryHostedCluster.Name, Namespace: entryHostedCluster.Namespace}, hc)
+ return hc, err
+ },
+ []Predicate[*hyperv1.HostedCluster]{
+ func(hostedCluster *hyperv1.HostedCluster) (done bool, reason string, err error) {
+ if hostedCluster.Status.CustomKubeconfig != nil {
+ return false, fmt.Sprintf("KAS custom kubeconfig secret still exists: %s", hostedCluster.Status.CustomKubeconfig.Name), nil
+ }
+ return true, "KAS custom kubeconfig secret disappeared", nil
+ },
+ }, WithInterval(5*time.Second), WithTimeout(30*time.Minute),
+ )
+
+ t.Run("EnsureCustomAdminKubeconfigIsRemoved", func(t *testing.T) {
+ g := NewWithT(t)
+ t.Log("Checking CustomAdminKubeconfig are removed")
+ hcpKASCustomKubeconfig := cpomanifests.KASCustomKubeconfigSecret(hcpNamespace, nil)
+ err := mgmtClient.Get(ctx, client.ObjectKeyFromObject(hcpKASCustomKubeconfig), hcpKASCustomKubeconfig)
+ g.Expect(err).To(HaveOccurred(), "KAS custom kubeconfig secret still exists in HCP namespace")
+
+ // Get KASCustomKubeconfig secret from HC Namespace
+ hcKASCustomKubeconfigSecret := &corev1.Secret{}
+ err = mgmtClient.Get(ctx, types.NamespacedName{Namespace: hc.Namespace, Name: hcKASCustomKubeconfigSecretName}, hcKASCustomKubeconfigSecret)
+ g.Expect(err).To(HaveOccurred(), "KAS custom kubeconfig secret still exists in HC namespace")
+ })
+
+ updatedHC := &hyperv1.HostedCluster{}
+ EventuallyObject(t, ctx, "the KAS custom kubeconfig status to be removed",
+ func(ctx context.Context) (*hyperv1.HostedCluster, error) {
+ err := mgmtClient.Get(ctx, types.NamespacedName{Name: entryHostedCluster.Name, Namespace: entryHostedCluster.Namespace}, updatedHC)
+ return updatedHC, err
+ },
+ []Predicate[*hyperv1.HostedCluster]{
+ func(hostedCluster *hyperv1.HostedCluster) (done bool, reason string, err error) {
+ if updatedHC.Status.CustomKubeconfig != nil {
+ return false, fmt.Sprintf("KAS custom kubeconfig status still exists: %s", updatedHC.Status.CustomKubeconfig), nil
+ }
+ return true, "KAS custom kubeconfig status disappeared", nil
+ },
+ }, WithInterval(5*time.Second), WithTimeout(30*time.Minute),
+ )
+
+ t.Run("EnsureCustomAdminKubeconfigStatusIsRemoved", func(t *testing.T) {
+ g := NewWithT(t)
+ t.Log("Checking CustomAdminKubeconfigStatus are removed")
+ g.Expect(updatedHC.Status.CustomKubeconfig).To(BeNil(), "HostedClusterKASCustomKubeconfigis not nil")
+ })
+
+}
+
func EnsureAdmissionPolicies(t *testing.T, ctx context.Context, mgmtClient crclient.Client, hc *hyperv1.HostedCluster) {
if !util.IsPublicHC(hc) {
return // Admission policies are only validated in public clusters does not worth to test it in private ones.
diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hosted_controlplane.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hosted_controlplane.go
index e227344d0c75..035c6f758701 100644
--- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hosted_controlplane.go
+++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hosted_controlplane.go
@@ -115,6 +115,20 @@ type HostedControlPlaneSpec struct {
// +optional
KubeConfig *KubeconfigSecretRef `json:"kubeconfig,omitempty"`
+ // kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS.
+ // When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field.
+ // If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted.
+ // The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider.
+ // This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable
+ // access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates
+ // for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates.
+ //
+ // +kubebuilder:validation:XValidation:rule=`self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$')`,message="kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com)"
+ // +kubebuilder:validation:MaxLength=253
+ // +kubebuilder:example: "api.example.com"
+ // +optional
+ KubeAPIServerDNSName string `json:"kubeAPIServerDNSName,omitempty"`
+
// Services defines metadata about how control plane services are published
// in the management cluster.
// +kubebuilder:validation:MaxItems=6
@@ -314,6 +328,15 @@ type HostedControlPlaneStatus struct {
// for this control plane.
KubeConfig *KubeconfigSecretRef `json:"kubeConfig,omitempty"`
+ // customKubeconfig references an external custom kubeconfig secret.
+ // This field is populated in the status when a custom kubeconfig secret has been generated
+ // for the hosted cluster. It contains the name and key of the secret located in the
+ // hostedCluster namespace. This field is only populated when kubeApiExternalName is set.
+ // If this field is removed during a day 2 operation, the referenced secret will be deleted
+ // and this field will be removed from the hostedCluster status.
+ // +optional
+ CustomKubeconfig *KubeconfigSecretRef `json:"customKubeconfig,omitempty"`
+
// KubeadminPassword is a reference to the secret containing the initial kubeadmin password
// for the guest cluster.
// +optional
diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go
index a018bb26f91a..f4fb6eb8ba80 100644
--- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go
+++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go
@@ -471,6 +471,20 @@ type HostedClusterSpec struct {
// +required
Platform PlatformSpec `json:"platform"`
+ // kubeAPIServerDNSName specifies a desired DNS name to resolve to the KAS.
+ // When set, the controller will automatically generate a secret with kubeconfig and expose it in the hostedCluster Status.customKubeconfig field.
+ // If it's set or removed day 2, the kubeconfig generated secret will be created, recreated or deleted.
+ // The DNS entries should be resolvable from the cluster, so this should be manually configured in the DNS provider.
+ // This field works in conjunction with configuration.APIServer.ServingCerts.NamedCertificates to enable
+ // access to the API server via a custom domain name. The NamedCertificates provide the TLS certificates
+ // for the custom domain, while this field triggers the generation of a kubeconfig that uses those certificates.
+ // This API endpoint only works in OCP version 4.19 or later. Older versions will result in a no-op.
+ // +kubebuilder:validation:XValidation:rule=`self == "" || self.matches('^(?:(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+)$')`,message="kubeAPIServerDNSName must be a valid URL name (e.g., api.example.com)"
+ // +kubebuilder:validation:MaxLength=253
+ // +kubebuilder:example: "api.example.com"
+ // +optional
+ KubeAPIServerDNSName string `json:"kubeAPIServerDNSName,omitempty"`
+
// controllerAvailabilityPolicy specifies the availability policy applied to critical control plane components like the Kube API Server.
// Possible values are HighlyAvailable and SingleReplica. The default value is HighlyAvailable.
// This field is immutable.
@@ -1478,6 +1492,11 @@ type HostedClusterStatus struct {
// +optional
KubeConfig *corev1.LocalObjectReference `json:"kubeconfig,omitempty"`
+ // CustomKubeconfig is a local secret reference to the external custom kubeconfig.
+ // Once the hypershift operator sets this status field, it will generate a secret with the specified name containing a kubeconfig within the `HostedCluster` namespace.
+ // +optional
+ CustomKubeconfig *corev1.LocalObjectReference `json:"customKubeconfig,omitempty"`
+
// KubeadminPassword is a reference to the secret that contains the initial
// kubeadmin user password for the guest cluster.
// +optional
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 eb752dddb349..7f27701145d2 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
@@ -1505,6 +1505,11 @@ func (in *HostedClusterStatus) DeepCopyInto(out *HostedClusterStatus) {
*out = new(corev1.LocalObjectReference)
**out = **in
}
+ if in.CustomKubeconfig != nil {
+ in, out := &in.CustomKubeconfig, &out.CustomKubeconfig
+ *out = new(corev1.LocalObjectReference)
+ **out = **in
+ }
if in.KubeadminPassword != nil {
in, out := &in.KubeadminPassword, &out.KubeadminPassword
*out = new(corev1.LocalObjectReference)
@@ -1729,6 +1734,11 @@ func (in *HostedControlPlaneStatus) DeepCopyInto(out *HostedControlPlaneStatus)
*out = new(KubeconfigSecretRef)
**out = **in
}
+ if in.CustomKubeconfig != nil {
+ in, out := &in.CustomKubeconfig, &out.CustomKubeconfig
+ *out = new(KubeconfigSecretRef)
+ **out = **in
+ }
if in.KubeadminPassword != nil {
in, out := &in.KubeadminPassword, &out.KubeadminPassword
*out = new(corev1.LocalObjectReference)