...

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