github.com/verrazzano/verrazzano-monitoring-operator@v0.0.30/pkg/resources/statefulsets/plan.go (about) 1 // Copyright (C) 2022, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package statefulsets 5 6 import ( 7 "errors" 8 "github.com/verrazzano/pkg/diff" 9 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/config" 10 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/resources" 11 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/util/logs/vzlog" 12 appsv1 "k8s.io/api/apps/v1" 13 ) 14 15 const ( 16 minClusterSize = 3 17 ) 18 19 type ( 20 //StatefulSetPlan describes how to update the cluster statefulsets 21 StatefulSetPlan struct { 22 Create []*appsv1.StatefulSet 23 Update []*appsv1.StatefulSet 24 Delete []*appsv1.StatefulSet 25 Conflict error 26 ExistingCluster bool 27 BounceNodes bool 28 } 29 30 statefulSetMapping struct { 31 existing map[string]*appsv1.StatefulSet 32 expected map[string]*appsv1.StatefulSet 33 isScaleDownAllowed bool 34 existingSize int32 35 expectedSize int32 36 } 37 ) 38 39 // CreatePlan creates a plan for sts that need to be created, updated, or deleted. 40 // if the cluster is not safe to scale down, updates/deletes will be rejected. 41 func CreatePlan(log vzlog.VerrazzanoLogger, existingList, expectedList []*appsv1.StatefulSet) *StatefulSetPlan { 42 mapping := createStatefulSetMapping(existingList, expectedList) 43 plan := &StatefulSetPlan{ 44 // There is no running cluster if the existing size is 0 45 ExistingCluster: mapping.existingSize > 0, 46 // Always bounce the master node if we are running a single node cluster 47 // This permits modifications to single node clusters, which are inherently unstable 48 BounceNodes: mapping.existingSize == 1, 49 } 50 for name, expected := range mapping.expected { 51 existing, ok := mapping.existing[name] 52 if !ok { 53 // the STS should be created 54 plan.Create = append(plan.Create, expected) 55 } else if mapping.isScaleDownAllowed || !plan.ExistingCluster { 56 // The cluster is in a state that allows updates, so we check if the STS has changed 57 CopyFromExisting(expected, existing) 58 specDiffs := diff.Diff(existing, expected) 59 if specDiffs != "" || *existing.Spec.Replicas != *expected.Spec.Replicas { 60 log.Oncef("Statefulset %s/%s has spec differences %s", expected.Namespace, expected.Name, specDiffs) 61 plan.Update = append(plan.Update, expected) 62 } 63 } 64 } 65 66 for name, existing := range mapping.existing { 67 // the existing STS isn't found in the expected list 68 if _, ok := mapping.expected[name]; !ok && mapping.isScaleDownAllowed { 69 plan.Delete = append(plan.Delete, existing) 70 } 71 } 72 73 if !mapping.isScaleDownAllowed && plan.ExistingCluster { 74 plan.Conflict = errors.New("skipping OpenSearch StatefulSet delete/update, cluster cannot safely lose any master nodes") 75 } 76 77 return plan 78 } 79 80 // createStatefulSetMapping creates a mapping of statefulset and checks if the plan would scale the cluster to an inconsistent state. 81 // A cluster cannot be scaled down if: 82 // - if has less than minClusterSize master replicas 83 // - the scale down would remove half or more of the master replicas 84 func createStatefulSetMapping(existingList, expectedList []*appsv1.StatefulSet) *statefulSetMapping { 85 mapping := &statefulSetMapping{ 86 existing: map[string]*appsv1.StatefulSet{}, 87 expected: map[string]*appsv1.StatefulSet{}, 88 } 89 var existingSize, expectedSize int32 90 for _, sts := range existingList { 91 existingSize += sts.Status.ReadyReplicas 92 mapping.existing[sts.Name] = sts 93 } 94 for _, sts := range expectedList { 95 expectedSize += *sts.Spec.Replicas 96 mapping.expected[sts.Name] = sts 97 } 98 99 // if expected size is one, we have a single node cluster. By definition these are 100 // less resilient, so updates/restarts are allowed (otherwise they would never be possible). 101 if expectedSize == 0 || (existingSize == 1 && expectedList[0].Name == existingList[0].Name) { 102 // if we have a single node cluster, or the desired outcome is to scale everything down to 0, 103 // scale down is allowed 104 mapping.isScaleDownAllowed = true 105 } else { 106 mapping.isScaleDownAllowed = expectedSize >= minClusterSize && 107 expectedSize > existingSize/2 108 } 109 mapping.existingSize = existingSize 110 mapping.expectedSize = expectedSize 111 return mapping 112 } 113 114 // CopyFromExisting copies fields that should not be changed from existing to expected. 115 func CopyFromExisting(expected, existing *appsv1.StatefulSet) { 116 // Changes to volume claim templates are forbidden 117 expected.Spec.VolumeClaimTemplates = existing.Spec.VolumeClaimTemplates 118 // Changes to selector are forbidden 119 expected.Spec.Selector = existing.Spec.Selector 120 resources.CopyImmutableEnvVars(expected.Spec.Template.Spec.Containers, existing.Spec.Template.Spec.Containers, config.ElasticsearchMaster.Name) 121 }