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  }