diff --git a/config/manifests/bases/aerospike-kubernetes-operator.clusterserviceversion.yaml b/config/manifests/bases/aerospike-kubernetes-operator.clusterserviceversion.yaml index 967ecbf8..d48338b4 100644 --- a/config/manifests/bases/aerospike-kubernetes-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/aerospike-kubernetes-operator.clusterserviceversion.yaml @@ -60,12 +60,12 @@ spec: path: resources - description: SecretMounts is the list of secret to be mounted in the backup service. - displayName: Backup Service Volume + displayName: Backup Service SecretMounts path: secrets - description: Service defines the Kubernetes service configuration for the backup service. It is used to expose the backup service deployment. By default, the service type is ClusterIP. - displayName: Backup Service + displayName: K8s Service path: service version: v1beta1 - description: AerospikeCluster is the schema for the AerospikeCluster API @@ -235,7 +235,8 @@ spec: displayName: Restore Service Polling Period path: pollingPeriod - description: Type is the type of restore. It can of type Full, Incremental, - and Timestamp. Based on the restore type, relevant restore config is given. + and Timestamp. Based on the restore type, the relevant restore config should + be given. displayName: Restore Type path: type version: v1beta1 diff --git a/controllers/reconciler.go b/controllers/reconciler.go index 3d786647..a6999b82 100644 --- a/controllers/reconciler.go +++ b/controllers/reconciler.go @@ -249,6 +249,16 @@ func (r *SingleClusterReconciler) Reconcile() (result ctrl.Result, recErr error) } } + if r.IsReclusterNeeded() { + if err = deployment.InfoRecluster( + r.Log, + r.getClientPolicy(), allHostConns, + ); err != nil { + r.Log.Error(err, "Failed to do recluster") + return reconcile.Result{}, err + } + } + // Update the AerospikeCluster status. if err = r.updateStatus(); err != nil { r.Log.Error(err, "Failed to update AerospikeCluster status") @@ -1023,3 +1033,48 @@ func (r *SingleClusterReconciler) AddAPIVersionLabel(ctx context.Context) error return r.Client.Update(ctx, aeroCluster, common.UpdateOption) } + +func (r *SingleClusterReconciler) IsReclusterNeeded() bool { + // Return false if dynamic configuration updates are disabled + if !asdbv1.GetBool(r.aeroCluster.Spec.EnableDynamicConfigUpdate) { + return false + } + + for specIdx := range r.aeroCluster.Spec.RackConfig.Racks { + for statusIdx := range r.aeroCluster.Status.RackConfig.Racks { + if r.aeroCluster.Spec.RackConfig.Racks[specIdx].ID == r.aeroCluster.Status.RackConfig.Racks[statusIdx].ID && + r.IsReclusterNeededForRack(&r.aeroCluster.Spec.RackConfig.Racks[specIdx], + &r.aeroCluster.Status.RackConfig.Racks[statusIdx]) { + return true + } + } + } + + return false +} + +func (r *SingleClusterReconciler) IsReclusterNeededForRack(specRack, statusRack *asdbv1.Rack) bool { + specNamespaces, ok := specRack.AerospikeConfig.Value["namespaces"].([]interface{}) + if !ok { + return false + } + + statusNamespaces, ok := statusRack.AerospikeConfig.Value["namespaces"].([]interface{}) + if !ok { + return false + } + + for _, specNamespace := range specNamespaces { + for _, statusNamespace := range statusNamespaces { + if specNamespace.(map[string]interface{})["name"] != statusNamespace.(map[string]interface{})["name"] { + continue + } + + if specNamespace.(map[string]interface{})["active-rack"] != statusNamespace.(map[string]interface{})["active-rack"] { + return true + } + } + } + + return false +} diff --git a/go.mod b/go.mod index e8752f31..464340a2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 toolchain go1.21.8 require ( - github.com/aerospike/aerospike-management-lib v1.4.0 + github.com/aerospike/aerospike-management-lib v1.4.1-0.20240905074352-532f7cf2e66b github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/evanphx/json-patch v4.12.0+incompatible github.com/go-logr/logr v1.4.1 diff --git a/go.sum b/go.sum index 1c101966..44818755 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/abhishekdwivedi3060/aerospike-backup-service v0.0.0-20240709182036-38 github.com/abhishekdwivedi3060/aerospike-backup-service v0.0.0-20240709182036-38038c5c38c7/go.mod h1:CMA+bHRLvL/Kj/aLlbu95iNnlPnvP67q62X81b5e2G4= github.com/aerospike/aerospike-client-go/v7 v7.4.0 h1:g8/7v8RHhQhTArhW3C7Au7o+u8j8x5eySZL6MXfpHKU= github.com/aerospike/aerospike-client-go/v7 v7.4.0/go.mod h1:pPKnWiS8VDJcH4IeB1b8SA2TWnkjcVLHwAAJ+BHfGK8= -github.com/aerospike/aerospike-management-lib v1.4.0 h1:wT0l3kwzXv5DV5Cd+hD0BQq3hjSIyaPX1HaUb1304TI= -github.com/aerospike/aerospike-management-lib v1.4.0/go.mod h1:3JKrmC/mLSV8SygbrPQPNV8T7bFaTMjB8wfnX25gB+4= +github.com/aerospike/aerospike-management-lib v1.4.1-0.20240905074352-532f7cf2e66b h1:x7zrG9VzAdGU4MUOVBhB8OZ8VWUgko1qqfA5ikyocWw= +github.com/aerospike/aerospike-management-lib v1.4.1-0.20240905074352-532f7cf2e66b/go.mod h1:3JKrmC/mLSV8SygbrPQPNV8T7bFaTMjB8wfnX25gB+4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= diff --git a/test/cluster/dynamic_config_test.go b/test/cluster/dynamic_config_test.go index e318c9fb..b78c6745 100644 --- a/test/cluster/dynamic_config_test.go +++ b/test/cluster/dynamic_config_test.go @@ -607,6 +607,87 @@ var _ = Describe( ) }, ) + + Context( + "When changing fields those need recluster", func() { + BeforeEach( + func() { + // Create a 4 node cluster + aeroCluster := createNonSCDummyAerospikeCluster( + clusterNamespacedName, 4, + ) + aeroCluster.Spec.EnableDynamicConfigUpdate = ptr.To(true) + aeroCluster.Spec.Image = "aerospike.jfrog.io/docker/aerospike/aerospike-server-enterprise-rc:7.2.0.0-rc1" + aeroCluster.Spec.PodSpec.ImagePullSecrets = []v1.LocalObjectReference{ + { + Name: "regcred", + }, + } + aeroCluster.Spec.RackConfig.Racks = append(aeroCluster.Spec.RackConfig.Racks, + asdbv1.Rack{ + ID: 1, + }, + asdbv1.Rack{ + ID: 2, + }) + aeroCluster.Spec.RackConfig.Namespaces = []string{ + "test", + } + err := deployCluster(k8sClient, ctx, aeroCluster) + Expect(err).ToNot(HaveOccurred()) + }, + ) + + AfterEach( + func() { + aeroCluster, err := getCluster(k8sClient, ctx, clusterNamespacedName) + Expect(err).ToNot(HaveOccurred()) + + _ = deleteCluster(k8sClient, ctx, aeroCluster) + }, + ) + + It( + "Should update active-rack dynamically", func() { + + By("Modify dynamic config by adding fields") + + aeroCluster, err := getCluster( + k8sClient, ctx, clusterNamespacedName, + ) + Expect(err).ToNot(HaveOccurred()) + + podPIDMap, err := getPodIDs(ctx, aeroCluster) + Expect(err).ToNot(HaveOccurred()) + + nsList := aeroCluster.Spec.AerospikeConfig.Value["namespaces"].([]interface{}) + nsList[0].(map[string]interface{})["active-rack"] = 1 + + err = updateCluster(k8sClient, ctx, aeroCluster) + Expect(err).ToNot(HaveOccurred()) + + By("Fetch and verify dynamic configs") + + pod := aeroCluster.Status.Pods["dynamic-config-test-1-0"] + + info, err := requestInfoFromNode(logger, k8sClient, ctx, clusterNamespacedName, "namespace/test", &pod) + Expect(err).ToNot(HaveOccurred()) + + confs := strings.Split(info["namespace/test"], ";") + for _, conf := range confs { + if strings.Contains(conf, "effective_active_rack") { + keyValue := strings.Split(conf, "=") + Expect(keyValue[1]).To(Equal("1")) + } + } + + By("Verify no warm/cold restarts in Pods") + + validateServerRestart(ctx, aeroCluster, podPIDMap, false) + }, + ) + }, + ) }, ) diff --git a/test/cluster/utils.go b/test/cluster/utils.go index a1af81c8..f4b51884 100644 --- a/test/cluster/utils.go +++ b/test/cluster/utils.go @@ -600,6 +600,30 @@ func getAerospikeConfigFromNode(log logr.Logger, k8sClient client.Client, ctx go return confs[configContext].(lib.Stats), nil } +func requestInfoFromNode(log logr.Logger, k8sClient client.Client, ctx goctx.Context, + clusterNamespacedName types.NamespacedName, cmd string, pod *asdbv1.AerospikePodStatus) (map[string]string, error) { + aeroCluster, err := getCluster(k8sClient, ctx, clusterNamespacedName) + if err != nil { + return nil, err + } + + host, err := createHost(pod) + if err != nil { + return nil, err + } + + asinfo := info.NewAsInfo( + log, host, getClientPolicy(aeroCluster, k8sClient), + ) + + confs, err := asinfo.RequestInfo(cmd) + if err != nil { + return nil, err + } + + return confs, nil +} + func getPasswordFromSecret(k8sClient client.Client, secretNamespcedName types.NamespacedName, passFileName string, ) (string, error) {