diff --git a/deploy/crds/kosmos.io_virtualclusters.yaml b/deploy/crds/kosmos.io_virtualclusters.yaml index aa4f3b3be..95aa72dc5 100644 --- a/deploy/crds/kosmos.io_virtualclusters.yaml +++ b/deploy/crds/kosmos.io_virtualclusters.yaml @@ -47,6 +47,12 @@ spec: description: ExternalIP is the external ip of the virtual kubernetes's control plane type: string + externalIps: + description: ExternalIps is the external ips of the virtual kubernetes's + control plane + items: + type: string + type: array kubeconfig: description: Kubeconfig is the kubeconfig of the virtual kubernetes's control plane diff --git a/pkg/apis/kosmos/v1alpha1/virtualcluster_types.go b/pkg/apis/kosmos/v1alpha1/virtualcluster_types.go index cd8b91dda..1d2ade319 100644 --- a/pkg/apis/kosmos/v1alpha1/virtualcluster_types.go +++ b/pkg/apis/kosmos/v1alpha1/virtualcluster_types.go @@ -53,6 +53,10 @@ type VirtualClusterSpec struct { // +optional ExternalIP string `json:"externalIP,omitempty"` + // ExternalIps is the external ips of the virtual kubernetes's control plane + // +optional + ExternalIps []string `json:"externalIps,omitempty"` + // PromotePolicies definites the policies for promote to the kubernetes's control plane // +required PromotePolicies []PromotePolicy `json:"promotePolicies,omitempty"` diff --git a/pkg/kubenest/controlplane/endpoint.go b/pkg/kubenest/controlplane/endpoint.go index cf67d09ec..f23924305 100644 --- a/pkg/kubenest/controlplane/endpoint.go +++ b/pkg/kubenest/controlplane/endpoint.go @@ -6,12 +6,10 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" "github.com/kosmos.io/kosmos/pkg/kubenest/constants" @@ -19,72 +17,66 @@ import ( "github.com/kosmos.io/kosmos/pkg/kubenest/util" ) -func EnsureApiServerExternalEndPoint(dynamicClient dynamic.Interface) error { - err := installApiServerExternalEndpointInVirtualCluster(dynamicClient) +func EnsureApiServerExternalEndPoint(kubeClient kubernetes.Interface) error { + err := CreateOrUpdateApiServerExternalEndpoint(kubeClient) if err != nil { return err } - err = installApiServerExternalServiceInVirtualCluster(dynamicClient) + err = CreateOrUpdateApiServerExternalService(kubeClient) if err != nil { return err } return nil } -func installApiServerExternalEndpointInVirtualCluster(dynamicClient dynamic.Interface) error { +func CreateOrUpdateApiServerExternalEndpoint(kubeClient kubernetes.Interface) error { klog.V(4).Info("begin to get kubernetes endpoint") - kubeEndpointUnstructured, err := dynamicClient.Resource(schema.GroupVersionResource{ - Group: "", - Version: "v1", - Resource: "endpoints", - }).Namespace(constants.DefaultNs).Get(context.TODO(), "kubernetes", metav1.GetOptions{}) + kubeEndpoint, err := kubeClient.CoreV1().Endpoints(constants.DefaultNs).Get(context.TODO(), "kubernetes", metav1.GetOptions{}) if err != nil { klog.Error("get Kubernetes endpoint failed", err) return errors.Wrap(err, "failed to get kubernetes endpoint") } - klog.V(4).Info("the Kubernetes endpoint is:", kubeEndpointUnstructured) + klog.V(4).Info("the Kubernetes endpoint is:", kubeEndpoint) - if kubeEndpointUnstructured != nil { - kubeEndpoint := &corev1.Endpoints{} - err := runtime.DefaultUnstructuredConverter.FromUnstructured(kubeEndpointUnstructured.Object, kubeEndpoint) - if err != nil { - klog.Error("switch Kubernetes endpoint to typed object failed", err) - return errors.Wrap(err, "failed to convert kubernetes endpoint to typed object") + newEndpoint := kubeEndpoint.DeepCopy() + newEndpoint.Name = constants.ApiServerExternalService + newEndpoint.Namespace = constants.DefaultNs + newEndpoint.ResourceVersion = "" + + // Try to create the endpoint + _, err = kubeClient.CoreV1().Endpoints(constants.DefaultNs).Create(context.TODO(), newEndpoint, metav1.CreateOptions{}) + if err != nil { + if !apierrors.IsAlreadyExists(err) { + klog.Error("create api-server-external-service endpoint failed", err) + return errors.Wrap(err, "failed to create api-server-external-service endpoint") } - newEndpoint := kubeEndpoint.DeepCopy() - newEndpoint.Name = constants.ApiServerExternalService - newEndpoint.Namespace = constants.DefaultNs - newEndpoint.ResourceVersion = "" - newEndpointUnstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(newEndpoint) + // Endpoint already exists, retrieve it + existingEndpoint, err := kubeClient.CoreV1().Endpoints(constants.DefaultNs).Get(context.TODO(), constants.ApiServerExternalService, metav1.GetOptions{}) if err != nil { - klog.Error("switch new endpoint to unstructured object failed", err) - return errors.Wrap(err, "failed to convert new endpoint to unstructured object") + klog.Error("get existing api-server-external-service endpoint failed", err) + return errors.Wrap(err, "failed to get existing api-server-external-service endpoint") } - klog.V(4).Info("after switch the Endpoint unstructured is:", newEndpointUnstructuredObj) - - newEndpointUnstructured := &unstructured.Unstructured{Object: newEndpointUnstructuredObj} - createResult, err := dynamicClient.Resource(schema.GroupVersionResource{ - Group: "", - Version: "v1", - Resource: "endpoints", - }).Namespace(constants.DefaultNs).Create(context.TODO(), newEndpointUnstructured, metav1.CreateOptions{}) + + // Update the existing endpoint + newEndpoint.SetResourceVersion(existingEndpoint.ResourceVersion) + _, err = kubeClient.CoreV1().Endpoints(constants.DefaultNs).Update(context.TODO(), newEndpoint, metav1.UpdateOptions{}) if err != nil { - klog.Error("create api-server-external-service endpoint failed", err) - return errors.Wrap(err, "failed to create api-server-external-service endpoint") + klog.Error("update api-server-external-service endpoint failed", err) + return errors.Wrap(err, "failed to update api-server-external-service endpoint") } else { - klog.V(4).Info("success create api-server-external-service endpoint:", createResult) + klog.V(4).Info("successfully updated api-server-external-service endpoint") } } else { - return errors.New("kubernetes endpoint does not exist") + klog.V(4).Info("successfully created api-server-external-service endpoint") } return nil } -func installApiServerExternalServiceInVirtualCluster(dynamicClient dynamic.Interface) error { - port, err := getEndPointPort(dynamicClient) +func CreateOrUpdateApiServerExternalService(kubeClient kubernetes.Interface) error { + port, err := getEndPointPort(kubeClient) if err != nil { return fmt.Errorf("error when getEndPointPort: %w", err) } @@ -97,62 +89,58 @@ func installApiServerExternalServiceInVirtualCluster(dynamicClient dynamic.Inter return fmt.Errorf("error when parsing api-server-external-serive template: %w", err) } - var obj unstructured.Unstructured - if err := yaml.Unmarshal([]byte(apiServerExternalServiceBytes), &obj); err != nil { - return fmt.Errorf("err when decoding api-server-external service in virtual cluster: %w", err) + var svc corev1.Service + if err := yaml.Unmarshal([]byte(apiServerExternalServiceBytes), &svc); err != nil { + return fmt.Errorf("err when decoding api-server-external-service in virtual cluster: %w", err) } - err = util.CreateObject(dynamicClient, "default", "api-server-external-service", &obj) + // Try to create the service + _, err = kubeClient.CoreV1().Services(constants.DefaultNs).Create(context.TODO(), &svc, metav1.CreateOptions{}) if err != nil { - return fmt.Errorf("error when creating api-server-external service in virtual cluster err: %w", err) + if !apierrors.IsAlreadyExists(err) { + return fmt.Errorf("failed to create api-server-external-service service: %w", err) + } + + // Service already exists, retrieve it + existingSvc, err := kubeClient.CoreV1().Services(constants.DefaultNs).Get(context.TODO(), constants.ApiServerExternalService, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get existing api-server-external-service service: %w", err) + } + + // Update the existing service + svc.ResourceVersion = existingSvc.ResourceVersion + _, err = kubeClient.CoreV1().Services(constants.DefaultNs).Update(context.TODO(), &svc, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update api-server-external-service service: %w", err) + } + klog.V(4).Info("successfully updated api-server-external-service service") + } else { + klog.V(4).Info("successfully created api-server-external-service service") } + return nil } -func getEndPointPort(dynamicClient dynamic.Interface) (int32, error) { +func getEndPointPort(kubeClient kubernetes.Interface) (int32, error) { klog.V(4).Info("begin to get Endpoints ports...") - endpointsRes := dynamicClient.Resource(schema.GroupVersionResource{ - Group: "", - Version: "v1", - Resource: "endpoints", - }).Namespace(constants.DefaultNs) - - endpointsRaw, err := endpointsRes.Get(context.TODO(), constants.ApiServerExternalService, metav1.GetOptions{}) + endpoints, err := kubeClient.CoreV1().Endpoints(constants.DefaultNs).Get(context.TODO(), constants.ApiServerExternalService, metav1.GetOptions{}) if err != nil { klog.Errorf("get Endpoints failed: %v", err) return 0, err } - subsets, found, err := unstructured.NestedSlice(endpointsRaw.Object, "subsets") - if !found || err != nil { - klog.Errorf("The subsets field was not found or parsing error occurred: %v", err) - return 0, fmt.Errorf("subsets field not found or error parsing it") - } - - if len(subsets) == 0 { + if len(endpoints.Subsets) == 0 { klog.Errorf("subsets is empty") return 0, fmt.Errorf("No subsets found in the endpoints") } - subset := subsets[0].(map[string]interface{}) - ports, found, err := unstructured.NestedSlice(subset, "ports") - if !found || err != nil { - klog.Errorf("ports field not found or parsing error: %v", err) - return 0, fmt.Errorf("ports field not found or error parsing it") - } - - if len(ports) == 0 { + subset := endpoints.Subsets[0] + if len(subset.Ports) == 0 { klog.Errorf("Port not found in the endpoint") return 0, fmt.Errorf("No ports found in the endpoint") } - port := ports[0].(map[string]interface{}) - portNum, found, err := unstructured.NestedInt64(port, "port") - if !found || err != nil { - klog.Errorf("ports field not found or parsing error: %v", err) - return 0, fmt.Errorf("port field not found or error parsing it") - } - - klog.V(4).Infof("The port number was successfully obtained: %d", portNum) - return int32(portNum), nil + port := subset.Ports[0].Port + klog.V(4).Infof("The port number was successfully obtained: %d", port) + return port, nil } diff --git a/pkg/kubenest/init.go b/pkg/kubenest/init.go index 4155dedcb..4382c86f4 100644 --- a/pkg/kubenest/init.go +++ b/pkg/kubenest/init.go @@ -35,6 +35,7 @@ type initData struct { virtualClusterDataDir string privateRegistry string externalIP string + externalIps []string hostPort int32 hostPortMap map[string]int32 kubeNestOptions *ko.KubeNestOptions @@ -185,6 +186,7 @@ func newRunData(opt *InitOptions) (*initData, error) { privateRegistry: utils.DefaultImageRepository, CertStore: cert.NewCertStore(), externalIP: opt.virtualCluster.Spec.ExternalIP, + externalIps: opt.virtualCluster.Spec.ExternalIps, hostPort: opt.virtualCluster.Status.Port, hostPortMap: opt.virtualCluster.Status.PortMap, kubeNestOptions: opt.KubeNestOptions, @@ -250,6 +252,8 @@ func (i initData) ExternalIP() string { return i.externalIP } +func (i initData) ExternalIPs() []string { return i.externalIps } + func (i initData) HostPort() int32 { return i.hostPort } diff --git a/pkg/kubenest/tasks/cert.go b/pkg/kubenest/tasks/cert.go index 266170ab5..28379a08a 100644 --- a/pkg/kubenest/tasks/cert.go +++ b/pkg/kubenest/tasks/cert.go @@ -135,6 +135,7 @@ func mutateCertConfig(data InitData, cc *cert.CertConfig) error { ControlplaneAddr: data.ControlplaneAddress(), ClusterIps: data.ServiceClusterIp(), ExternalIP: data.ExternalIP(), + ExternalIPs: data.ExternalIPs(), }, cc) if err != nil { return err diff --git a/pkg/kubenest/tasks/data.go b/pkg/kubenest/tasks/data.go index edf63616f..a1a6e1c57 100644 --- a/pkg/kubenest/tasks/data.go +++ b/pkg/kubenest/tasks/data.go @@ -21,6 +21,7 @@ type InitData interface { DataDir() string VirtualCluster() *v1alpha1.VirtualCluster ExternalIP() string + ExternalIPs() []string HostPort() int32 HostPortMap() map[string]int32 DynamicClient() *dynamic.DynamicClient diff --git a/pkg/kubenest/tasks/endpoint.go b/pkg/kubenest/tasks/endpoint.go index edfa81e6f..4de1231e9 100644 --- a/pkg/kubenest/tasks/endpoint.go +++ b/pkg/kubenest/tasks/endpoint.go @@ -3,10 +3,9 @@ package tasks import ( "context" "fmt" - "github.com/pkg/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" @@ -54,12 +53,13 @@ func runEndPointInVirtualClusterTask(r workflow.RunData) error { if err != nil { return err } - dynamicClient, err := dynamic.NewForConfig(config) + + kubeClient, err := kubernetes.NewForConfig(config) if err != nil { return err } - err = controlplane.EnsureApiServerExternalEndPoint(dynamicClient) + err = controlplane.EnsureApiServerExternalEndPoint(kubeClient) if err != nil { return err } diff --git a/pkg/kubenest/util/cert/certs.go b/pkg/kubenest/util/cert/certs.go index 908d29c4b..dd608c004 100644 --- a/pkg/kubenest/util/cert/certs.go +++ b/pkg/kubenest/util/cert/certs.go @@ -43,6 +43,7 @@ type AltNamesMutatorConfig struct { ControlplaneAddr string ClusterIps []string ExternalIP string + ExternalIPs []string } func (config *CertConfig) defaultPublicKeyAlgorithm() { @@ -232,6 +233,13 @@ func proxyServerAltNamesMutator(cfg *AltNamesMutatorConfig) (*certutil.AltNames, if len(cfg.ExternalIP) > 0 { appendSANsToAltNames(altNames, []string{cfg.ExternalIP}) } + + if len(cfg.ExternalIPs) > 0 { + for _, externalIp := range cfg.ExternalIPs { + appendSANsToAltNames(altNames, []string{externalIp}) + } + } + if len(cfg.ClusterIps) > 0 { for _, clusterIp := range cfg.ClusterIps { appendSANsToAltNames(altNames, []string{clusterIp}) @@ -273,6 +281,13 @@ func apiServerAltNamesMutator(cfg *AltNamesMutatorConfig) (*certutil.AltNames, e if len(cfg.ExternalIP) > 0 { appendSANsToAltNames(altNames, []string{cfg.ExternalIP}) } + + if len(cfg.ExternalIPs) > 0 { + for _, externalIp := range cfg.ExternalIPs { + appendSANsToAltNames(altNames, []string{externalIp}) + } + } + if len(cfg.ClusterIps) > 0 { for _, clusterIp := range cfg.ClusterIps { appendSANsToAltNames(altNames, []string{clusterIp})