github.com/verrazzano/verrazzano-monitoring-operator@v0.0.30/pkg/resources/deployments/deployment.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 deployments 5 6 import ( 7 "fmt" 8 "strconv" 9 "strings" 10 11 vmcontrollerv1 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/apis/vmcontroller/v1" 12 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/config" 13 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/constants" 14 "github.com/verrazzano/verrazzano-monitoring-operator/pkg/resources" 15 16 appsv1 "k8s.io/api/apps/v1" 17 corev1 "k8s.io/api/core/v1" 18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 "k8s.io/client-go/kubernetes" 20 ) 21 22 // Elasticsearch interface 23 type Elasticsearch interface { 24 createElasticsearchDeploymentElements(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, pvcToAdMap map[string]string) []*appsv1.Deployment 25 createElasticsearchDataDeploymentElements(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, pvcToAdMap map[string]string) []*appsv1.Deployment 26 createElasticsearchIngestDeploymentElements(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance) []*appsv1.Deployment 27 } 28 29 type ExpectedDeployments struct { 30 Deployments []*appsv1.Deployment 31 GrafanaDeployments int 32 OpenSearchDataDeployments int 33 OpenSearchIngestDeployments int 34 } 35 36 // New function creates deployment objects for a VMO resource. It also sets the appropriate OwnerReferences on 37 // the resource so handleObject can discover the VMO resource that 'owns' it. 38 func New(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, kubeclientset kubernetes.Interface, operatorConfig *config.OperatorConfig, pvcToAdMap map[string]string) (*ExpectedDeployments, error) { 39 expected := &ExpectedDeployments{} 40 var deployments []*appsv1.Deployment 41 var err error 42 43 if vmo.Spec.Elasticsearch.Enabled { 44 basic := ElasticsearchBasic{} 45 ingestDeployments := basic.createElasticsearchIngestDeploymentElements(vmo) 46 dataDeployments := basic.createElasticsearchDataDeploymentElements(vmo, pvcToAdMap) 47 deployments = append(deployments, ingestDeployments...) 48 deployments = append(deployments, dataDeployments...) 49 expected.OpenSearchIngestDeployments += len(ingestDeployments) 50 expected.OpenSearchDataDeployments += len(dataDeployments) 51 } 52 53 // Grafana 54 if vmo.Spec.Grafana.Enabled { 55 expected.GrafanaDeployments++ 56 deployment := createDeploymentElement(vmo, &vmo.Spec.Grafana.Storage, &vmo.Spec.Grafana.Resources, config.Grafana, config.Grafana.Name) 57 deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy = config.Grafana.ImagePullPolicy 58 deployment.Spec.Replicas = resources.NewVal(vmo.Spec.Grafana.Replicas) 59 deployment.Spec.Template.Spec.Affinity = resources.CreateZoneAntiAffinityElement(vmo.Name, config.Grafana.Name) 60 61 deployment.Spec.Strategy.Type = "Recreate" 62 deployment.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{ 63 {Name: "GF_PATHS_PROVISIONING", Value: "/etc/grafana/provisioning"}, 64 {Name: "GF_SERVER_ENABLE_GZIP", Value: "true"}, 65 {Name: "PROMETHEUS_TARGETS", Value: "http://" + constants.VMOServiceNamePrefix + vmo.Name + "-" + config.Prometheus.Name + ":" + strconv.Itoa(config.Prometheus.Port)}, 66 } 67 if config.Grafana.OidcProxy == nil { 68 deployment.Spec.Template.Spec.Containers[0].Env = append(deployment.Spec.Template.Spec.Containers[0].Env, []corev1.EnvVar{ 69 { 70 Name: "GF_SECURITY_ADMIN_USER", 71 ValueFrom: &corev1.EnvVarSource{ 72 SecretKeyRef: &corev1.SecretKeySelector{ 73 LocalObjectReference: corev1.LocalObjectReference{ 74 Name: constants.GrafanaAdminSecret, 75 }, 76 Key: constants.VMOSecretUsernameField, 77 }, 78 }, 79 }, 80 { 81 Name: "GF_SECURITY_ADMIN_PASSWORD", 82 ValueFrom: &corev1.EnvVarSource{ 83 SecretKeyRef: &corev1.SecretKeySelector{ 84 LocalObjectReference: corev1.LocalObjectReference{ 85 Name: constants.GrafanaAdminSecret, 86 }, 87 Key: constants.VMOSecretPasswordField, 88 }, 89 }, 90 }, 91 {Name: "GF_AUTH_ANONYMOUS_ENABLED", Value: "false"}, 92 {Name: "GF_AUTH_BASIC_ENABLED", Value: "true"}, 93 {Name: "GF_USERS_ALLOW_SIGN_UP", Value: "false"}, 94 {Name: "GF_USERS_AUTO_ASSIGN_ORG", Value: "true"}, 95 {Name: "GF_USERS_AUTO_ASSIGN_ORG_ROLE", Value: "Editor"}, 96 {Name: "GF_AUTH_DISABLE_LOGIN_FORM", Value: "false"}, 97 {Name: "GF_AUTH_DISABLE_SIGNOUT_MENU", Value: "false"}, 98 }...) 99 } else { 100 deployment.Spec.Template.Spec.Containers[0].Env = append(deployment.Spec.Template.Spec.Containers[0].Env, []corev1.EnvVar{ 101 { 102 Name: "GF_SECURITY_ADMIN_USER", 103 ValueFrom: &corev1.EnvVarSource{ 104 SecretKeyRef: &corev1.SecretKeySelector{ 105 LocalObjectReference: corev1.LocalObjectReference{ 106 Name: constants.GrafanaAdminSecret, 107 }, 108 Key: constants.VMOSecretUsernameField, 109 }, 110 }, 111 }, 112 { 113 Name: "GF_SECURITY_ADMIN_PASSWORD", 114 ValueFrom: &corev1.EnvVarSource{ 115 SecretKeyRef: &corev1.SecretKeySelector{ 116 LocalObjectReference: corev1.LocalObjectReference{ 117 Name: constants.GrafanaAdminSecret, 118 }, 119 Key: constants.VMOSecretPasswordField, 120 }, 121 }, 122 }, 123 {Name: "GF_AUTH_ANONYMOUS_ENABLED", Value: "false"}, 124 {Name: "GF_AUTH_BASIC_ENABLED", Value: "false"}, 125 {Name: "GF_USERS_ALLOW_SIGN_UP", Value: "false"}, 126 {Name: "GF_USERS_AUTO_ASSIGN_ORG", Value: "true"}, 127 {Name: "GF_USERS_AUTO_ASSIGN_ORG_ROLE", Value: "Editor"}, 128 {Name: "GF_AUTH_DISABLE_LOGIN_FORM", Value: "true"}, 129 {Name: "GF_AUTH_DISABLE_SIGNOUT_MENU", Value: "true"}, 130 {Name: "GF_AUTH_PROXY_ENABLED", Value: "true"}, 131 {Name: "GF_AUTH_PROXY_HEADER_NAME", Value: "X-WEBAUTH-USER"}, 132 {Name: "GF_AUTH_PROXY_HEADER_PROPERTY", Value: "username"}, 133 {Name: "GF_AUTH_PROXY_AUTO_SIGN_UP", Value: "true"}, 134 }...) 135 } 136 if vmo.Spec.Grafana.Database != nil { 137 deployment.Spec.Template.Spec.Containers[0].Env = append(deployment.Spec.Template.Spec.Containers[0].Env, []corev1.EnvVar{ 138 { 139 Name: "GF_DATABASE_PASSWORD", 140 ValueFrom: &corev1.EnvVarSource{ 141 SecretKeyRef: &corev1.SecretKeySelector{ 142 LocalObjectReference: corev1.LocalObjectReference{ 143 Name: vmo.Spec.Grafana.Database.PasswordSecret, 144 }, 145 Key: constants.VMOSecretPasswordField, 146 }, 147 }, 148 }, 149 { 150 Name: "GF_DATABASE_USER", 151 ValueFrom: &corev1.EnvVarSource{ 152 SecretKeyRef: &corev1.SecretKeySelector{ 153 LocalObjectReference: corev1.LocalObjectReference{ 154 Name: vmo.Spec.Grafana.Database.PasswordSecret, 155 }, 156 Key: constants.VMOSecretUsernameField, 157 }, 158 }, 159 }, 160 {Name: "GF_DATABASE_HOST", Value: vmo.Spec.Grafana.Database.Host}, 161 {Name: "GF_DATABASE_TYPE", Value: "mysql"}, 162 {Name: "GF_DATABASE_NAME", Value: vmo.Spec.Grafana.Database.Name}, 163 }...) 164 } 165 if vmo.Spec.URI != "" { 166 externalDomainName := config.Grafana.Name + "." + vmo.Spec.URI 167 deployment.Spec.Template.Spec.Containers[0].Env = append(deployment.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: "GF_SERVER_DOMAIN", Value: externalDomainName}) 168 deployment.Spec.Template.Spec.Containers[0].Env = append(deployment.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: "GF_SERVER_ROOT_URL", Value: "https://" + externalDomainName}) 169 } 170 // container will be restarted (per restart policy) if it fails the following liveness check: 171 deployment.Spec.Template.Spec.Containers[0].LivenessProbe.InitialDelaySeconds = 15 172 deployment.Spec.Template.Spec.Containers[0].LivenessProbe.TimeoutSeconds = 3 173 deployment.Spec.Template.Spec.Containers[0].LivenessProbe.PeriodSeconds = 20 174 175 // container will be removed from services if fails the following readiness check. 176 deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.InitialDelaySeconds = 5 177 deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.TimeoutSeconds = 3 178 deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.PeriodSeconds = 20 179 180 deployment.Spec.Template.Spec.Containers[0].ReadinessProbe = deployment.Spec.Template.Spec.Containers[0].LivenessProbe 181 182 // dashboard volume 183 volumes := []corev1.Volume{ 184 { 185 Name: "dashboards-volume", 186 VolumeSource: corev1.VolumeSource{ 187 ConfigMap: &corev1.ConfigMapVolumeSource{ 188 LocalObjectReference: corev1.LocalObjectReference{Name: vmo.Spec.Grafana.DashboardsConfigMap}, 189 }, 190 }, 191 }, 192 { 193 Name: "datasources-volume", 194 VolumeSource: corev1.VolumeSource{ 195 ConfigMap: &corev1.ConfigMapVolumeSource{ 196 LocalObjectReference: corev1.LocalObjectReference{Name: vmo.Spec.Grafana.DatasourcesConfigMap}, 197 }, 198 }, 199 }, 200 } 201 volumeMounts := []corev1.VolumeMount{ 202 { 203 Name: "dashboards-volume", 204 MountPath: "/etc/grafana/provisioning/dashboards", 205 }, 206 207 { 208 Name: "datasources-volume", 209 MountPath: "/etc/grafana/provisioning/datasources", 210 }, 211 } 212 deployment.Spec.Template.Spec.Containers[0].VolumeMounts = append(deployment.Spec.Template.Spec.Containers[0].VolumeMounts, volumeMounts...) 213 deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, volumes...) 214 215 // When the deployment does not have a pod security context with an FSGroup attribute, any mounted volumes are 216 // initially owned by root/root. Previous versions of the Grafana image were run as "root", and chown'd the mounted 217 // directory to "grafana", but we don't want to run as "root". The current Grafana image creates a group 218 // "grafana" (GID 472), and a user "grafana" (UID 472) in that group. When we provide FSGroup = 219 // 472 below, the volume is owned by root/grafana, with permissions "rwxrwsr-x". This allows the Grafana 220 // image to run as UID 472, and have sufficient permissions to write to the mounted volume. 221 grafanaGid := int64(472) 222 deployment.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{ 223 FSGroup: &grafanaGid, 224 } 225 deployments = append(deployments, deployment) 226 } 227 228 // API 229 if !config.API.Disabled { 230 deployment := createDeploymentElement(vmo, nil, nil, config.API, config.API.Name) 231 deployment.Spec.Template.Spec.Containers[0].ImagePullPolicy = config.API.ImagePullPolicy 232 deployment.Spec.Replicas = resources.NewVal(vmo.Spec.API.Replicas) 233 deployment.Spec.Template.Spec.Affinity = resources.CreateZoneAntiAffinityElement(vmo.Name, config.API.Name) 234 deployment.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{ 235 {Name: "VMI_NAME", Value: vmo.Name}, 236 {Name: "NAMESPACE", Value: vmo.Namespace}, 237 {Name: "ENV_NAME", Value: operatorConfig.EnvName}, 238 } 239 if len(vmo.Spec.NatGatewayIPs) > 0 { 240 deployment.Spec.Template.Spec.Containers[0].Args = []string{fmt.Sprintf("--natGatewayIPs=%s", strings.Join(vmo.Spec.NatGatewayIPs, ","))} 241 } 242 243 deployment.Spec.Template.Spec.Containers[0].LivenessProbe.InitialDelaySeconds = 15 244 deployment.Spec.Template.Spec.Containers[0].LivenessProbe.TimeoutSeconds = 3 245 deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.InitialDelaySeconds = 5 246 deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.TimeoutSeconds = 3 247 248 deployments = append(deployments, deployment) 249 } 250 251 expected.Deployments = deployments 252 return expected, err 253 } 254 255 func NewOpenSearchDashboardsDeployment(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance) *appsv1.Deployment { 256 var deployment *appsv1.Deployment 257 if vmo.Spec.Kibana.Enabled { 258 opensearchURL := fmt.Sprintf("http://%s%s-%s:%d/", constants.VMOServiceNamePrefix, vmo.Name, config.OpensearchIngest.Name, config.OpensearchIngest.Port) 259 260 deployment = createDeploymentElement(vmo, nil, &vmo.Spec.Kibana.Resources, config.OpenSearchDashboards, config.OpenSearchDashboards.Name) 261 deployment.Spec.Strategy = appsv1.DeploymentStrategy{ 262 Type: appsv1.RecreateDeploymentStrategyType, 263 } 264 deployment.Spec.Replicas = resources.NewVal(vmo.Spec.Kibana.Replicas) 265 deployment.Spec.Template.Spec.Affinity = resources.CreateZoneAntiAffinityElement(vmo.Name, config.OpenSearchDashboards.Name) 266 deployment.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{ 267 {Name: "OPENSEARCH_HOSTS", Value: opensearchURL}, 268 } 269 270 deployment.Spec.Template.Spec.Containers[0].LivenessProbe.InitialDelaySeconds = 120 271 deployment.Spec.Template.Spec.Containers[0].LivenessProbe.TimeoutSeconds = 3 272 deployment.Spec.Template.Spec.Containers[0].LivenessProbe.PeriodSeconds = 20 273 deployment.Spec.Template.Spec.Containers[0].LivenessProbe.FailureThreshold = 10 274 275 deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.InitialDelaySeconds = 15 276 deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.TimeoutSeconds = 3 277 deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.PeriodSeconds = 20 278 deployment.Spec.Template.Spec.Containers[0].ReadinessProbe.FailureThreshold = 5 279 280 // add the required istio annotations to allow inter-es component communication 281 if deployment.Spec.Template.Annotations == nil { 282 deployment.Spec.Template.Annotations = make(map[string]string) 283 } 284 deployment.Spec.Template.Annotations["traffic.sidecar.istio.io/includeOutboundPorts"] = fmt.Sprintf("%d", constants.OSHTTPPort) 285 // Adding command to install OS plugins at pod bootup 286 deployment.Spec.Template.Spec.Containers[0].Command = []string{ 287 "sh", 288 "-c", 289 fmt.Sprintf(resources.OpenSearchDashboardCmdTmpl, resources.GetOSPluginsInstallTmpl(resources.GetOSDashboardPluginList(vmo), resources.OSDashboardPluginsInstallCmd)), 290 } 291 } 292 293 return deployment 294 } 295 296 func createVolumeElement(pvcName string) corev1.Volume { 297 return corev1.Volume{ 298 Name: constants.StorageVolumeName, 299 VolumeSource: corev1.VolumeSource{ 300 PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 301 ClaimName: pvcName, 302 ReadOnly: false, 303 }, 304 }, 305 } 306 } 307 308 // Creates a deployment element for the given VMO and component. 309 func createDeploymentElement(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, vmoStorage *vmcontrollerv1.Storage, 310 vmoResources *vmcontrollerv1.Resources, componentDetails config.ComponentDetails, name string) *appsv1.Deployment { 311 return createDeploymentElementByPvcIndex(vmo, vmoStorage, vmoResources, componentDetails, -1, name) 312 } 313 314 // Creates a deployment element for the given VMO and component. A non-negative pvcIndex is used to indicate which 315 // PVC in the list of PVCs should be used for this particular deployment. 316 func createDeploymentElementByPvcIndex(vmo *vmcontrollerv1.VerrazzanoMonitoringInstance, vmoStorage *vmcontrollerv1.Storage, 317 vmoResources *vmcontrollerv1.Resources, componentDetails config.ComponentDetails, pvcIndex int, name string) *appsv1.Deployment { 318 319 labels := resources.GetSpecID(vmo.Name, componentDetails.Name) 320 var deploymentName string 321 if pvcIndex < 0 { 322 deploymentName = resources.GetMetaName(vmo.Name, name) 323 pvcIndex = 0 324 } else { 325 deploymentName = resources.GetMetaName(vmo.Name, fmt.Sprintf("%s-%d", name, pvcIndex)) 326 } 327 328 var volumes []corev1.Volume 329 if vmoStorage != nil && vmoStorage.PvcNames != nil && vmoStorage.Size != "" { 330 // Create volume element for this component, attaching to that component's current known PVC (if set) 331 volumes = append(volumes, createVolumeElement(vmoStorage.PvcNames[pvcIndex])) 332 labels["index"] = strconv.Itoa(pvcIndex) 333 } 334 335 resourceLabel := resources.GetMetaLabels(vmo) 336 resourceLabel[constants.ComponentLabel] = resources.GetCompLabel(componentDetails.Name) 337 podLabels := resources.DeepCopyMap(labels) 338 podLabels[constants.ComponentLabel] = resources.GetCompLabel(componentDetails.Name) 339 return &appsv1.Deployment{ 340 ObjectMeta: metav1.ObjectMeta{ 341 Labels: resourceLabel, 342 Name: deploymentName, 343 Namespace: vmo.Namespace, 344 OwnerReferences: resources.GetOwnerReferences(vmo), 345 }, 346 Spec: appsv1.DeploymentSpec{ 347 Replicas: resources.NewVal(1), 348 Selector: &metav1.LabelSelector{ 349 MatchLabels: labels, 350 }, 351 Template: corev1.PodTemplateSpec{ 352 ObjectMeta: metav1.ObjectMeta{ 353 Labels: podLabels, 354 }, 355 Spec: corev1.PodSpec{ 356 Volumes: volumes, 357 Containers: []corev1.Container{ 358 resources.CreateContainerElement(vmoStorage, vmoResources, componentDetails), 359 }, 360 ServiceAccountName: constants.ServiceAccountName, 361 TerminationGracePeriodSeconds: resources.New64Val(1), 362 }, 363 }, 364 }, 365 } 366 } 367 368 // Helper function that returns the AD name for the PVC at the given index in the given Storage element. Under any 369 // error condition, an empty string is returned. 370 func getAvailabilityDomainForPvcIndex(vmoStorage *vmcontrollerv1.Storage, pvcToAdMap map[string]string, pvcIndex int) string { 371 if vmoStorage == nil || pvcIndex > len(vmoStorage.PvcNames)-1 || pvcIndex < 0 { 372 return "" 373 } 374 if ad, ok := pvcToAdMap[vmoStorage.PvcNames[pvcIndex]]; ok { 375 return ad 376 } 377 return "" 378 }