...

Source file src/github.com/chaos-mesh/chaos-mesh/pkg/webhook/inject/inject.go

Documentation: github.com/chaos-mesh/chaos-mesh/pkg/webhook/inject

     1  // Copyright 2019 Chaos Mesh Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package inject
    15  
    16  import (
    17  	"context"
    18  	"encoding/json"
    19  	"fmt"
    20  	"strings"
    21  
    22  	"github.com/chaos-mesh/chaos-mesh/controllers/metrics"
    23  	"github.com/chaos-mesh/chaos-mesh/pkg/annotation"
    24  	controllerCfg "github.com/chaos-mesh/chaos-mesh/pkg/config"
    25  	podselector "github.com/chaos-mesh/chaos-mesh/pkg/selector/pod"
    26  	"github.com/chaos-mesh/chaos-mesh/pkg/webhook/config"
    27  
    28  	ctrl "sigs.k8s.io/controller-runtime"
    29  	"sigs.k8s.io/controller-runtime/pkg/client"
    30  
    31  	"k8s.io/api/admission/v1beta1"
    32  	corev1 "k8s.io/api/core/v1"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	"k8s.io/apimachinery/pkg/types"
    35  )
    36  
    37  var log = ctrl.Log.WithName("inject-webhook")
    38  
    39  var ignoredNamespaces = []string{
    40  	metav1.NamespaceSystem,
    41  	metav1.NamespacePublic,
    42  }
    43  
    44  const (
    45  	// StatusInjected is the annotation value for /status that indicates an injection was already performed on this pod
    46  	StatusInjected = "injected"
    47  )
    48  
    49  // Inject do pod template config inject
    50  func Inject(res *v1beta1.AdmissionRequest, cli client.Client, cfg *config.Config, controllerCfg *controllerCfg.ChaosControllerConfig, metrics *metrics.ChaosCollector) *v1beta1.AdmissionResponse {
    51  	var pod corev1.Pod
    52  	if err := json.Unmarshal(res.Object.Raw, &pod); err != nil {
    53  		log.Error(err, "Could not unmarshal raw object")
    54  		return &v1beta1.AdmissionResponse{
    55  			Result: &metav1.Status{
    56  				Message: err.Error(),
    57  			},
    58  		}
    59  	}
    60  
    61  	// Deal with potential empty fields, e.g., when the pod is created by a deployment
    62  	podName := potentialPodName(&pod.ObjectMeta)
    63  	if pod.ObjectMeta.Namespace == "" {
    64  		pod.ObjectMeta.Namespace = res.Namespace
    65  	}
    66  
    67  	log.Info("AdmissionReview for",
    68  		"Kind", res.Kind, "Namespace", res.Namespace, "Name", res.Name, "podName", podName, "UID", res.UID, "patchOperation", res.Operation, "UserInfo", res.UserInfo)
    69  	log.V(4).Info("Object", "Object", string(res.Object.Raw))
    70  	log.V(4).Info("OldObject", "OldObject", string(res.OldObject.Raw))
    71  	log.V(4).Info("Pod", "Pod", pod)
    72  
    73  	requiredKey, ok := injectRequired(&pod.ObjectMeta, cli, cfg, controllerCfg)
    74  	if !ok {
    75  		log.Info("Skipping injection due to policy check", "namespace", pod.ObjectMeta.Namespace, "name", podName)
    76  		return &v1beta1.AdmissionResponse{
    77  			Allowed: true,
    78  		}
    79  	}
    80  
    81  	if metrics != nil {
    82  		metrics.InjectRequired.WithLabelValues(res.Namespace, requiredKey).Inc()
    83  	}
    84  	injectionConfig, err := cfg.GetRequestedConfig(pod.Namespace, requiredKey)
    85  	if err != nil {
    86  		log.Error(err, "Error getting injection config, permitting launch of pod with no sidecar injected", "injectionConfig",
    87  			injectionConfig)
    88  		// dont prevent pods from launching! just return allowed
    89  		return &v1beta1.AdmissionResponse{
    90  			Allowed: true,
    91  		}
    92  	}
    93  
    94  	if injectionConfig.Selector != nil {
    95  		meet, err := podselector.CheckPodMeetSelector(pod, *injectionConfig.Selector)
    96  		if err != nil {
    97  			log.Error(err, "Failed to check pod selector", "namespace", pod.Namespace)
    98  			return &v1beta1.AdmissionResponse{
    99  				Allowed: true,
   100  			}
   101  		}
   102  
   103  		if !meet {
   104  			log.Info("Skipping injection, this pod does not meet the selection criteria",
   105  				"namespace", pod.Namespace, "name", pod.Name)
   106  			return &v1beta1.AdmissionResponse{
   107  				Allowed: true,
   108  			}
   109  		}
   110  	}
   111  
   112  	annotations := map[string]string{cfg.StatusAnnotationKey(): StatusInjected}
   113  
   114  	patchBytes, err := createPatch(&pod, injectionConfig, annotations)
   115  	if err != nil {
   116  		return &v1beta1.AdmissionResponse{
   117  			Result: &metav1.Status{
   118  				Message: err.Error(),
   119  			},
   120  		}
   121  	}
   122  
   123  	log.Info("AdmissionResponse: patch", "patchBytes", string(patchBytes))
   124  	if metrics != nil {
   125  		metrics.Injections.WithLabelValues(res.Namespace, requiredKey).Inc()
   126  	}
   127  	return &v1beta1.AdmissionResponse{
   128  		Allowed: true,
   129  		Patch:   patchBytes,
   130  		PatchType: func() *v1beta1.PatchType {
   131  			pt := v1beta1.PatchTypeJSONPatch
   132  			return &pt
   133  		}(),
   134  	}
   135  }
   136  
   137  // Check whether the target resource need to be injected and return the required config name
   138  func injectRequired(metadata *metav1.ObjectMeta, cli client.Client, cfg *config.Config, controllerCfg *controllerCfg.ChaosControllerConfig) (string, bool) {
   139  	// skip special kubernetes system namespaces
   140  	for _, namespace := range ignoredNamespaces {
   141  		if metadata.Namespace == namespace {
   142  			log.Info("Skip mutation for it' in special namespace", "name", metadata.Name, "namespace", metadata.Namespace)
   143  			return "", false
   144  		}
   145  	}
   146  
   147  	if controllerCfg.EnableFilterNamespace {
   148  		ok, err := podselector.IsAllowedNamespaces(context.Background(), cli, metadata.Namespace)
   149  		if err != nil {
   150  			log.Error(err, "fail to check whether this namespace should be injected", "namespace", metadata.Namespace)
   151  		}
   152  
   153  		if !ok {
   154  			log.Info("Skip mutation for it' in special namespace", "name", metadata.Name, "namespace", metadata.Namespace)
   155  			return "", false
   156  		}
   157  	}
   158  
   159  	log.V(4).Info("meta", "meta", metadata)
   160  
   161  	if checkInjectStatus(metadata, cfg) {
   162  		log.Info("Pod annotation indicates injection already satisfied, skipping",
   163  			"namespace", metadata.Namespace, "name", metadata.Name,
   164  			"annotationKey", cfg.StatusAnnotationKey(), "value", StatusInjected)
   165  		return "", false
   166  	}
   167  
   168  	requiredConfig, ok := injectByPodRequired(metadata, cfg)
   169  	if ok {
   170  		log.Info("Pod annotation requesting sidecar config",
   171  			"namespace", metadata.Namespace, "name", metadata.Name,
   172  			"annotation", cfg.RequestAnnotationKey(), "requiredConfig", requiredConfig)
   173  		return requiredConfig, true
   174  	}
   175  
   176  	requiredConfig, ok = injectByNamespaceRequired(metadata, cli, cfg)
   177  	if ok {
   178  		log.Info("Pod annotation requesting sidecar config",
   179  			"namespace", metadata.Namespace, "name", metadata.Name,
   180  			"annotation", cfg.RequestAnnotationKey(), "requiredConfig", requiredConfig)
   181  		return requiredConfig, true
   182  	}
   183  
   184  	requiredConfig, ok = injectByNamespaceInitRequired(metadata, cli, cfg)
   185  	if ok {
   186  		log.Info("Pod annotation init requesting sidecar config",
   187  			"namespace", metadata.Namespace, "name", metadata.Name,
   188  			"annotation", cfg.RequestAnnotationKey(), "requiredConfig", requiredConfig)
   189  		return requiredConfig, true
   190  	}
   191  
   192  	return "", false
   193  }
   194  
   195  func checkInjectStatus(metadata *metav1.ObjectMeta, cfg *config.Config) bool {
   196  	annotations := metadata.GetAnnotations()
   197  	if annotations == nil {
   198  		annotations = make(map[string]string)
   199  	}
   200  
   201  	status, ok := annotations[cfg.StatusAnnotationKey()]
   202  	if ok && strings.ToLower(status) == StatusInjected {
   203  		return true
   204  	}
   205  
   206  	return false
   207  }
   208  
   209  func injectByNamespaceRequired(metadata *metav1.ObjectMeta, cli client.Client, cfg *config.Config) (string, bool) {
   210  	var ns corev1.Namespace
   211  	if err := cli.Get(context.Background(), types.NamespacedName{Name: metadata.Namespace}, &ns); err != nil {
   212  		log.Error(err, "failed to get namespace", "namespace", metadata.Namespace)
   213  		return "", false
   214  	}
   215  	annotations := ns.GetAnnotations()
   216  	if annotations == nil {
   217  		annotations = make(map[string]string)
   218  	}
   219  
   220  	required, ok := annotations[annotation.GenKeyForWebhook(cfg.RequestAnnotationKey(), metadata.Name)]
   221  	if !ok {
   222  		log.Info("Pod annotation by namespace is missing, skipping injection",
   223  			"namespace", metadata.Namespace, "pod", metadata.Name, "config", required)
   224  		return "", false
   225  	}
   226  
   227  	log.Info("Get sidecar config from namespace annotations",
   228  		"namespace", metadata.Namespace, "pod", metadata.Name, "config", required)
   229  	return strings.ToLower(required), true
   230  }
   231  
   232  func injectByNamespaceInitRequired(metadata *metav1.ObjectMeta, cli client.Client, cfg *config.Config) (string, bool) {
   233  	var ns corev1.Namespace
   234  	if err := cli.Get(context.Background(), types.NamespacedName{Name: metadata.Namespace}, &ns); err != nil {
   235  		log.Error(err, "failed to get namespace", "namespace", metadata.Namespace)
   236  		return "", false
   237  	}
   238  
   239  	annotations := ns.GetAnnotations()
   240  	if annotations == nil {
   241  		annotations = make(map[string]string)
   242  	}
   243  
   244  	required, ok := annotations[cfg.RequestInitAnnotationKey()]
   245  	if !ok {
   246  		log.Info("Pod annotation by namespace is missing, skipping injection",
   247  			"namespace", metadata.Namespace, "pod", metadata.Name, "config", required)
   248  		return "", false
   249  	}
   250  
   251  	log.Info("Get sidecar config from namespace annotations",
   252  		"namespace", metadata.Namespace, "pod", metadata.Name, "config", required)
   253  	return strings.ToLower(required), true
   254  }
   255  
   256  func injectByPodRequired(metadata *metav1.ObjectMeta, cfg *config.Config) (string, bool) {
   257  	annotations := metadata.GetAnnotations()
   258  	if annotations == nil {
   259  		annotations = make(map[string]string)
   260  	}
   261  
   262  	required, ok := annotations[cfg.RequestAnnotationKey()]
   263  	if !ok {
   264  		log.Info("Pod annotation is missing, skipping injection",
   265  			"namespace", metadata.Namespace, "name", metadata.Name, "annotation", cfg.RequestAnnotationKey())
   266  		return "", false
   267  	}
   268  
   269  	log.Info("Get sidecar config from pod annotations",
   270  		"namespace", metadata.Namespace, "pod", metadata.Name, "config", required)
   271  	return strings.ToLower(required), true
   272  }
   273  
   274  // create mutation patch for resource
   275  func createPatch(pod *corev1.Pod, inj *config.InjectionConfig, annotations map[string]string) ([]byte, error) {
   276  	var patch []patchOperation
   277  
   278  	// make sure any injected containers in our config get the EnvVars and VolumeMounts injected
   279  	// this mutates inj.Containers with our environment vars
   280  	mutatedInjectedContainers := mergeEnvVars(inj.Environment, inj.Containers)
   281  	mutatedInjectedContainers = mergeVolumeMounts(inj.VolumeMounts, mutatedInjectedContainers)
   282  
   283  	// make sure any injected init containers in our config get the EnvVars and VolumeMounts injected
   284  	// this mutates inj.InitContainers with our environment vars
   285  	mutatedInjectedInitContainers := mergeEnvVars(inj.Environment, inj.InitContainers)
   286  	mutatedInjectedInitContainers = mergeVolumeMounts(inj.VolumeMounts, mutatedInjectedInitContainers)
   287  
   288  	// patch all existing containers with the env vars and volume mounts
   289  	patch = append(patch, setVolumeMounts(pod.Spec.Containers, inj.VolumeMounts, "/spec/containers")...)
   290  	// TODO: fix set env
   291  	// setEnvironment may not work, because we replace the whole container in `setVolumeMounts`
   292  	patch = append(patch, setEnvironment(pod.Spec.Containers, inj.Environment)...)
   293  
   294  	// patch containers with our injected containers
   295  	patch = append(patch, addContainers(pod.Spec.Containers, mutatedInjectedContainers, "/spec/containers")...)
   296  
   297  	// add initContainers, hostAliases and volumes
   298  	patch = append(patch, addContainers(pod.Spec.InitContainers, mutatedInjectedInitContainers, "/spec/initContainers")...)
   299  	patch = append(patch, addHostAliases(pod.Spec.HostAliases, inj.HostAliases, "/spec/hostAliases")...)
   300  	patch = append(patch, addVolumes(pod.Spec.Volumes, inj.Volumes, "/spec/volumes")...)
   301  
   302  	// set annotations
   303  	patch = append(patch, updateAnnotations(pod.Annotations, annotations)...)
   304  
   305  	// set shareProcessNamespace
   306  	patch = append(patch, updateShareProcessNamespace(inj.ShareProcessNamespace)...)
   307  
   308  	// TODO: remove injecting commands when sidecar container supported
   309  	// set commands and args
   310  	patch = append(patch, setCommands(pod.Spec.Containers, inj.PostStart)...)
   311  
   312  	return json.Marshal(patch)
   313  }
   314  
   315  func setCommands(target []corev1.Container, postStart map[string]config.ExecAction) (patch []patchOperation) {
   316  	if postStart == nil {
   317  		return
   318  	}
   319  
   320  	for containerIndex, container := range target {
   321  		execCmd, ok := postStart[container.Name]
   322  		if !ok {
   323  			continue
   324  		}
   325  
   326  		path := fmt.Sprintf("/spec/containers/%d/command", containerIndex)
   327  
   328  		commands := MergeCommands(execCmd.Command, container.Command, container.Args)
   329  
   330  		log.Info("Inject command", "command", commands)
   331  
   332  		patch = append(patch, patchOperation{
   333  			Op:    "replace",
   334  			Path:  path,
   335  			Value: commands,
   336  		})
   337  
   338  		argsPath := fmt.Sprintf("/spec/containers/%d/args", containerIndex)
   339  		patch = append(patch, patchOperation{
   340  			Op:    "replace",
   341  			Path:  argsPath,
   342  			Value: []string{},
   343  		})
   344  	}
   345  	return patch
   346  }
   347  
   348  type patchOperation struct {
   349  	Op    string      `json:"op"`
   350  	Path  string      `json:"path"`
   351  	Value interface{} `json:"value,omitempty"`
   352  }
   353  
   354  func setEnvironment(target []corev1.Container, addedEnv []corev1.EnvVar) (patch []patchOperation) {
   355  	var value interface{}
   356  	for containerIndex, container := range target {
   357  		// for each container in the spec, determine if we want to patch with any env vars
   358  		first := len(container.Env) == 0
   359  		for _, add := range addedEnv {
   360  			path := fmt.Sprintf("/spec/containers/%d/env", containerIndex)
   361  			hasKey := false
   362  			// make sure we dont override any existing env vars; we only add, dont replace
   363  			for _, origEnv := range container.Env {
   364  				if origEnv.Name == add.Name {
   365  					hasKey = true
   366  					break
   367  				}
   368  			}
   369  			if !hasKey {
   370  				// make a patch
   371  				value = add
   372  				if first {
   373  					first = false
   374  					value = []corev1.EnvVar{add}
   375  				} else {
   376  					path = path + "/-"
   377  				}
   378  				patch = append(patch, patchOperation{
   379  					Op:    "add",
   380  					Path:  path,
   381  					Value: value,
   382  				})
   383  			}
   384  		}
   385  	}
   386  
   387  	return patch
   388  }
   389  
   390  func addContainers(target, added []corev1.Container, basePath string) (patch []patchOperation) {
   391  	first := len(target) == 0
   392  	var value interface{}
   393  	for _, add := range added {
   394  		value = add
   395  		log.V(6).Info("Add container", "add", add)
   396  		path := basePath
   397  		if first {
   398  			first = false
   399  			value = []corev1.Container{add}
   400  		} else {
   401  			path = path + "/-"
   402  		}
   403  		patch = append(patch, patchOperation{
   404  			Op:    "add",
   405  			Path:  path,
   406  			Value: value,
   407  		})
   408  	}
   409  	return patch
   410  }
   411  
   412  func addVolumes(target, added []corev1.Volume, basePath string) (patch []patchOperation) {
   413  	first := len(target) == 0
   414  	var value interface{}
   415  	for _, add := range added {
   416  		value = add
   417  		path := basePath
   418  		if first {
   419  			first = false
   420  			value = []corev1.Volume{add}
   421  		} else {
   422  			path = path + "/-"
   423  		}
   424  		patch = append(patch, patchOperation{
   425  			Op:    "add",
   426  			Path:  path,
   427  			Value: value,
   428  		})
   429  	}
   430  	return patch
   431  }
   432  
   433  func setVolumeMounts(target []corev1.Container, addedVolumeMounts []corev1.VolumeMount, basePath string) (patch []patchOperation) {
   434  	for index, c := range target {
   435  		volumeMounts := map[string]corev1.VolumeMount{}
   436  		for _, vm := range c.VolumeMounts {
   437  			volumeMounts[vm.Name] = vm
   438  		}
   439  		for _, added := range addedVolumeMounts {
   440  			log.Info("volumeMount", "add", added)
   441  			volumeMounts[added.Name] = added
   442  		}
   443  
   444  		vs := []corev1.VolumeMount{}
   445  		for _, vm := range volumeMounts {
   446  			vs = append(vs, vm)
   447  		}
   448  		target[index].VolumeMounts = vs
   449  	}
   450  
   451  	patch = append(patch, patchOperation{
   452  		Op:    "replace",
   453  		Path:  basePath,
   454  		Value: target,
   455  	})
   456  
   457  	return patch
   458  }
   459  
   460  func addHostAliases(target, added []corev1.HostAlias, basePath string) (patch []patchOperation) {
   461  	first := len(target) == 0
   462  	var value interface{}
   463  	for _, add := range added {
   464  		value = add
   465  		path := basePath
   466  		if first {
   467  			first = false
   468  			value = []corev1.HostAlias{add}
   469  		} else {
   470  			path = path + "/-"
   471  		}
   472  		patch = append(patch, patchOperation{
   473  			Op:    "add",
   474  			Path:  path,
   475  			Value: value,
   476  		})
   477  	}
   478  	return patch
   479  }
   480  
   481  // for containers, add any env vars that are not already defined in the Env list.
   482  // this does _not_ return patches; this is intended to be used only on containers defined
   483  // in the injection config, so the resources do not exist yet in the k8s api (thus no patch needed)
   484  func mergeEnvVars(envs []corev1.EnvVar, containers []corev1.Container) []corev1.Container {
   485  	mutatedContainers := []corev1.Container{}
   486  	for _, c := range containers {
   487  		for _, newEnv := range envs {
   488  			// check each container for each env var by name.
   489  			// if the container has a matching name, dont override!
   490  			skip := false
   491  			for _, origEnv := range c.Env {
   492  				if origEnv.Name == newEnv.Name {
   493  					skip = true
   494  					break
   495  				}
   496  			}
   497  			if !skip {
   498  				c.Env = append(c.Env, newEnv)
   499  			}
   500  		}
   501  		mutatedContainers = append(mutatedContainers, c)
   502  	}
   503  	return mutatedContainers
   504  }
   505  
   506  func mergeVolumeMounts(volumeMounts []corev1.VolumeMount, containers []corev1.Container) []corev1.Container {
   507  	mutatedContainers := []corev1.Container{}
   508  	for _, c := range containers {
   509  		for _, newVolumeMount := range volumeMounts {
   510  			// check each container for each volume mount by name.
   511  			// if the container has a matching name, dont override!
   512  			skip := false
   513  			for _, origVolumeMount := range c.VolumeMounts {
   514  				if origVolumeMount.Name == newVolumeMount.Name {
   515  					skip = true
   516  					break
   517  				}
   518  			}
   519  			if !skip {
   520  				c.VolumeMounts = append(c.VolumeMounts, newVolumeMount)
   521  			}
   522  		}
   523  		mutatedContainers = append(mutatedContainers, c)
   524  	}
   525  	return mutatedContainers
   526  }
   527  
   528  func updateAnnotations(target map[string]string, added map[string]string) (patch []patchOperation) {
   529  	for key, value := range added {
   530  		if target == nil || target[key] == "" {
   531  			target = map[string]string{}
   532  			patch = append(patch, patchOperation{
   533  				Op:   "add",
   534  				Path: "/metadata/annotations",
   535  				Value: map[string]string{
   536  					key: value,
   537  				},
   538  			})
   539  		} else {
   540  			patch = append(patch, patchOperation{
   541  				Op:    "replace",
   542  				Path:  "/metadata/annotations/" + key,
   543  				Value: value,
   544  			})
   545  		}
   546  	}
   547  	return patch
   548  }
   549  
   550  func updateShareProcessNamespace(value bool) (patch []patchOperation) {
   551  	op := "add"
   552  	patch = append(patch, patchOperation{
   553  		Op:    op,
   554  		Path:  "/spec/shareProcessNamespace",
   555  		Value: value,
   556  	})
   557  	return patch
   558  }
   559  
   560  func potentialPodName(metadata *metav1.ObjectMeta) string {
   561  	if metadata.Name != "" {
   562  		return metadata.Name
   563  	}
   564  	if metadata.GenerateName != "" {
   565  		return metadata.GenerateName + "***** (actual name not yet known)"
   566  	}
   567  	return ""
   568  }
   569