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  }