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",