patch.go (214 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package main import ( "fmt" "path/filepath" "strconv" "strings" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/validation" ) // patchOperation is an operation of a JSON patch, see // https://tools.ietf.org/html/rfc6902. type patchOperation struct { Op string `json:"op"` Path string `json:"path"` Value interface{} `json:"value,omitempty"` } const ( volumeName = "elastic-apm-agent" mountPath = "/elastic/apm/agent" ) var ( volumeMounts = corev1.VolumeMount{ Name: volumeName, MountPath: mountPath, } agentVolume = corev1.Volume{ Name: volumeName, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, } kubernetesEnvironmentVariables = map[string]string{ "KUBERNETES_NODE_NAME": "spec.nodeName", "KUBERNETES_POD_NAME": "metadata.name", "KUBERNETES_NAMESPACE": "metadata.namespace", "KUBERNETES_POD_UID": "metadata.uid", } ) func createPatch(config agentConfig, spec corev1.PodSpec) ([]patchOperation, error) { // Create patch operations var patches []patchOperation envVariables, err := generateEnvironmentVariables(config) if err != nil { return nil, err } // Add a volume mount to the pod patches = append(patches, createVolumePatch(spec.Volumes == nil)) // Add an init container, that will fetch the agent Docker image and // extract the agent jar to the agent volume patch, err := createInitContainerPatch(config, spec.InitContainers == nil) if err != nil { return nil, err } patches = append(patches, patch) // Add agent env variables for each container at the pod, as well as // the volume mount containers := spec.Containers for index, container := range containers { // Filter out environment variables from the agent config to // not overwrite environment variables set directly on the // container. envVars := uniqueEnvironmentVariables(envVariables, container.Env) patches = append(patches, createVolumeMountsPatch(container.VolumeMounts == nil, index)) patches = append(patches, createEnvVariablesPatches(envVars, container.Env == nil, index)...) } return patches, nil } func uniqueEnvironmentVariables(configEnvironmentVariables, containerEnvironmentVariables []corev1.EnvVar) []corev1.EnvVar { if len(containerEnvironmentVariables) == 0 { return configEnvironmentVariables } unique := make([]corev1.EnvVar, 0, len(configEnvironmentVariables)) containerKeys := make(map[string]struct{}, len(containerEnvironmentVariables)) for _, v := range containerEnvironmentVariables { containerKeys[v.Name] = struct{}{} } for _, v := range configEnvironmentVariables { if _, ok := containerKeys[v.Name]; ok { continue } unique = append(unique, v) } return unique } func generateEnvironmentVariables(config agentConfig) ([]corev1.EnvVar, error) { optional := true vars := []corev1.EnvVar{{ Name: "ELASTIC_APM_SECRET_TOKEN", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{Name: "apm-agent-auth"}, Key: "secret_token", Optional: &optional, }, }, }, { Name: "ELASTIC_APM_API_KEY", ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{Name: "apm-agent-auth"}, Key: "api_key", Optional: &optional, }, }, }} if _, ok := config.Environment["ELASTIC_APM_SERVER_URL"]; !ok { // No apm-server url present, inject the local node address. vars = append(vars, corev1.EnvVar{ Name: "HOST_IP", ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{ FieldPath: "status.hostIP", }, }, }, corev1.EnvVar{ Name: "ELASTIC_APM_SERVER_URL", Value: "http://$(HOST_IP):8200", }) } if _, ok := config.Environment["ELASTIC_APM_ACTIVATION_METHOD"]; !ok { vars = append(vars, corev1.EnvVar{ Name: "ELASTIC_APM_ACTIVATION_METHOD", Value: "K8S_ATTACH", }) } for k, v := range kubernetesEnvironmentVariables { vars = append(vars, corev1.EnvVar{ Name: k, ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{ FieldPath: v, }, }, }) } for name, value := range config.Environment { vars = append(vars, corev1.EnvVar{Name: name, Value: value}) } for _, ev := range vars { if errs := validation.IsEnvVarName(ev.Name); len(errs) != 0 { return nil, fmt.Errorf("failed to validate environment variable %q: %v", ev.Name, errs) } } return vars, nil } func createVolumePatch(createArray bool) patchOperation { if createArray { return patchOperation{ Op: "add", Path: "/spec/volumes", Value: []corev1.Volume{agentVolume}, } } return patchOperation{ Op: "add", Path: "/spec/volumes/-", Value: agentVolume, } } func createInitContainerPatch(config agentConfig, createArray bool) (patchOperation, error) { bp := filepath.Base(config.Image) name := strings.Split(bp, ":") allowPrivilegeEscalation := false agentInitContainer := corev1.Container{ Name: name[0], Image: config.Image, VolumeMounts: []corev1.VolumeMount{volumeMounts}, // TODO: should this be a default, and then users can modify it // *if needed*? Command: []string{"cp", "-v", "-r", config.ArtifactPath, mountPath}, SecurityContext: &corev1.SecurityContext{ AllowPrivilegeEscalation: &allowPrivilegeEscalation, Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{"ALL"}, }, }, } if errs := validation.IsDNS1123Label(agentInitContainer.Name); len(errs) != 0 { return patchOperation{}, fmt.Errorf("failed to extract container name from image (%s): init container name (%s) is not a valid DNS_LABEL: %v", config.Image, agentInitContainer.Name, errs, ) } if createArray { return patchOperation{ Op: "add", Path: "/spec/initContainers", Value: []corev1.Container{agentInitContainer}, }, nil } return patchOperation{ Op: "add", Path: "/spec/initContainers/-", Value: agentInitContainer, }, nil } // If the env variable array does not already exist, this method will return a // single patch operation for the addition of the entire list, otherwise it // would return a list of patches for each env variable func createEnvVariablesPatches(envVariables []corev1.EnvVar, createArray bool, index int) []patchOperation { containerIndex := strconv.Itoa(index) envVariablesPath := "/spec/containers/" + containerIndex + "/env" if createArray { return []patchOperation{{ Op: "add", Path: envVariablesPath, Value: envVariables, }} } patches := make([]patchOperation, len(envVariables)) envVariablesPath += "/-" for i, variable := range envVariables { patches[i] = patchOperation{ Op: "add", Path: envVariablesPath, Value: variable, } } return patches } func createVolumeMountsPatch(createArray bool, index int) patchOperation { containerIndex := strconv.Itoa(index) volumeMountsPath := "/spec/containers/" + containerIndex + "/volumeMounts" if createArray { return patchOperation{ Op: "add", Path: volumeMountsPath, Value: []corev1.VolumeMount{volumeMounts}, } } return patchOperation{ Op: "add", Path: volumeMountsPath + "/-", Value: volumeMounts, } }