| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| package computegroups | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "github.com/apache/doris-operator/pkg/common/utils/runtimeconfig" | |
| "regexp" | |
| "strconv" | |
| "strings" | |
| "sync" | |
| dv1 "github.com/apache/doris-operator/api/disaggregated/v1" | |
| "github.com/apache/doris-operator/pkg/common/utils" | |
| "github.com/apache/doris-operator/pkg/common/utils/k8s" | |
| "github.com/apache/doris-operator/pkg/common/utils/mysql" | |
| "github.com/apache/doris-operator/pkg/common/utils/resource" | |
| "github.com/apache/doris-operator/pkg/common/utils/set" | |
| sc "github.com/apache/doris-operator/pkg/controller/sub_controller" | |
| appv1 "k8s.io/api/apps/v1" | |
| corev1 "k8s.io/api/core/v1" | |
| "k8s.io/apimachinery/pkg/api/equality" | |
| apierrors "k8s.io/apimachinery/pkg/api/errors" | |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | |
| "k8s.io/apimachinery/pkg/types" | |
| "k8s.io/client-go/rest" | |
| "k8s.io/klog/v2" | |
| ctrl "sigs.k8s.io/controller-runtime" | |
| "sigs.k8s.io/controller-runtime/pkg/client" | |
| ) | |
| var _ sc.DisaggregatedSubController = &DisaggregatedComputeGroupsController{} | |
| var ( | |
| disaggregatedComputeGroupsController = "disaggregatedComputeGroupsController" | |
| ) | |
| type DisaggregatedComputeGroupsController struct { | |
| sc.DisaggregatedSubDefaultController | |
| RestConfig *rest.Config | |
| } | |
| func New(mgr ctrl.Manager) *DisaggregatedComputeGroupsController { | |
| return &DisaggregatedComputeGroupsController{ | |
| DisaggregatedSubDefaultController: sc.DisaggregatedSubDefaultController{ | |
| K8sclient: mgr.GetClient(), | |
| K8srecorder: mgr.GetEventRecorderFor(disaggregatedComputeGroupsController), | |
| ControllerName: disaggregatedComputeGroupsController, | |
| }, | |
| RestConfig: mgr.GetConfig(), | |
| } | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) Sync(ctx context.Context, obj client.Object) error { | |
| ddc := obj.(*dv1.DorisDisaggregatedCluster) | |
| if len(ddc.Spec.ComputeGroups) == 0 { | |
| klog.Errorf("disaggregatedComputeGroupsController sync disaggregatedDorisCluster namespace=%s,name=%s have not compute group spec.", ddc.Namespace, ddc.Name) | |
| dcgs.K8srecorder.Event(ddc, string(sc.EventWarning), string(sc.ComputeGroupsEmpty), "compute group empty, the cluster will not work normal.") | |
| return nil | |
| } | |
| if !dcgs.feAvailable(ddc) { | |
| dcgs.K8srecorder.Event(ddc, string(sc.EventNormal), string(sc.WaitFEAvailable), "fe have not ready.") | |
| return nil | |
| } | |
| // validating compute group information. | |
| if event, res := dcgs.validateComputeGroup(ddc.Spec.ComputeGroups); !res { | |
| klog.Errorf("disaggregatedComputeGroupsController namespace=%s name=%s validateComputeGroup have not match specifications %s.", ddc.Namespace, ddc.Name, sc.EventString(event)) | |
| dcgs.K8srecorder.Eventf(ddc, string(event.Type), string(event.Reason), event.Message) | |
| return errors.New("validating compute group failed") | |
| } | |
| var errs []error | |
| cgs := ddc.Spec.ComputeGroups | |
| for i, _ := range cgs { | |
| if event, err := dcgs.computeGroupSync(ctx, ddc, &cgs[i]); err != nil { | |
| if event != nil { | |
| dcgs.K8srecorder.Event(ddc, string(event.Type), string(event.Reason), event.Message) | |
| } | |
| errs = append(errs, err) | |
| klog.Errorf("disaggregatedComputeGroupsController computeGroups sync failed, compute group Uniqueid %s sync failed, err=%s", cgs[i].UniqueId, err.Error()) | |
| } | |
| } | |
| if len(errs) != 0 { | |
| msg := fmt.Sprintf("disaggregatedComputeGroupsController sync namespace: %s ,ddc name: %s, compute group has the following error: ", ddc.Namespace, ddc.Name) | |
| for _, err := range errs { | |
| msg += err.Error() | |
| } | |
| return errors.New(msg) | |
| } | |
| return nil | |
| } | |
| // validate compute group config information. | |
| func (dcgs *DisaggregatedComputeGroupsController) validateComputeGroup(cgs []dv1.ComputeGroup) (*sc.Event, bool) { | |
| dupl := dcgs.validateDuplicated(cgs) | |
| if dupl != "" { | |
| klog.Errorf("disaggregatedComputeGroupsController validateComputeGroup validate Duplicated have duplicate unique identifier %s.", dupl) | |
| return &sc.Event{Type: sc.EventWarning, Reason: sc.CGUniqueIdentifierDuplicate, Message: "unique identifier " + dupl + " duplicate in compute groups."}, false | |
| } | |
| if reg, res := dcgs.validateRegex(cgs); !res { | |
| klog.Errorf("disaggregatedComputeGroupsController validateComputeGroup validateRegex %s have not match regular expression", reg) | |
| return &sc.Event{Type: sc.EventWarning, Reason: sc.CGUniqueIdentifierNotMatchRegex, Message: reg}, false | |
| } | |
| return nil, true | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) feAvailable(ddc *dv1.DorisDisaggregatedCluster) bool { | |
| //if fe deploy in k8s, should wait fe available | |
| //1. wait for fe ok. | |
| endpoints := corev1.Endpoints{} | |
| if err := dcgs.K8sclient.Get(context.Background(), types.NamespacedName{Namespace: ddc.Namespace, Name: ddc.GetFEServiceName()}, &endpoints); err != nil { | |
| klog.Infof("disaggregatedComputeGroupsController Sync wait fe service name %s available occur failed %s\n", ddc.GetFEServiceName(), err.Error()) | |
| return false | |
| } | |
| for _, sub := range endpoints.Subsets { | |
| if len(sub.Addresses) > 0 { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) computeGroupSync(ctx context.Context, ddc *dv1.DorisDisaggregatedCluster, cg *dv1.ComputeGroup) (*sc.Event, error) { | |
| if cg.Replicas == nil { | |
| cg.Replicas = resource.GetInt32Pointer(1) | |
| } | |
| cvs := dcgs.GetConfigValuesFromConfigMaps(ddc.Namespace, resource.BE_RESOLVEKEY, cg.CommonSpec.ConfigMaps) | |
| st := dcgs.NewStatefulset(ddc, cg, cvs) | |
| if err := runtimeconfig.Prepare(ctx, dcgs.K8sclient, dcgs.K8srecorder, ddc, st, cvs); err != nil { | |
| return nil, err | |
| } | |
| internalSvc := dcgs.newInternalService(ddc, cg, cvs) | |
| externalSvc := dcgs.newExternalService(ddc, cg, cvs) | |
| dcgs.initialCGStatus(ddc, cg) | |
| dcgs.CheckSecretMountPath(ddc, cg.Secrets) | |
| dcgs.CheckSecretExist(ctx, ddc, cg.Secrets) | |
| // Reconcile internal headless service. | |
| // During upgrade from older versions, the existing service may not be headless (has a ClusterIP assigned). | |
| // Since K8s does not allow changing spec.clusterIP on an existing service, we must delete and recreate it. | |
| if err := dcgs.reconcileInternalService(ctx, internalSvc); err != nil { | |
| klog.Errorf("disaggregatedComputeGroupsController reconcile internal service namespace %s name %s failed, err=%s", internalSvc.Namespace, internalSvc.Name, err.Error()) | |
| return &sc.Event{Type: sc.EventWarning, Reason: sc.ServiceApplyedFailed, Message: err.Error()}, err | |
| } | |
| // Reconcile external service for load-balanced access. | |
| event, err := dcgs.DefaultReconcileService(ctx, externalSvc) | |
| if err != nil { | |
| klog.Errorf("disaggregatedComputeGroupsController reconcile external service namespace %s name %s failed, err=%s", externalSvc.Namespace, externalSvc.Name, err.Error()) | |
| return event, err | |
| } | |
| event, err = dcgs.reconcileStatefulset(ctx, st, ddc, cg) | |
| if err != nil { | |
| klog.Errorf("disaggregatedComputeGroupsController reconcile statefulset namespace %s name %s failed, err=%s", st.Namespace, st.Name, err.Error()) | |
| return event, err | |
| } | |
| event, err = dcgs.ReconcilePVC(ctx, ddc, cvs, dv1.DisaggregatedBE, st, cg) | |
| if err != nil { | |
| klog.Errorf("computeGroupSync ReconcilePVC failed, namespace: %s, ddc name %s, cgName: %s, error=%s!", ddc.Namespace, ddc.Name, cg.UniqueId, err.Error()) | |
| } | |
| return event, err | |
| } | |
| // reconcileInternalService reconciles the internal headless service for a compute group. | |
| // If the existing service is not headless (upgrade scenario), it deletes and recreates it, | |
| // because K8s does not allow mutating spec.clusterIP from a valid IP to "None". | |
| func (dcgs *DisaggregatedComputeGroupsController) reconcileInternalService(ctx context.Context, svc *corev1.Service) error { | |
| var existingSvc corev1.Service | |
| err := dcgs.K8sclient.Get(ctx, types.NamespacedName{Namespace: svc.Namespace, Name: svc.Name}, &existingSvc) | |
| if err != nil { | |
| if apierrors.IsNotFound(err) { | |
| return k8s.CreateClientObject(ctx, dcgs.K8sclient, svc) | |
| } | |
| return err | |
| } | |
| // If the existing service is already headless, use normal reconcile path. | |
| if existingSvc.Spec.ClusterIP == "None" { | |
| _, err = dcgs.DefaultReconcileService(ctx, svc) | |
| return err | |
| } | |
| // Existing service is not headless — delete and recreate. | |
| klog.Infof("reconcileInternalService existing service %s/%s is not headless (clusterIP=%s), deleting and recreating as headless.", | |
| existingSvc.Namespace, existingSvc.Name, existingSvc.Spec.ClusterIP) | |
| if err = k8s.DeleteService(ctx, dcgs.K8sclient, svc.Namespace, svc.Name); err != nil { | |
| return err | |
| } | |
| return k8s.CreateClientObject(ctx, dcgs.K8sclient, svc) | |
| } | |
| // reconcileStatefulset return bool means reconcile print error message. | |
| func (dcgs *DisaggregatedComputeGroupsController) reconcileStatefulset(ctx context.Context, st *appv1.StatefulSet, cluster *dv1.DorisDisaggregatedCluster, cg *dv1.ComputeGroup) (*sc.Event, error) { | |
| //use new default value before apply new statefulset, when creating and apply spec change. | |
| ndf := func(st *appv1.StatefulSet, est *appv1.StatefulSet) { | |
| dcgs.useNewDefaultValuesInStatefulset(st) | |
| } | |
| var est appv1.StatefulSet | |
| if err := dcgs.K8sclient.Get(ctx, types.NamespacedName{Namespace: st.Namespace, Name: st.Name}, &est); apierrors.IsNotFound(err) { | |
| //if err = k8s.CreateClientObject(ctx, dcgs.K8sclient, st); err != nil { | |
| // klog.Errorf("disaggregatedComputeGroupsController reconcileStatefulset create statefulset namespace=%s name=%s failed, err=%s", st.Namespace, st.Name, err.Error()) | |
| // return &sc.Event{Type: sc.EventWarning, Reason: sc.CGCreateResourceFailed, Message: err.Error()}, err | |
| //} | |
| //use apply replace create, if use create the default image not replace with be image and annotation for equal not assign. | |
| if err = k8s.ApplyStatefulSet(ctx, dcgs.K8sclient, st, func(new, est *appv1.StatefulSet) bool { | |
| dcgs.RestrictConditionsEqual(new, est) | |
| //creating use the function to assign equal annotation. | |
| return resource.StatefulsetDeepEqualWithKey(new, est, dv1.DisaggregatedSpecHashValueAnnotation, false) | |
| }, ndf); err != nil { | |
| klog.Errorf("disaggregatedComputeGroupsController reconcileStatefulset create statefulset namespace=%s name=%s failed, err=%s", st.Namespace, st.Name, err.Error()) | |
| return &sc.Event{Type: sc.EventWarning, Reason: sc.CGCreateResourceFailed, Message: err.Error()}, err | |
| } | |
| return nil, nil | |
| } else if err != nil { | |
| klog.Errorf("disaggregatedComputeGroupsController reconcileStatefulset get statefulset failed, namespace=%s name=%s failed, err=%s", st.Namespace, st.Name, err.Error()) | |
| return nil, err | |
| } | |
| if !volumeClaimTemplatesEqual(st.Spec.VolumeClaimTemplates, est.Spec.VolumeClaimTemplates) { | |
| msg := fmt.Sprintf("compute group %s storage template is immutable after creation; modifying BE file_cache_path or persistent volume settings requires recreating the compute group", cg.UniqueId) | |
| klog.Errorf("disaggregatedComputeGroupsController reconcileStatefulset immutable storage template changed, namespace=%s name=%s, err=%s", st.Namespace, st.Name, msg) | |
| return &sc.Event{Type: sc.EventWarning, Reason: sc.CGStorageTemplateImmutable, Message: msg}, errors.New(msg) | |
| } | |
| // Finish an existing graceful action before starting SQL decommission. Keep | |
| // replicas unchanged while that action owns the pod lifecycle. | |
| if cluster.Spec.EnableDecommission && hasGracefulAction(&est) { | |
| replicas := *est.Spec.Replicas | |
| st.Spec.Replicas = &replicas | |
| } | |
| if pending, err := dcgs.reconcileDecommissionScaleDown(ctx, st, &est, cluster, cg); pending || err != nil { | |
| if err != nil { | |
| return &sc.Event{Type: sc.EventWarning, Reason: sc.CGSqlExecFailed, Message: err.Error()}, err | |
| } | |
| return nil, nil | |
| } | |
| // Direct-drop scale-down is owned by the graceful state machine when the image | |
| // supports it. SQL decommission is handled above before applying resources. | |
| gracefulInProgress := false | |
| if !cluster.Spec.EnableDecommission { | |
| gracefulInProgress = dcgs.reconcileGracefulStatefulSet(ctx, st, &est, cluster, cg) | |
| } | |
| if !gracefulInProgress { | |
| err := dcgs.preApplyStatefulSet(ctx, st, &est, cluster, cg) | |
| if err != nil { | |
| klog.Errorf("disaggregatedComputeGroupsController reconcileStatefulset preApplyStatefulSet namespace=%s name=%s failed, err=%s", st.Namespace, st.Name, err.Error()) | |
| return &sc.Event{Type: sc.EventWarning, Reason: sc.CGSqlExecFailed, Message: err.Error()}, err | |
| } | |
| // be decommission processing, skip apply statefulset. | |
| if skipApplyStatefulset(cluster, cg) { | |
| return nil, nil | |
| } | |
| } | |
| if cluster.Spec.EnableDecommission { | |
| dcgs.reconcileGracefulStatefulSet(ctx, st, &est, cluster, cg) | |
| } | |
| if st.Spec.UpdateStrategy.Type == appv1.OnDeleteStatefulSetStrategyType { | |
| dcgs.clearStatefulSetRollingUpdate(ctx, st.Namespace, st.Name) | |
| } | |
| if err := k8s.ApplyStatefulSet(ctx, dcgs.K8sclient, st, func(new, est *appv1.StatefulSet) bool { | |
| dcgs.RestrictConditionsEqual(new, est) | |
| //store annotations "doris.disaggregated.cluster/generation={generation}" on statefulset | |
| //store annotations "doris.disaggregated.cluster/update-{uniqueid}=true/false" on DorisDisaggregatedCluster | |
| businessEqual := dcgs.businessStatefulSetEqual(new, est) | |
| if !businessEqual { | |
| if len(new.Annotations) == 0 { | |
| new.Annotations = map[string]string{} | |
| } | |
| st_annos := (resource.Annotations)(new.Annotations) | |
| st_annos.Add(dv1.UpdateStatefulsetGeneration, strconv.FormatInt(cluster.Generation, 10)) | |
| if len(cluster.Annotations) == 0 { | |
| cluster.Annotations = map[string]string{} | |
| } | |
| ddc_annos := (resource.Annotations)(cluster.Annotations) | |
| msUniqueIdKey := strings.ToLower(fmt.Sprintf(dv1.UpdateStatefulsetName, cluster.GetCGStatefulsetName(cg))) | |
| ddc_annos.Add(msUniqueIdKey, "true") | |
| } | |
| return businessEqual && gracefulStatefulSetControlEqual(new, est) | |
| }, ndf); err != nil { | |
| klog.Errorf("disaggregatedComputeGroupsController reconcileStatefulset apply statefulset namespace=%s name=%s failed, err=%s", st.Namespace, st.Name, err.Error()) | |
| return &sc.Event{Type: sc.EventWarning, Reason: sc.CGApplyResourceFailed, Message: err.Error()}, err | |
| } | |
| return nil, nil | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) reconcileGracefulStatefulSet( | |
| ctx context.Context, | |
| st, est *appv1.StatefulSet, | |
| cluster *dv1.DorisDisaggregatedCluster, | |
| cg *dv1.ComputeGroup, | |
| ) bool { | |
| if dcgs.RestConfig == nil { | |
| return false | |
| } | |
| var cgStatus *dv1.ComputeGroupStatus | |
| for i := range cluster.Status.ComputeGroupStatuses { | |
| if cluster.Status.ComputeGroupStatuses[i].UniqueId == cg.UniqueId { | |
| cgStatus = &cluster.Status.ComputeGroupStatuses[i] | |
| break | |
| } | |
| } | |
| if cgStatus == nil { | |
| return false | |
| } | |
| skipApply, err := dcgs.gracefulRolloutReconcile(ctx, dcgs.RestConfig, st, est, cluster, cg, cgStatus) | |
| if err != nil { | |
| klog.Errorf("reconcileStatefulset gracefulRolloutReconcile failed: %v", err) | |
| } | |
| if skipApply { | |
| // Do not fall through to the legacy DropBE workflow while the graceful | |
| // state machine owns this StatefulSet. | |
| ensureOnDeleteStrategy(st) | |
| } | |
| return skipApply | |
| } | |
| func volumeClaimTemplatesEqual(new, old []corev1.PersistentVolumeClaim) bool { | |
| if len(new) != len(old) { | |
| return false | |
| } | |
| normalizedNew := make([]corev1.PersistentVolumeClaim, len(new)) | |
| normalizedOld := make([]corev1.PersistentVolumeClaim, len(old)) | |
| for i := range new { | |
| normalizedNew[i] = normalizeVolumeClaimTemplate(new[i]) | |
| normalizedOld[i] = normalizeVolumeClaimTemplate(old[i]) | |
| } | |
| return equality.Semantic.DeepEqual(normalizedNew, normalizedOld) | |
| } | |
| func normalizeVolumeClaimTemplate(pvc corev1.PersistentVolumeClaim) corev1.PersistentVolumeClaim { | |
| pvc.TypeMeta = metav1.TypeMeta{} | |
| pvc.ObjectMeta = metav1.ObjectMeta{ | |
| Name: pvc.Name, | |
| Labels: normalizeStringMap(pvc.Labels), | |
| Annotations: normalizeStringMap(pvc.Annotations), | |
| } | |
| pvc.Status = corev1.PersistentVolumeClaimStatus{} | |
| if pvc.Spec.VolumeMode == nil { | |
| volumeMode := corev1.PersistentVolumeFilesystem | |
| pvc.Spec.VolumeMode = &volumeMode | |
| } | |
| return pvc | |
| } | |
| func normalizeStringMap(m map[string]string) map[string]string { | |
| if len(m) == 0 { | |
| return nil | |
| } | |
| normalized := make(map[string]string, len(m)) | |
| for k, v := range m { | |
| normalized[k] = v | |
| } | |
| return normalized | |
| } | |
| // initial compute group status before sync resources. status changing with sync steps, and generate the last status by classify pods. | |
| func (dcgs *DisaggregatedComputeGroupsController) initialCGStatus(ddc *dv1.DorisDisaggregatedCluster, cg *dv1.ComputeGroup) { | |
| cgss := ddc.Status.ComputeGroupStatuses | |
| //clusterId := ddc.GetCGId(cg) | |
| uniqueId := cg.UniqueId | |
| defaultStatus := dv1.ComputeGroupStatus{ | |
| Phase: dv1.Reconciling, | |
| UniqueId: cg.UniqueId, | |
| StatefulsetName: ddc.GetCGStatefulsetName(cg), | |
| ServiceName: ddc.GetCGServiceName(cg), | |
| //set for status updated. | |
| Replicas: *cg.Replicas, | |
| } | |
| for i := range cgss { | |
| if cgss[i].UniqueId == uniqueId { | |
| cgss[i].Replicas = *cg.Replicas | |
| return | |
| } | |
| } | |
| // Need to adjust by pointer | |
| (&ddc.Status).ComputeGroupStatuses = append((&ddc.Status).ComputeGroupStatuses, defaultStatus) | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) businessStatefulSetEqual(new, est *appv1.StatefulSet) bool { | |
| nst := new.DeepCopy() | |
| eSt := est.DeepCopy() | |
| normalizeGracefulStatefulSetForCompare(nst) | |
| normalizeGracefulStatefulSetForCompare(eSt) | |
| equal := resource.StatefulsetDeepEqualWithKey(nst, eSt, dv1.DisaggregatedSpecHashValueAnnotation, false) | |
| if hashValue := nst.Annotations[dv1.DisaggregatedSpecHashValueAnnotation]; hashValue != "" { | |
| if new.Annotations == nil { | |
| new.Annotations = map[string]string{} | |
| } | |
| new.Annotations[dv1.DisaggregatedSpecHashValueAnnotation] = hashValue | |
| } | |
| return equal | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) clearStatefulSetRollingUpdate(ctx context.Context, namespace, name string) { | |
| patch := []byte(`{"spec":{"updateStrategy":{"rollingUpdate":null}}}`) | |
| st := &appv1.StatefulSet{} | |
| st.Namespace = namespace | |
| st.Name = name | |
| if err := dcgs.K8sclient.Patch(ctx, st, client.RawPatch(types.MergePatchType, patch)); err != nil { | |
| klog.Errorf("clearStatefulSetRollingUpdate: failed to clear rollingUpdate for StatefulSet %s/%s: %v", namespace, name, err) | |
| } | |
| } | |
| // check compute groups unique identifier duplicated or not. return duplicated key. | |
| func (dcgs *DisaggregatedComputeGroupsController) validateDuplicated(cgs []dv1.ComputeGroup) string { | |
| dupl := "" | |
| uniqueIds := set.NewSetString() | |
| for _, cg := range cgs { | |
| if uniqueIds.Find(cg.UniqueId) { | |
| dupl = dupl + cg.UniqueId + ";" | |
| } | |
| uniqueIds.Add(cg.UniqueId) | |
| } | |
| return dupl | |
| } | |
| // checking the cg name compliant with regular expression or not. | |
| func (dcgs *DisaggregatedComputeGroupsController) validateRegex(cgs []dv1.ComputeGroup) (string, bool) { | |
| var regStr = "" | |
| for _, cg := range cgs { | |
| res, err := regexp.Match(compute_group_name_regex, []byte(cg.UniqueId)) | |
| if !res { | |
| regStr = regStr + cg.UniqueId + " not match " + compute_group_name_regex | |
| } | |
| //for debugging, output the error in log | |
| if err != nil { | |
| klog.Errorf("disaggregatedComputeGroupsController validateRegex compute group name %s failed, err=%s", cg.UniqueId, err.Error()) | |
| } | |
| } | |
| if regStr != "" { | |
| return regStr, false | |
| } | |
| return "", true | |
| } | |
| // clear not configed cg resources, delete not configed cg status from ddc.status . | |
| func (dcgs *DisaggregatedComputeGroupsController) ClearResources(ctx context.Context, obj client.Object) (bool, error) { | |
| ddc := obj.(*dv1.DorisDisaggregatedCluster) | |
| var eCGs []dv1.ComputeGroupStatus | |
| var delComputeGroupIds []string | |
| for i, cgs := range ddc.Status.ComputeGroupStatuses { | |
| exist := false | |
| for _, cg := range ddc.Spec.ComputeGroups { | |
| if cgs.UniqueId == cg.UniqueId { | |
| eCGs = append(eCGs, ddc.Status.ComputeGroupStatuses[i]) | |
| exist = true | |
| break | |
| } | |
| } | |
| if !exist { | |
| delComputeGroupIds = append(delComputeGroupIds, cgs.ComputeGroupId) | |
| } | |
| } | |
| //list the svcs and stss owner reference to dorisDisaggregatedCluster. | |
| cls := dcgs.GetCG2LayerCommonSchedulerLabels(ddc.Name) | |
| svcs, err := k8s.ListServicesInNamespace(ctx, dcgs.K8sclient, ddc.Namespace, cls) | |
| if err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController ListServicesInNamespace failed, dorisdisaggregatedcluster name=%s", ddc.Name) | |
| return false, err | |
| } | |
| stss, err := k8s.ListStatefulsetInNamespace(ctx, dcgs.K8sclient, ddc.Namespace, cls) | |
| if err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController ListStatefulsetInNamespace failed, dorisdisaggregatedcluster name=%s", ddc.Name) | |
| return false, err | |
| } | |
| //clear unused service and statefulset. | |
| delSvcNames := dcgs.findUnusedSvcs(svcs, ddc) | |
| delStsNames, delUniqueIds := dcgs.findUnusedStssAndUniqueIds(stss, ddc) | |
| if err = dcgs.clearCGInDorisMeta(ctx, delComputeGroupIds, ddc); err != nil { | |
| return false, err | |
| } | |
| if err = dcgs.clearSvcs(ctx, delSvcNames, ddc); err != nil { | |
| return false, err | |
| } | |
| if err = dcgs.clearStatefulsets(ctx, delStsNames, ddc); err != nil { | |
| return false, err | |
| } | |
| //clear unused pvc | |
| for i := range eCGs { | |
| err = dcgs.ClearStatefulsetUnusedPVCs(ctx, ddc, eCGs[i]) | |
| if err != nil { | |
| klog.Errorf("disaggregatedComputeGroupsController ClearStatefulsetUnusedPVCs clear ComputeGroup reduced replicas PVC failed, namespace=%s, ddc name=%s, uniqueId=%s err=%s", ddc.Namespace, ddc.Name, eCGs[i].UniqueId, err.Error()) | |
| } | |
| } | |
| for _, uniqueId := range delUniqueIds { | |
| //new fake computeGroup status for clear all pvcs owner reference to deleted compute group. | |
| fakeCgs := dv1.ComputeGroupStatus{ | |
| UniqueId: uniqueId, | |
| } | |
| err = dcgs.ClearStatefulsetUnusedPVCs(ctx, ddc, fakeCgs) | |
| if err != nil { | |
| klog.Errorf("disaggregatedComputeGroupsController ClearStatefulsetUnusedPVCs clear deleted compute group failed, namespace=%s, ddc name=%s, uniqueId=%s err=%s", ddc.Namespace, ddc.Name, uniqueId, err.Error()) | |
| } | |
| } | |
| ddc.Status.ComputeGroupStatuses = eCGs | |
| return true, nil | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) clearStatefulsets(ctx context.Context, stsNames []string, ddc *dv1.DorisDisaggregatedCluster) error { | |
| for _, name := range stsNames { | |
| if err := k8s.DeleteStatefulset(ctx, dcgs.K8sclient, ddc.Namespace, name); err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController clear statefulset failed, namespace=%s, name =%s, err=%s", ddc.Namespace, name, err.Error()) | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) clearSvcs(ctx context.Context, svcNames []string, ddc *dv1.DorisDisaggregatedCluster) error { | |
| for _, name := range svcNames { | |
| if err := k8s.DeleteService(ctx, dcgs.K8sclient, ddc.Namespace, name); err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController clear service failed, namespace=%s, name =%s, err=%s", ddc.Namespace, name, err.Error()) | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) clearCGInDorisMeta(ctx context.Context, cgids []string, ddc *dv1.DorisDisaggregatedCluster) error { | |
| if len(cgids) == 0 { | |
| return nil | |
| } | |
| sqlClient, err := dcgs.getMasterSqlClient(ctx, ddc) | |
| if err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController clearCGInDorisMeta dropCGBySQLClient getMasterSqlClient failed: %s", err.Error()) | |
| dcgs.K8srecorder.Event(ddc, string(sc.EventWarning), string(sc.CGSqlExecFailed), "computeGroupSync dropCGBySQLClient failed: "+err.Error()) | |
| return err | |
| } | |
| defer sqlClient.Close() | |
| for _, cgid := range cgids { | |
| //clear cg, the keepAmount = 0 | |
| err = dcgs.scaledOutBENodesByDrop(sqlClient, cgid, 0) | |
| if err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController clearCGInDorisMeta dropCGBySQLClient failed: %s", err.Error()) | |
| dcgs.K8srecorder.Event(ddc, string(sc.EventWarning), string(sc.CGSqlExecFailed), "computeGroupSync dropCGBySQLClient failed: "+err.Error()) | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) findUnusedSvcs(svcs []corev1.Service, ddc *dv1.DorisDisaggregatedCluster) []string { | |
| var unusedSvcNames []string | |
| for i, _ := range svcs { | |
| own := ownerReference2ddc(&svcs[i], ddc) | |
| if !own { | |
| //not owner reference to ddc, should skip the service. | |
| continue | |
| } | |
| svcUniqueId := getUniqueIdFromClientObject(&svcs[i]) | |
| exist := false | |
| for j := 0; j < len(ddc.Spec.ComputeGroups); j++ { | |
| if ddc.Spec.ComputeGroups[j].UniqueId == svcUniqueId { | |
| exist = true | |
| break | |
| } | |
| } | |
| if !exist { | |
| unusedSvcNames = append(unusedSvcNames, svcs[i].Name) | |
| } | |
| } | |
| return unusedSvcNames | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) findUnusedStssAndUniqueIds(stss []appv1.StatefulSet, ddc *dv1.DorisDisaggregatedCluster) ([]string /*sts*/, []string /*uniqueIds*/) { | |
| var unusedStsNames []string | |
| var unusedUniqueIds []string | |
| for i, _ := range stss { | |
| own := ownerReference2ddc(&stss[i], ddc) | |
| if !own { | |
| //not owner reference tto ddc should skip the statefulset. | |
| continue | |
| } | |
| stsUniqueId := getUniqueIdFromClientObject(&stss[i]) | |
| exist := false | |
| for j := 0; j < len(ddc.Spec.ComputeGroups); j++ { | |
| if ddc.Spec.ComputeGroups[j].UniqueId == stsUniqueId { | |
| exist = true | |
| break | |
| } | |
| } | |
| if !exist { | |
| unusedStsNames = append(unusedStsNames, stss[i].Name) | |
| unusedUniqueIds = append(unusedUniqueIds, stsUniqueId) | |
| } | |
| } | |
| return unusedStsNames, unusedUniqueIds | |
| } | |
| // ClearStatefulsetUnusedPVCs | |
| // 1.delete unused pvc skip cluster is Suspend | |
| // 2.delete unused pvc for statefulset | |
| // 3.delete pvc if not used by any statefulset | |
| func (dcgs *DisaggregatedComputeGroupsController) ClearStatefulsetUnusedPVCs(ctx context.Context, ddc *dv1.DorisDisaggregatedCluster, cgs dv1.ComputeGroupStatus) error { | |
| var cg *dv1.ComputeGroup | |
| for i := range ddc.Spec.ComputeGroups { | |
| /* uniqueId := ddc.GetCGId(&ddc.Spec.ComputeGroups[i]) | |
| if clusterId == cgs.ClusterId { | |
| cg = &ddc.Spec.ComputeGroups[i] | |
| }*/ | |
| if ddc.Spec.ComputeGroups[i].UniqueId == cgs.UniqueId { | |
| cg = &ddc.Spec.ComputeGroups[i] | |
| } | |
| } | |
| currentPVCs := corev1.PersistentVolumeClaimList{} | |
| //pvcMap := make(map[string]*corev1.PersistentVolumeClaim) | |
| pvcLabels := dcgs.newCGPodsSelector(ddc.Name, cgs.UniqueId) | |
| if err := dcgs.K8sclient.List(ctx, ¤tPVCs, client.InNamespace(ddc.Namespace), client.MatchingLabels(pvcLabels)); err != nil { | |
| dcgs.K8srecorder.Event(ddc, string(sc.EventWarning), sc.PVCListFailed, fmt.Sprintf("DisaggregatedComputeGroupsController ClearStatefulsetUnusedPVCs list pvc failed:%s!", err.Error())) | |
| return err | |
| } | |
| // now only clear scale down pod. | |
| if cg == nil { | |
| return nil | |
| } | |
| var clearPVC []string | |
| //we should use statefulset replicas for avoiding the phase=scaleDown, when phase `scaleDown` cg' replicas is less than statefuslet. | |
| stsName := ddc.GetCGStatefulsetName(cg) | |
| sts, err := k8s.GetStatefulSet(ctx, dcgs.K8sclient, ddc.Namespace, stsName) | |
| if err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController ClearStatefulsetUnusedPVCs get statefulset namespace=%s, name=%s, failed, err=%s", ddc.Namespace, stsName, err.Error()) | |
| //waiting next reconciling. | |
| return nil | |
| } | |
| replicas := *sts.Spec.Replicas | |
| for _, pvc := range currentPVCs.Items { | |
| pvcName := pvc.Name | |
| sl := strings.Split(pvcName, stsName+"-") | |
| if len(sl) != 2 { | |
| klog.Errorf("DisaggregatedComputeGroupsController ClearStatefulsetUnusedPVCs namespace %s name %s not format pvc name format.", ddc.Namespace, pvcName) | |
| continue | |
| } | |
| var index int64 | |
| var perr error | |
| index, perr = strconv.ParseInt(sl[1], 10, 32) | |
| if perr != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController ClearStatefulsetUnusedPVCs namespace %s name %s index parse failed, err=%s", ddc.Namespace, pvcName, perr.Error()) | |
| continue | |
| } | |
| if int32(index) >= replicas { | |
| retain, err := runtimeconfig.RetainPVCOnScaleDown(sts, pvcName) | |
| if err != nil { | |
| return err | |
| } | |
| if retain { | |
| continue | |
| } | |
| clearPVC = append(clearPVC, pvcName) | |
| } | |
| } | |
| var mergeError error | |
| for _, pvcName := range clearPVC { | |
| if err = k8s.DeletePVC(ctx, dcgs.K8sclient, ddc.Namespace, pvcName, pvcLabels); err != nil { | |
| dcgs.K8srecorder.Event(ddc, string(sc.EventWarning), sc.PVCDeleteFailed, err.Error()) | |
| klog.Errorf("ClearStatefulsetUnusedPVCs deletePVCs failed: namespace %s, name %s delete pvc %s, err: %s .", ddc.Namespace, pvcName, pvcName, err.Error()) | |
| mergeError = utils.MergeError(mergeError, err) | |
| } | |
| } | |
| return mergeError | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) GetControllerName() string { | |
| return dcgs.ControllerName | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) UpdateComponentStatus(obj client.Object) error { | |
| ddc := obj.(*dv1.DorisDisaggregatedCluster) | |
| cgss := ddc.Status.ComputeGroupStatuses | |
| if len(cgss) == 0 { | |
| klog.Errorf("disaggregatedComputeGroupsController updateComponentStatus compute group status is empty!") | |
| return nil | |
| } | |
| errChan := make(chan error, len(cgss)) | |
| wg := sync.WaitGroup{} | |
| wg.Add(len(cgss)) | |
| for i, _ := range cgss { | |
| go func(idx int) { | |
| defer wg.Done() | |
| errChan <- dcgs.updateCGStatus(ddc, &cgss[idx]) | |
| }(i) | |
| } | |
| wg.Wait() | |
| close(errChan) | |
| errMs := "" | |
| for err := range errChan { | |
| if err != nil { | |
| errMs += err.Error() | |
| } | |
| } | |
| for _, cgs := range ddc.Status.ComputeGroupStatuses { | |
| if cgs.ComputeGroupId == "" { | |
| dcgs.recordComputeGroupIds(ddc) | |
| break | |
| } | |
| } | |
| var fullAvailableCount int32 | |
| var availableCount int32 | |
| for _, cgs := range ddc.Status.ComputeGroupStatuses { | |
| if cgs.Phase == dv1.Ready { | |
| fullAvailableCount++ | |
| } | |
| if cgs.AvailableReplicas > 0 { | |
| availableCount++ | |
| } | |
| } | |
| ddc.Status.ClusterHealth.CGCount = int32(len(ddc.Status.ComputeGroupStatuses)) | |
| ddc.Status.ClusterHealth.CGFullAvailableCount = fullAvailableCount | |
| ddc.Status.ClusterHealth.CGAvailableCount = availableCount | |
| if errMs == "" { | |
| return nil | |
| } | |
| return errors.New(errMs) | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) recordComputeGroupIds(ddc *dv1.DorisDisaggregatedCluster) error { | |
| // get user and password | |
| adminUserName, password := dcgs.GetManagementAdminUserAndPWD(context.Background(), ddc) | |
| // get host and port | |
| // When the operator and dcr are deployed in different namespace, it will be inaccessible, so need to add the dcr svc namespace | |
| host := ddc.GetFEVIPAddresss() | |
| confMap := dcgs.GetConfigValuesFromConfigMaps(ddc.Namespace, resource.FE_RESOLVEKEY, ddc.Spec.FeSpec.ConfigMaps) | |
| queryPort := resource.GetPort(confMap, resource.QUERY_PORT) | |
| cfg := mysql.NewDBConfig() | |
| cfg.User = adminUserName | |
| cfg.Password = password | |
| cfg.Host = host | |
| cfg.Port = strconv.FormatInt(int64(queryPort), 10) | |
| tlsConfig, secretName := dcgs.DisaggregatedSubDefaultController.FindSecretTLSConfig(confMap, ddc) | |
| secret, _ := k8s.GetSecret(context.Background(), dcgs.K8sclient, ddc.Namespace, secretName) | |
| db, err := mysql.NewDorisSqlDB(cfg, tlsConfig, secret) | |
| if err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController recordComputeGroupIds new doris client failed,err=%s", err.Error()) | |
| return err | |
| } | |
| defer db.Close() | |
| backends, err := db.ShowBackends() | |
| if err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController recordComputeGroupIds show backends failed, err=%s", err.Error()) | |
| return err | |
| } | |
| m := map[string]string{} //statefulsetname:computegroupid | |
| for _, backend := range backends { | |
| tags := map[string]string{} | |
| err = json.Unmarshal([]byte(backend.Tag), &tags) | |
| if err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController recordComputeGroupIds backend tag stirng to map failed, tag: %s, err: %s", backend.Tag, err.Error()) | |
| return err | |
| } | |
| if _, ok := tags[mysql.COMPUTE_GROUP_ID]; !ok { | |
| errMsg := fmt.Sprintf("DisaggregatedComputeGroupsController recordComputeGroupIds backend tag get compute_group_name failed, tag: %s, err: no compute_group_id field found ", backend.Tag) | |
| klog.Errorf(errMsg) | |
| return errors.New(errMsg) | |
| } | |
| podName := strings.Split(backend.Host, ".")[0] | |
| re, _ := regexp.Compile("(.*)-[0-9]+$") | |
| matchs := re.FindStringSubmatch(podName) | |
| stsName := matchs[len(matchs)-1] | |
| m[stsName] = tags[mysql.COMPUTE_GROUP_ID] | |
| } | |
| for i, cgs := range ddc.Status.ComputeGroupStatuses { | |
| ddc.Status.ComputeGroupStatuses[i].ComputeGroupId = m[cgs.StatefulsetName] | |
| } | |
| return nil | |
| } | |
| func (dcgs *DisaggregatedComputeGroupsController) updateCGStatus(ddc *dv1.DorisDisaggregatedCluster, cgs *dv1.ComputeGroupStatus) error { | |
| stfName := cgs.StatefulsetName | |
| sts, err := k8s.GetStatefulSet(context.Background(), dcgs.K8sclient, ddc.Namespace, stfName) | |
| if err != nil { | |
| klog.Errorf("DisaggregatedComputeGroupsController updateCGStatus get statefulset %s failed, err=%s", stfName, err.Error()) | |
| return err | |
| } | |
| //check statefulset updated or not, if this reconcile update the sts, should exclude the circumstance that get old sts and the pods not updated. | |
| updateStatefulsetKey := strings.ToLower(fmt.Sprintf(dv1.UpdateStatefulsetName, stfName)) | |
| if _, updated := ddc.Annotations[updateStatefulsetKey]; updated { | |
| generation := dcgs.DisaggregatedSubDefaultController.ReturnStatefulsetUpdatedGeneration(sts, updateStatefulsetKey) | |
| //if this reconcile not update statefulset will not check the generation equals or not. | |
| if ddc.Generation != generation { | |
| return errors.New("waiting statefulset updated") | |
| } | |
| } | |
| selector := dcgs.newCGPodsSelector(ddc.Name, cgs.UniqueId) | |
| var podList corev1.PodList | |
| if err := dcgs.K8sclient.List(context.Background(), &podList, client.InNamespace(ddc.Namespace), client.MatchingLabels(selector)); err != nil { | |
| return err | |
| } | |
| updateRevision := sts.Status.UpdateRevision | |
| //check all pods controlled by new statefulset. | |
| allUpdated := dcgs.DisaggregatedSubDefaultController.StatefulsetControlledPodsAllUseNewUpdateRevision(updateRevision, podList.Items) | |
| var availableReplicas int32 | |
| var creatingReplicas int32 | |
| var failedReplicas int32 | |
| //get all pod status that controlled by st. | |
| for _, pod := range podList.Items { | |
| if ready := k8s.PodIsReady(&pod.Status); ready { | |
| availableReplicas++ | |
| } else if pod.Status.Phase == corev1.PodRunning || pod.Status.Phase == corev1.PodPending { | |
| creatingReplicas++ | |
| } else { | |
| failedReplicas++ | |
| } | |
| } | |
| cgs.AvailableReplicas = availableReplicas | |
| if hasGracefulAction(sts) { | |
| switch cgs.Phase { | |
| case dv1.Ready: | |
| cgs.Phase = dv1.GracefulRolling | |
| case dv1.Reconciling, dv1.GracefulRolling, dv1.GracefulScaling, dv1.GracefulDeleting: | |
| // Keep the phase in a reconciling/graceful state while the StatefulSet | |
| // still carries graceful-action annotation. | |
| } | |
| return nil | |
| } | |
| if allUpdated && availableReplicas == cgs.Replicas { | |
| cgs.Phase = dv1.Ready | |
| } | |
| return nil | |
| } | |
| func hasGracefulAction(sts *appv1.StatefulSet) bool { | |
| if sts == nil || sts.Annotations == nil { | |
| return false | |
| } | |
| return sts.Annotations[gracefulActionAnnotation] != "" | |
| } |