github.com/verrazzano/verrazzano-monitoring-operator@v0.0.30/pkg/resources/statefulsets/statefulset.go (about)

     1  // Copyright (C) 2020, 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  	"fmt"
     8  
     9  	vmcontrollerv1 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/apis/vmcontroller/v1"
    10  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/config"
    11  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/constants"
    12  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/resources"
    13  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/resources/nodes"
    14  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/util/logs/vzlog"
    15  	"github.com/verrazzano/verrazzano-monitoring-operator/pkg/util/memory"
    16  
    17  	appsv1 "k8s.io/api/apps/v1"
    18  	corev1 "k8s.io/api/core/v1"
    19  	storagev1 "k8s.io/api/storage/v1"
    20  	"k8s.io/apimachinery/pkg/api/resource"
    21  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    22  	"k8s.io/apimachinery/pkg/util/intstr"
    23  )
    24  
    25  // New creates StatefulSet objects for a VMO resource
    26  func New(log vzlog.VerrazzanoLogger, vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, storageClass *storagev1.StorageClass, initialMasterNodes string) ([]*appsv1.StatefulSet, error) {
    27  	var statefulSets []*appsv1.StatefulSet
    28  
    29  	// OpenSearch MasterNodes
    30  	if vmo.Spec.Elasticsearch.Enabled {
    31  		statefulSets = append(statefulSets, createOpenSearchStatefulSets(log, vmo, storageClass, initialMasterNodes)...)
    32  	}
    33  	return statefulSets, nil
    34  }
    35  
    36  func createOpenSearchStatefulSets(log vzlog.VerrazzanoLogger, vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, storageClass *storagev1.StorageClass, initialMasterNodes string) []*appsv1.StatefulSet {
    37  	var statefulSets []*appsv1.StatefulSet
    38  	for _, node := range nodes.MasterNodes(vmo) {
    39  		if node.Replicas > 0 {
    40  			statefulSet := createOpenSearchStatefulSet(log, vmo, storageClass, node, initialMasterNodes)
    41  			statefulSets = append(statefulSets, statefulSet)
    42  		}
    43  	}
    44  
    45  	return statefulSets
    46  }
    47  
    48  // Creates StatefulSet for OpenSearch
    49  func createOpenSearchStatefulSet(log vzlog.VerrazzanoLogger, vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, storageClass *storagev1.StorageClass, node vmcontrollerv1.ElasticsearchNode, initialMasterNodes string) *appsv1.StatefulSet {
    50  	// Headless service for OpenSearch
    51  	headlessService := resources.GetMetaName(vmo.Name, config.ElasticsearchMaster.Name)
    52  	statefulSetName := resources.GetMetaName(vmo.Name, node.Name)
    53  	// Create base statefulset object
    54  	statefulSet := createStatefulSetElement(vmo, &node.Resources, config.ElasticsearchMaster, headlessService, statefulSetName)
    55  	// Add node labels
    56  	statefulSet.Spec.Selector.MatchLabels[constants.NodeGroupLabel] = node.Name
    57  	statefulSet.Spec.Template.Labels[constants.NodeGroupLabel] = node.Name
    58  
    59  	statefulSet.Spec.Replicas = resources.NewVal(node.Replicas)
    60  	statefulSet.Spec.Template.Spec.Affinity = resources.CreateZoneAntiAffinityElement(vmo.Name, config.ElasticsearchMaster.Name)
    61  
    62  	var elasticsearchUID int64 = 1000
    63  	esMasterContainer := &statefulSet.Spec.Template.Spec.Containers[0]
    64  	esMasterContainer.SecurityContext.RunAsUser = &elasticsearchUID
    65  	esMasterContainer.Ports[0].Name = "transport"
    66  	esMasterContainer.Ports = append(esMasterContainer.Ports, corev1.ContainerPort{Name: "http", ContainerPort: int32(constants.OSHTTPPort), Protocol: "TCP"})
    67  
    68  	javaOpts, err := memory.PodMemToJvmHeapArgs(node.Resources.RequestMemory, constants.DefaultDevProfileESMemArgs) // Default JVM heap settings if none provided
    69  	if err != nil {
    70  		javaOpts = constants.DefaultDevProfileESMemArgs
    71  		log.Errorf("Failed to derive heap sizes from MasterNodes pod, using default %s: %v", javaOpts, err)
    72  	}
    73  
    74  	if node.JavaOpts != "" {
    75  		javaOpts = node.JavaOpts
    76  	}
    77  	// Adding command for add keystore values at pod bootup
    78  	esMasterContainer.Command = []string{
    79  		"sh",
    80  		"-c",
    81  		resources.CreateOpenSearchContainerCMD(javaOpts, resources.GetOpenSearchPluginList(vmo)),
    82  	}
    83  	var envVars = []corev1.EnvVar{
    84  		{
    85  			Name: "node.name",
    86  			ValueFrom: &corev1.EnvVarSource{
    87  				FieldRef: &corev1.ObjectFieldSelector{
    88  					FieldPath: "metadata.name",
    89  				},
    90  			},
    91  		},
    92  		{Name: "cluster.name", Value: vmo.Name},
    93  		// HTTP is enabled on the master here solely for our readiness check below (on _cluster/health)
    94  		{Name: "HTTP_ENABLE", Value: "true"},
    95  		{Name: "logger.org.opensearch", Value: "info"},
    96  		{Name: constants.ObjectStoreAccessKeyVarName,
    97  			ValueFrom: &corev1.EnvVarSource{
    98  				SecretKeyRef: &corev1.SecretKeySelector{
    99  					LocalObjectReference: corev1.LocalObjectReference{
   100  						Name: constants.VerrazzanoBackupScrtName,
   101  					},
   102  					Key: constants.ObjectStoreAccessKey,
   103  					Optional: func(opt bool) *bool {
   104  						return &opt
   105  					}(true),
   106  				},
   107  			},
   108  		},
   109  		{Name: constants.ObjectStoreCustomerKeyVarName,
   110  			ValueFrom: &corev1.EnvVarSource{
   111  				SecretKeyRef: &corev1.SecretKeySelector{
   112  					LocalObjectReference: corev1.LocalObjectReference{
   113  						Name: constants.VerrazzanoBackupScrtName,
   114  					},
   115  					Key: constants.ObjectStoreCustomerKey,
   116  					Optional: func(opt bool) *bool {
   117  						return &opt
   118  					}(true),
   119  				},
   120  			},
   121  		},
   122  	}
   123  	var readinessProbeCondition string
   124  	envVars = append(envVars,
   125  		corev1.EnvVar{Name: "OPENSEARCH_JAVA_OPTS", Value: javaOpts},
   126  	)
   127  	if nodes.IsSingleNodeCluster(vmo) {
   128  		node.Roles = []vmcontrollerv1.NodeRole{
   129  			vmcontrollerv1.MasterRole,
   130  			vmcontrollerv1.DataRole,
   131  			vmcontrollerv1.IngestRole,
   132  		}
   133  		log.Oncef("ES topology for %s indicates a single-node cluster (single master node only)", vmo.Name)
   134  		envVars = append(envVars,
   135  			corev1.EnvVar{Name: "node.roles", Value: nodes.GetRolesString(&node)},
   136  			corev1.EnvVar{Name: "discovery.type", Value: "single-node"},
   137  		)
   138  	} else {
   139  		envVars = append(envVars,
   140  			corev1.EnvVar{Name: "node.roles", Value: nodes.GetRolesString(&node)},
   141  			corev1.EnvVar{
   142  				Name:  "discovery.seed_hosts",
   143  				Value: headlessService,
   144  			},
   145  		)
   146  		if initialMasterNodes != "" {
   147  			envVars = append(envVars, corev1.EnvVar{Name: constants.ClusterInitialMasterNodes, Value: initialMasterNodes})
   148  		}
   149  	}
   150  	esMasterContainer.Env = envVars
   151  
   152  	basicAuthParams := ""
   153  	readinessProbeCondition = `kpo
   154  
   155          echo 'Cluster is not yet ready'
   156          exit 1
   157  `
   158  	// Customized Readiness and Liveness probes
   159  	esMasterContainer.ReadinessProbe =
   160  		&corev1.Probe{
   161  			ProbeHandler: corev1.ProbeHandler{
   162  				Exec: &corev1.ExecAction{
   163  					Command: []string{
   164  						"sh",
   165  						"-c",
   166  						`#!/usr/bin/env bash -e
   167  # If the node is starting up wait for the cluster to be ready' )
   168  # Once it has started only check that the node itself is responding
   169  START_FILE=/tmp/.es_start_file
   170  http () {
   171      local path="${1}"
   172      curl -v -XGET -s -k ` + basicAuthParams + ` --fail http://127.0.0.1:9200${path}
   173  }
   174  if [ -f "${START_FILE}" ]; then
   175      echo 'Elasticsearch is already running, lets check the node is healthy'
   176      http "` + config.ElasticsearchMaster.ReadinessHTTPPath + `"
   177  else
   178      echo 'Waiting for elasticsearch cluster to become cluster to be ready'
   179      if http "` + config.ElasticsearchMaster.ReadinessHTTPPath + `" ; then
   180          touch ${START_FILE}
   181      else` + readinessProbeCondition + `
   182      fi
   183      exit 0
   184  fi`,
   185  					},
   186  				},
   187  			},
   188  			InitialDelaySeconds: 90,
   189  			SuccessThreshold:    3,
   190  			PeriodSeconds:       5,
   191  			TimeoutSeconds:      5,
   192  		}
   193  
   194  	esMasterContainer.LivenessProbe =
   195  		&corev1.Probe{
   196  			ProbeHandler: corev1.ProbeHandler{
   197  				TCPSocket: &corev1.TCPSocketAction{
   198  					Port: intstr.IntOrString{
   199  						IntVal: int32(config.ElasticsearchMaster.Port),
   200  					},
   201  				},
   202  			},
   203  			InitialDelaySeconds: 30,
   204  			PeriodSeconds:       10,
   205  			TimeoutSeconds:      5,
   206  			FailureThreshold:    5,
   207  		}
   208  
   209  	const esMasterVolName = "elasticsearch-master"
   210  	const esMasterData = "/usr/share/opensearch/data"
   211  
   212  	// Add the pv volume mount to the main container
   213  	esMasterContainer.VolumeMounts =
   214  		append(esMasterContainer.VolumeMounts, corev1.VolumeMount{
   215  			Name:      esMasterVolName,
   216  			MountPath: esMasterData,
   217  		})
   218  
   219  	// Add init container
   220  	statefulSet.Spec.Template.Spec.InitContainers = append(statefulSet.Spec.Template.Spec.InitContainers,
   221  		*resources.GetElasticsearchMasterInitContainer())
   222  
   223  	// Add the pv volume mount to the init container
   224  	statefulSet.Spec.Template.Spec.InitContainers[0].VolumeMounts =
   225  		[]corev1.VolumeMount{{
   226  			Name:      esMasterVolName,
   227  			MountPath: esMasterData,
   228  		}}
   229  
   230  	// Add the pvc templates, this will result in a PV + PVC being created automatically for each
   231  	// pod in the stateful set.
   232  	if node.Storage != nil && len(node.Storage.Size) > 0 {
   233  		statefulSet.Spec.VolumeClaimTemplates =
   234  			[]corev1.PersistentVolumeClaim{{
   235  				ObjectMeta: metav1.ObjectMeta{
   236  					Name:      esMasterVolName,
   237  					Namespace: vmo.Namespace,
   238  				},
   239  				Spec: corev1.PersistentVolumeClaimSpec{
   240  					AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
   241  					Resources: corev1.ResourceRequirements{
   242  						Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse(node.Storage.Size)},
   243  					},
   244  				},
   245  			}}
   246  		// Only set the storage class name if one was explicitly specified by the user.
   247  		// This is to facilitate upgrades where storage class name is empty,
   248  		// since you cannot update this field of a statefulset
   249  		if storageClass != nil {
   250  			statefulSet.Spec.VolumeClaimTemplates[0].Spec.StorageClassName = &storageClass.Name
   251  		}
   252  	} else {
   253  		statefulSet.Spec.Template.Spec.Volumes = []corev1.Volume{
   254  			{
   255  				Name: esMasterVolName,
   256  				VolumeSource: corev1.VolumeSource{
   257  					EmptyDir: &corev1.EmptyDirVolumeSource{},
   258  				},
   259  			},
   260  		}
   261  	}
   262  
   263  	// add istio annotations required for inter component communication
   264  	if statefulSet.Spec.Template.Annotations == nil {
   265  		statefulSet.Spec.Template.Annotations = make(map[string]string)
   266  	}
   267  	statefulSet.Spec.Template.Annotations["traffic.sidecar.istio.io/excludeInboundPorts"] = fmt.Sprintf("%d", constants.OSTransportPort)
   268  	statefulSet.Spec.Template.Annotations["traffic.sidecar.istio.io/excludeOutboundPorts"] = fmt.Sprintf("%d", constants.OSTransportPort)
   269  
   270  	// set Node Role labels for role based selectors
   271  	nodes.SetNodeRoleLabels(&node, statefulSet.Spec.Template.Labels)
   272  	return statefulSet
   273  }
   274  
   275  // Creates a statefulset element for the given VMO and component
   276  func createStatefulSetElement(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, vmoResources *vmcontrollerv1.Resources,
   277  	componentDetails config.ComponentDetails, serviceName, statefulSetName string) *appsv1.StatefulSet {
   278  	labels := resources.GetSpecID(vmo.Name, componentDetails.Name)
   279  	resourceLabel := resources.GetMetaLabels(vmo)
   280  	resourceLabel[constants.ComponentLabel] = resources.GetCompLabel(componentDetails.Name)
   281  	podLabels := resources.DeepCopyMap(labels)
   282  	podLabels[constants.ComponentLabel] = resources.GetCompLabel(componentDetails.Name)
   283  	return &appsv1.StatefulSet{
   284  		ObjectMeta: metav1.ObjectMeta{
   285  			Labels:          resourceLabel,
   286  			Name:            statefulSetName,
   287  			Namespace:       vmo.Namespace,
   288  			OwnerReferences: resources.GetOwnerReferences(vmo),
   289  		},
   290  		Spec: appsv1.StatefulSetSpec{
   291  			Replicas: resources.NewVal(1),
   292  			// The default PodManagementPolicy (OrderedReady) has known issues where a statefulset with
   293  			// a crashing pod is never updated on further statefulset changes, so use Parallel here
   294  			PodManagementPolicy: appsv1.ParallelPodManagement,
   295  			ServiceName:         serviceName,
   296  			Selector: &metav1.LabelSelector{
   297  				MatchLabels: labels,
   298  			},
   299  			Template: corev1.PodTemplateSpec{
   300  				ObjectMeta: metav1.ObjectMeta{
   301  					Labels: podLabels,
   302  				},
   303  				Spec: corev1.PodSpec{
   304  					Containers: []corev1.Container{
   305  						resources.CreateContainerElement(nil, vmoResources, componentDetails),
   306  					},
   307  					ServiceAccountName:            constants.ServiceAccountName,
   308  					TerminationGracePeriodSeconds: resources.New64Val(1),
   309  				},
   310  			},
   311  		},
   312  	}
   313  }