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 }