From 0aaa1e95da1f66cdaa47a905068e059ee10c0c3c Mon Sep 17 00:00:00 2001
From: Juan Manuel Parrilla Madrid
Date: Tue, 4 Feb 2025 14:24:31 +0100
Subject: [PATCH 1/4] CNTRLPLANE-216: Add KubeAPIExteralName api
This new API changes the value of the KAS Custom URL to points to your desired one
Signed-off-by: Juan Manuel Parrilla Madrid
---
Dockerfile.control-plane | 1 +
Dockerfile.dev | 3 +-
api/hypershift/v1beta1/hosted_controlplane.go | 23 ++
api/hypershift/v1beta1/hostedcluster_types.go | 19 ++
.../hostedcontrolplane_controller.go | 90 ++++++-
.../hostedcontrolplane_controller_test.go | 166 ++++++++++++
.../hostedcontrolplane/kas/kubeconfig.go | 2 +-
.../hostedcontrolplane/kas/params.go | 39 ++-
.../hostedcontrolplane/manifests/kas.go | 15 +-
.../hostedcontrolplane/manifests/pki.go | 9 +
.../custom-admin-kubeconfig.yaml | 8 +
.../hostedcontrolplane/v2/kas/component.go | 11 +
.../hostedcontrolplane/v2/kas/kubeconfig.go | 26 +-
.../how-to/aws/define-custom-kube-api-name.md | 35 +++
docs/mkdocs.yml | 1 +
.../hostedcluster/hostedcluster_controller.go | 105 ++++++--
.../hostedcluster_controller_test.go | 32 ++-
.../controllers/manifests/manifests.go | 9 +
support/globalconfig/infrastructure.go | 3 +
support/util/util.go | 10 +
test/e2e/create_cluster_test.go | 45 ++++
test/e2e/util/util.go | 237 ++++++++++++++++++
22 files changed, 853 insertions(+), 36 deletions(-)
create mode 100644 control-plane-operator/controllers/hostedcontrolplane/v2/assets/kube-apiserver/custom-admin-kubeconfig.yaml
create mode 100644 docs/content/how-to/aws/define-custom-kube-api-name.md
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/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go
index 91ee569a7c0c..3c5570a58af1 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["tls.crt"]
+ }
+
+ tlsCRT = fmt.Sprintf("%s\n%s", tlsCRT, string(newCRT.Data["tls.crt"]))
+ }
+
+ if len(tlsCRT) > 0 {
+ newRootCA.Data["tls.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..a4c586da27d7 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{
+ "tls.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["tls.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/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/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..4c0428ae497b 100644
--- a/support/util/util.go
+++ b/support/util/util.go
@@ -681,4 +681,14 @@ 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 {
+ if len(hcp.Spec.KubeAPIServerDNSName) > 0 {
+ return true
+ }
+
+ return false
}
diff --git a/test/e2e/create_cluster_test.go b/test/e2e/create_cluster_test.go
index 3662b7f52d07..10f1ec8cde13 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 {
diff --git a/test/e2e/util/util.go b/test/e2e/util/util.go
index b0071806a0e2..b8fb95c3745b 100644
--- a/test/e2e/util/util.go
+++ b/test/e2e/util/util.go
@@ -150,6 +150,73 @@ func DeleteNamespace(t *testing.T, ctx context.Context, client crclient.Client,
return nil
}
+<<<<<<< Updated upstream
+=======
+// WaitForKASCustomKubeconfigClient waits for the KAS custom kubeconfig to be published for the given HostedCluster and returns a client for it.
+func WaitForKASCustomKubeconfigClient(t *testing.T, ctx context.Context, client crclient.Client, hostedCluster *hyperv1.HostedCluster, serverAddress string) 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
+ }
+ kubeClient, err := kubernetes.NewForConfig(customConfig)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create custom kube client for guest cluster")
+ EventuallyObject(t, ctx, "a successful connection to the custom DNS guest API server",
+ func(ctx context.Context) (*authenticationv1.SelfSubjectReview, error) {
+ return kubeClient.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
+ }, nil, WithTimeout(30*time.Minute),
+ )
+
+ customClient, err := crclient.New(customConfig, crclient.Options{Scheme: scheme})
+ g.Expect(err).NotTo(HaveOccurred(), "could not create custom DNS client for guest cluster")
+
+ return customClient
+}
+
+// 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
+}
+
+>>>>>>> Stashed changes
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 +1438,176 @@ func EnsureGuestWebhooksValidated(t *testing.T, ctx context.Context, guestClient
})
}
+<<<<<<< Updated upstream
+=======
+func EnsureKubeAPIDNSName(t *testing.T, ctx context.Context, mgmtClient crclient.Client, hc *hyperv1.HostedCluster) {
+ AtLeast(t, Version419)
+ var (
+ hcKASCustomKubeconfigSecretName string
+ customApiServerHost = "localhost"
+ opts = framework.DefaultOptions()
+ logger = log.Log
+ )
+ g := NewWithT(t)
+ if !util.IsPublicHC(hc) {
+ return
+ }
+
+ mgmtCfg, err := GetConfig()
+ g.Expect(err).NotTo(HaveOccurred(), "couldn't get mgmt kubernetes config")
+ forwardedLocalPort, err := framework.GetFreePort(ctx, logger, opts, t)
+ g.Expect(err).NotTo(HaveOccurred(), "couldn't fetch a free port")
+ customApiServerURL := fmt.Sprintf("https://%s:%s", customApiServerHost, forwardedLocalPort)
+ mgmtKubeconfig, err := RestConfigToKubeconfig(mgmtCfg, "mgmt")
+ g.Expect(err).NotTo(HaveOccurred(), "couldn't generate kubeconfig from rest.Config")
+ t.Logf("Generated kubeconfig at: %s", mgmtKubeconfig)
+
+ guestClient := WaitForGuestClient(t, ctx, mgmtClient, hc)
+ hcpNamespace := manifests.HostedControlPlaneNamespace(hc.Namespace, hc.Name)
+
+ hcp := &hyperv1.HostedControlPlane{}
+ err = mgmtClient.Get(ctx, types.NamespacedName{Name: hc.Name, Namespace: hcpNamespace}, hcp)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to get hosted control plane")
+
+ // update HC with a KubeAPIDNSName
+ hcCP := hc.DeepCopy()
+ hcCP.Spec.KubeAPIServerDNSName = customApiServerHost
+ t.Log("Updating hosted cluster with KubeAPIDNSName")
+ err = mgmtClient.Update(ctx, hcCP)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to update hosted cluster")
+
+ // wait for the KubeAPIDNSName to be reconciled
+ t.Log("waiting for the KubeAPIDNSName to be reconciled")
+ _ = WaitForCustomKubeconfig(t, ctx, mgmtClient, hc)
+
+ // Get HC and HCP updated
+ err = mgmtClient.Get(ctx, client.ObjectKeyFromObject(hc), hc)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to get updated HostedCluster")
+ err = mgmtClient.Get(ctx, client.ObjectKeyFromObject(hcp), hcp)
+ g.Expect(err).NotTo(HaveOccurred(), "failed to get updated HostedControlPlane")
+
+ opts.Kubeconfig = mgmtKubeconfig
+ g.Expect(err).NotTo(HaveOccurred(), "couldn't fetch a free port")
+
+ go func() {
+ portForwardCtx := context.Background() // we need this during cleanup, possible to do better but hard
+ logPath := "apiserver-port-forward.log"
+ cmd := exec.CommandContext(portForwardCtx, opts.OCPath,
+ "port-forward", "service/kube-apiserver", "--namespace", hcpNamespace,
+ fmt.Sprintf("%s:6443", forwardedLocalPort),
+ "--kubeconfig", opts.Kubeconfig,
+ )
+ if err := framework.StartCommand(logger, opts, logPath, cmd); err != nil {
+ logger.Error(err, "failed to start port-forwarding")
+ }
+ }()
+
+ 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")
+ kasCustomKubeconfigClient := WaitForKASCustomKubeconfigClient(t, ctx, mgmtClient, hc, customApiServerURL)
+ cv := &configv1.ClusterVersion{}
+ err := kasCustomKubeconfigClient.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")
+ kasCustomKubeconfigClient := WaitForKASCustomKubeconfigClient(t, ctx, mgmtClient, hc, customApiServerURL)
+ infra := &configv1.Infrastructure{}
+ err := kasCustomKubeconfigClient.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
+ hcCP = hc.DeepCopy()
+ hcCP.Spec.KubeAPIServerDNSName = ""
+ err = mgmtClient.Update(ctx, hcCP)
+ 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: hcCP.Name, Namespace: hcCP.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: hcCP.Name, Namespace: hcCP.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")
+ })
+ t.Run("EnsureCustomAdminKubeconfigInfraStatusMatchesAPIInt", func(t *testing.T) {
+ g := NewWithT(t)
+ t.Log("Checking APIServerURL points back to the same address as APIServerInternalURL")
+ infra := &configv1.Infrastructure{}
+ err := guestClient.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(Equal(infra.Status.APIServerInternalURL), "Infrastructure APIServerURL and APIServerInternalURL should be equal")
+ })
+}
+
+>>>>>>> Stashed changes
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.
From 749d7767c42bd12becadc33edccb58997fa23fa3 Mon Sep 17 00:00:00 2001
From: Juan Manuel Parrilla Madrid
Date: Tue, 4 Feb 2025 14:27:58 +0100
Subject: [PATCH 2/4] CNTRLPLANE-216: Add autogenerated code and files
Signed-off-by: Juan Manuel Parrilla Madrid
---
.../v1beta1/zz_generated.deepcopy.go | 10 +++
.../AAA_ungated.yaml | 31 +++++++
.../AutoNodeKarpenter.yaml | 31 +++++++
.../ClusterVersionOperatorConfiguration.yaml | 31 +++++++
.../DynamicResourceAllocation.yaml | 31 +++++++
.../ExternalOIDC.yaml | 31 +++++++
.../ImageStreamImportMode.yaml | 31 +++++++
.../NetworkDiagnosticsConfig.yaml | 31 +++++++
.../OpenStack.yaml | 31 +++++++
.../AAA_ungated.yaml | 31 +++++++
.../AutoNodeKarpenter.yaml | 31 +++++++
.../ClusterVersionOperatorConfiguration.yaml | 31 +++++++
.../DynamicResourceAllocation.yaml | 31 +++++++
.../ExternalOIDC.yaml | 31 +++++++
.../ImageStreamImportMode.yaml | 31 +++++++
.../NetworkDiagnosticsConfig.yaml | 31 +++++++
.../OpenStack.yaml | 31 +++++++
.../hypershift/v1beta1/hostedclusterspec.go | 9 ++
.../hypershift/v1beta1/hostedclusterstatus.go | 9 ++
.../v1beta1/hostedcontrolplanespec.go | 9 ++
.../v1beta1/hostedcontrolplanestatus.go | 9 ++
.../hostedclusters-CustomNoUpgrade.crd.yaml | 31 +++++++
.../hostedclusters-Default.crd.yaml | 31 +++++++
...stedclusters-TechPreviewNoUpgrade.crd.yaml | 31 +++++++
...stedcontrolplanes-CustomNoUpgrade.crd.yaml | 31 +++++++
.../hostedcontrolplanes-Default.crd.yaml | 31 +++++++
...ontrolplanes-TechPreviewNoUpgrade.crd.yaml | 31 +++++++
docs/content/reference/api.md | 90 +++++++++++++++++++
.../hypershift/v1beta1/hosted_controlplane.go | 23 +++++
.../hypershift/v1beta1/hostedcluster_types.go | 19 ++++
.../v1beta1/zz_generated.deepcopy.go | 10 +++
31 files changed, 870 insertions(+)
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/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/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
+
+ |
+
+(Optional)
+ 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
+
+ |
+
+(Optional)
+ 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
+
+
+ |
+
+(Optional)
+ 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
+
+ |
+
+(Optional)
+ 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
+
+
+ |
+
+(Optional)
+ 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/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)
From fa1ff156c516ccc4038dd195341e467c04a6b549 Mon Sep 17 00:00:00 2001
From: Juan Manuel Parrilla Madrid
Date: Fri, 14 Feb 2025 17:53:07 +0100
Subject: [PATCH 3/4] CNTRLPLANE-216: Add E2E test for new API KubeAPIDNSName
Signed-off-by: Juan Manuel Parrilla Madrid
---
cmd/cluster/core/dump.go | 32 ++---
.../hostedcontrolplane_controller.go | 4 +-
.../hostedcontrolplane_controller_test.go | 4 +-
.../core => support/forwarder}/forwarder.go | 31 +++-
support/forwarder/forwarder_test.go | 134 ++++++++++++++++++
test/e2e/create_cluster_test.go | 1 +
test/e2e/util/util.go | 126 ++++++++--------
7 files changed, 235 insertions(+), 97 deletions(-)
rename {cmd/cluster/core => support/forwarder}/forwarder.go (51%)
create mode 100644 support/forwarder/forwarder_test.go
diff --git a/cmd/cluster/core/dump.go b/cmd/cluster/core/dump.go
index 8aa23fddbc44..9ac27ade99f7 100644
--- a/cmd/cluster/core/dump.go
+++ b/cmd/cluster/core/dump.go
@@ -12,6 +12,11 @@ import (
"strings"
"time"
+ configv1 "github.com/openshift/api/config/v1"
+ imagev1 "github.com/openshift/api/image/v1"
+ routev1 "github.com/openshift/api/route/v1"
+ securityv1 "github.com/openshift/api/security/v1"
+ agentv1 "github.com/openshift/cluster-api-provider-agent/api/v1beta1"
hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
scheduling "github.com/openshift/hypershift/api/scheduling/v1alpha1"
"github.com/openshift/hypershift/cmd/log"
@@ -20,14 +25,9 @@ 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"
- imagev1 "github.com/openshift/api/image/v1"
- routev1 "github.com/openshift/api/route/v1"
- securityv1 "github.com/openshift/api/security/v1"
- agentv1 "github.com/openshift/cluster-api-provider-agent/api/v1beta1"
-
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
@@ -183,21 +183,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 +204,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/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go
index 3c5570a58af1..eb1320cb2aa3 100644
--- a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go
+++ b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller.go
@@ -5741,14 +5741,14 @@ func includeServingCertificates(ctx context.Context, c client.Client, hcp *hyper
}
if len(tlsCRT) <= 0 {
- tlsCRT = newRootCA.Data["tls.crt"]
+ tlsCRT = newRootCA.Data["ca.crt"]
}
tlsCRT = fmt.Sprintf("%s\n%s", tlsCRT, string(newCRT.Data["tls.crt"]))
}
if len(tlsCRT) > 0 {
- newRootCA.Data["tls.crt"] = tlsCRT
+ newRootCA.Data["ca.crt"] = tlsCRT
}
}
diff --git a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go
index a4c586da27d7..e6bd241a5c95 100644
--- a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go
+++ b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go
@@ -1559,7 +1559,7 @@ func TestIncludeServingCertificates(t *testing.T) {
Namespace: hcp.Namespace,
},
Data: map[string]string{
- "tls.crt": "root-ca-cert",
+ "ca.crt": "root-ca-cert",
},
}
@@ -1672,7 +1672,7 @@ func TestIncludeServingCertificates(t *testing.T) {
g.Expect(err).To(HaveOccurred())
} else {
g.Expect(err).NotTo(HaveOccurred())
- g.Expect(newRootCA.Data["tls.crt"]).To(Equal(tc.expectedCert))
+ g.Expect(newRootCA.Data["ca.crt"]).To(Equal(tc.expectedCert))
}
})
}
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..c9d044387b69 100644
--- a/cmd/cluster/core/forwarder.go
+++ b/support/forwarder/forwarder.go
@@ -1,18 +1,22 @@
-// 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 +27,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 +54,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..0f6325ec19fb
--- /dev/null
+++ b/support/forwarder/forwarder_test.go
@@ -0,0 +1,134 @@
+package forwarder
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ 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"
+
+ hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
+)
+
+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/test/e2e/create_cluster_test.go b/test/e2e/create_cluster_test.go
index 10f1ec8cde13..be456cc26d12 100644
--- a/test/e2e/create_cluster_test.go
+++ b/test/e2e/create_cluster_test.go
@@ -1374,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 b8fb95c3745b..089858d99d04 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"
@@ -150,11 +154,9 @@ func DeleteNamespace(t *testing.T, ctx context.Context, client crclient.Client,
return nil
}
-<<<<<<< Updated upstream
-=======
-// WaitForKASCustomKubeconfigClient waits for the KAS custom kubeconfig to be published for the given HostedCluster and returns a client for it.
-func WaitForKASCustomKubeconfigClient(t *testing.T, ctx context.Context, client crclient.Client, hostedCluster *hyperv1.HostedCluster, serverAddress string) crclient.Client {
+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")
@@ -164,18 +166,12 @@ func WaitForKASCustomKubeconfigClient(t *testing.T, ctx context.Context, client
if len(serverAddress) > 0 {
customConfig.Host = serverAddress
}
- kubeClient, err := kubernetes.NewForConfig(customConfig)
+ GetKubeClientSet, err := kubernetes.NewForConfig(customConfig)
g.Expect(err).NotTo(HaveOccurred(), "failed to create custom kube client for guest cluster")
- EventuallyObject(t, ctx, "a successful connection to the custom DNS guest API server",
- func(ctx context.Context) (*authenticationv1.SelfSubjectReview, error) {
- return kubeClient.AuthenticationV1().SelfSubjectReviews().Create(ctx, &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{})
- }, nil, WithTimeout(30*time.Minute),
- )
+ kbCrclient, err := crclient.New(customConfig, crclient.Options{Scheme: scheme})
+ g.Expect(err).NotTo(HaveOccurred(), "failed to create custom cr client for guest cluster")
- customClient, err := crclient.New(customConfig, crclient.Options{Scheme: scheme})
- g.Expect(err).NotTo(HaveOccurred(), "could not create custom DNS client for guest cluster")
-
- return customClient
+ return GetKubeClientSet, kbCrclient
}
// WaitForCustomKubeconfig waits for a KAS custom kubeconfig to be published for the given HostedCluster.
@@ -216,7 +212,6 @@ func WaitForCustomKubeconfig(t *testing.T, ctx context.Context, client crclient.
return data
}
->>>>>>> Stashed changes
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),
@@ -1438,69 +1433,70 @@ func EnsureGuestWebhooksValidated(t *testing.T, ctx context.Context, guestClient
})
}
-<<<<<<< Updated upstream
-=======
-func EnsureKubeAPIDNSName(t *testing.T, ctx context.Context, mgmtClient crclient.Client, hc *hyperv1.HostedCluster) {
+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(hc) {
+ 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")
- customApiServerURL := fmt.Sprintf("https://%s:%s", customApiServerHost, forwardedLocalPort)
- mgmtKubeconfig, err := RestConfigToKubeconfig(mgmtCfg, "mgmt")
- g.Expect(err).NotTo(HaveOccurred(), "couldn't generate kubeconfig from rest.Config")
- t.Logf("Generated kubeconfig at: %s", mgmtKubeconfig)
-
- guestClient := WaitForGuestClient(t, ctx, mgmtClient, hc)
- hcpNamespace := manifests.HostedControlPlaneNamespace(hc.Namespace, hc.Name)
-
- hcp := &hyperv1.HostedControlPlane{}
- err = mgmtClient.Get(ctx, types.NamespacedName{Name: hc.Name, Namespace: hcpNamespace}, hcp)
- g.Expect(err).NotTo(HaveOccurred(), "failed to get hosted control plane")
// update HC with a KubeAPIDNSName
- hcCP := hc.DeepCopy()
- hcCP.Spec.KubeAPIServerDNSName = customApiServerHost
+ hc := entryHostedCluster.DeepCopy()
+ hc.Spec.KubeAPIServerDNSName = customApiServerHost
t.Log("Updating hosted cluster with KubeAPIDNSName")
- err = mgmtClient.Update(ctx, hcCP)
+ err = 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, hc)
+ _ = 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")
- err = mgmtClient.Get(ctx, client.ObjectKeyFromObject(hcp), hcp)
- g.Expect(err).NotTo(HaveOccurred(), "failed to get updated HostedControlPlane")
- opts.Kubeconfig = mgmtKubeconfig
- g.Expect(err).NotTo(HaveOccurred(), "couldn't fetch a free port")
+ 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")
- go func() {
- portForwardCtx := context.Background() // we need this during cleanup, possible to do better but hard
- logPath := "apiserver-port-forward.log"
- cmd := exec.CommandContext(portForwardCtx, opts.OCPath,
- "port-forward", "service/kube-apiserver", "--namespace", hcpNamespace,
- fmt.Sprintf("%s:6443", forwardedLocalPort),
- "--kubeconfig", opts.Kubeconfig,
- )
- if err := framework.StartCommand(logger, opts, logPath, cmd); err != nil {
- logger.Error(err, "failed to start port-forwarding")
- }
- }()
+ // 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)
@@ -1526,31 +1522,33 @@ func EnsureKubeAPIDNSName(t *testing.T, ctx context.Context, mgmtClient crclient
t.Run("EnsureCustomAdminKubeconfigReachesTheKAS", func(t *testing.T) {
g := NewWithT(t)
t.Log("Checking CustomAdminKubeconfig reaches the KAS")
- kasCustomKubeconfigClient := WaitForKASCustomKubeconfigClient(t, ctx, mgmtClient, hc, customApiServerURL)
cv := &configv1.ClusterVersion{}
- err := kasCustomKubeconfigClient.Get(ctx, types.NamespacedName{Name: "version"}, cv)
+ 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")
- kasCustomKubeconfigClient := WaitForKASCustomKubeconfigClient(t, ctx, mgmtClient, hc, customApiServerURL)
+ 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 := kasCustomKubeconfigClient.Get(ctx, types.NamespacedName{Name: "cluster"}, infra)
+ 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
- hcCP = hc.DeepCopy()
- hcCP.Spec.KubeAPIServerDNSName = ""
- err = mgmtClient.Update(ctx, hcCP)
+ hc.Spec.KubeAPIServerDNSName = ""
+ err = 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: hcCP.Name, Namespace: hcCP.Namespace}, hc)
+ err := mgmtClient.Get(ctx, types.NamespacedName{Name: entryHostedCluster.Name, Namespace: entryHostedCluster.Namespace}, hc)
return hc, err
},
[]Predicate[*hyperv1.HostedCluster]{
@@ -1579,7 +1577,7 @@ func EnsureKubeAPIDNSName(t *testing.T, ctx context.Context, mgmtClient crclient
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: hcCP.Name, Namespace: hcCP.Namespace}, updatedHC)
+ err := mgmtClient.Get(ctx, types.NamespacedName{Name: entryHostedCluster.Name, Namespace: entryHostedCluster.Namespace}, updatedHC)
return updatedHC, err
},
[]Predicate[*hyperv1.HostedCluster]{
@@ -1597,17 +1595,9 @@ func EnsureKubeAPIDNSName(t *testing.T, ctx context.Context, mgmtClient crclient
t.Log("Checking CustomAdminKubeconfigStatus are removed")
g.Expect(updatedHC.Status.CustomKubeconfig).To(BeNil(), "HostedClusterKASCustomKubeconfigis not nil")
})
- t.Run("EnsureCustomAdminKubeconfigInfraStatusMatchesAPIInt", func(t *testing.T) {
- g := NewWithT(t)
- t.Log("Checking APIServerURL points back to the same address as APIServerInternalURL")
- infra := &configv1.Infrastructure{}
- err := guestClient.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(Equal(infra.Status.APIServerInternalURL), "Infrastructure APIServerURL and APIServerInternalURL should be equal")
- })
+
}
->>>>>>> Stashed changes
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.
From f0b828b1f67db7ac622e1d8bbf29a09437d24b01 Mon Sep 17 00:00:00 2001
From: Juan Manuel Parrilla Madrid
Date: Fri, 14 Mar 2025 12:16:49 +0100
Subject: [PATCH 4/4] NO-JIRA: Fix new gosimple minor issues in the code
Signed-off-by: Juan Manuel Parrilla Madrid
---
cmd/cluster/core/dump.go | 11 ++++++-----
.../hostedcontrolplane_controller_test.go | 2 +-
support/forwarder/forwarder.go | 2 ++
support/forwarder/forwarder_test.go | 5 +++--
support/util/util.go | 6 +-----
test/e2e/util/util.go | 9 +++++++--
6 files changed, 20 insertions(+), 15 deletions(-)
diff --git a/cmd/cluster/core/dump.go b/cmd/cluster/core/dump.go
index 9ac27ade99f7..b1a0958ec116 100644
--- a/cmd/cluster/core/dump.go
+++ b/cmd/cluster/core/dump.go
@@ -12,11 +12,6 @@ import (
"strings"
"time"
- configv1 "github.com/openshift/api/config/v1"
- imagev1 "github.com/openshift/api/image/v1"
- routev1 "github.com/openshift/api/route/v1"
- securityv1 "github.com/openshift/api/security/v1"
- agentv1 "github.com/openshift/cluster-api-provider-agent/api/v1beta1"
hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
scheduling "github.com/openshift/hypershift/api/scheduling/v1alpha1"
"github.com/openshift/hypershift/cmd/log"
@@ -28,6 +23,12 @@ import (
supportforwarder "github.com/openshift/hypershift/support/forwarder"
supportutil "github.com/openshift/hypershift/support/util"
+ configv1 "github.com/openshift/api/config/v1"
+ imagev1 "github.com/openshift/api/image/v1"
+ routev1 "github.com/openshift/api/route/v1"
+ securityv1 "github.com/openshift/api/security/v1"
+ agentv1 "github.com/openshift/cluster-api-provider-agent/api/v1beta1"
+
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
diff --git a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go
index e6bd241a5c95..98d9b736724e 100644
--- a/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go
+++ b/control-plane-operator/controllers/hostedcontrolplane/hostedcontrolplane_controller_test.go
@@ -1664,7 +1664,7 @@ func TestIncludeServingCertificates(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithObjects(rootCA).Build()
for _, secret := range tc.servingSecrets {
- fakeClient.Create(ctx, secret)
+ _ = fakeClient.Create(ctx, secret)
}
newRootCA, err := includeServingCertificates(ctx, fakeClient, hcp, rootCA)
diff --git a/support/forwarder/forwarder.go b/support/forwarder/forwarder.go
index c9d044387b69..5ea25ce826cb 100644
--- a/support/forwarder/forwarder.go
+++ b/support/forwarder/forwarder.go
@@ -7,11 +7,13 @@ import (
"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"
)
diff --git a/support/forwarder/forwarder_test.go b/support/forwarder/forwarder_test.go
index 0f6325ec19fb..af247edaf16c 100644
--- a/support/forwarder/forwarder_test.go
+++ b/support/forwarder/forwarder_test.go
@@ -5,13 +5,14 @@ import (
"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"
-
- hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
)
func TestGetRunningKubeAPIServerPod(t *testing.T) {
diff --git a/support/util/util.go b/support/util/util.go
index 4c0428ae497b..fe45de13a3c4 100644
--- a/support/util/util.go
+++ b/support/util/util.go
@@ -686,9 +686,5 @@ func HostFromURL(addr string) (string, error) {
// EnableIfCustomKubeconfig returns true if the hosted control plane has a custom kubeconfig defined
func EnableIfCustomKubeconfig(hcp *hyperv1.HostedControlPlane) bool {
- if len(hcp.Spec.KubeAPIServerDNSName) > 0 {
- return true
- }
-
- return false
+ return len(hcp.Spec.KubeAPIServerDNSName) > 0
}
diff --git a/test/e2e/util/util.go b/test/e2e/util/util.go
index 089858d99d04..b8991bfe285c 100644
--- a/test/e2e/util/util.go
+++ b/test/e2e/util/util.go
@@ -52,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"
@@ -1461,7 +1462,9 @@ func EnsureKubeAPIDNSName(t *testing.T, ctx context.Context, mgmtClient crclient
hc := entryHostedCluster.DeepCopy()
hc.Spec.KubeAPIServerDNSName = customApiServerHost
t.Log("Updating hosted cluster with KubeAPIDNSName")
- err = mgmtClient.Update(ctx, hc)
+ 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)
@@ -1542,7 +1545,9 @@ func EnsureKubeAPIDNSName(t *testing.T, ctx context.Context, mgmtClient crclient
// removing KubeAPIDNSName from HC
hc.Spec.KubeAPIServerDNSName = ""
- err = mgmtClient.Update(ctx, hc)
+ 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",
|