istio.io/istio@v0.0.0-20240520182934-d79c90f27776/cni/pkg/plugin/plugin.go (about)

     1  // Copyright Istio 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  // This is a sample chained plugin that supports multiple CNI versions. It
    16  // parses prevResult according to the cniVersion
    17  package plugin
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"runtime/debug"
    24  	"strconv"
    25  	"time"
    26  
    27  	"github.com/containernetworking/cni/pkg/skel"
    28  	"github.com/containernetworking/cni/pkg/types"
    29  	cniv1 "github.com/containernetworking/cni/pkg/types/100"
    30  	"github.com/containernetworking/cni/pkg/version"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/client-go/kubernetes"
    33  
    34  	"istio.io/api/annotation"
    35  	"istio.io/api/label"
    36  	"istio.io/istio/cni/pkg/constants"
    37  	"istio.io/istio/cni/pkg/util"
    38  	"istio.io/istio/pkg/log"
    39  	"istio.io/istio/pkg/util/sets"
    40  )
    41  
    42  var (
    43  	injectAnnotationKey = annotation.SidecarInject.Name
    44  	sidecarStatusKey    = annotation.SidecarStatus.Name
    45  
    46  	podRetrievalMaxRetries = 30
    47  	podRetrievalInterval   = 1 * time.Second
    48  )
    49  
    50  const (
    51  	ISTIOINIT  = "istio-init"
    52  	ISTIOPROXY = "istio-proxy"
    53  )
    54  
    55  // Kubernetes a K8s specific struct to hold config
    56  type Kubernetes struct {
    57  	Kubeconfig        string   `json:"kubeconfig"`
    58  	ExcludeNamespaces []string `json:"exclude_namespaces"`
    59  }
    60  
    61  // Config is whatever you expect your configuration json to be. This is whatever
    62  // is passed in on stdin. Your plugin may wish to expose its functionality via
    63  // runtime args, see CONVENTIONS.md in the CNI spec.
    64  type Config struct {
    65  	types.NetConf
    66  
    67  	// Add plugin-specific flags here
    68  	LogLevel        string     `json:"log_level"`
    69  	LogUDSAddress   string     `json:"log_uds_address"`
    70  	CNIEventAddress string     `json:"cni_event_address"`
    71  	AmbientEnabled  bool       `json:"ambient_enabled"`
    72  	Kubernetes      Kubernetes `json:"kubernetes"`
    73  }
    74  
    75  // K8sArgs is the valid CNI_ARGS used for Kubernetes
    76  // The field names need to match exact keys in containerd args for unmarshalling
    77  // https://github.com/containerd/containerd/blob/ced9b18c231a28990617bc0a4b8ce2e81ee2ffa1/pkg/cri/server/sandbox_run.go#L526-L532
    78  type K8sArgs struct {
    79  	types.CommonArgs
    80  	K8S_POD_NAME               types.UnmarshallableString // nolint: revive, stylecheck
    81  	K8S_POD_NAMESPACE          types.UnmarshallableString // nolint: revive, stylecheck
    82  	K8S_POD_INFRA_CONTAINER_ID types.UnmarshallableString // nolint: revive, stylecheck
    83  }
    84  
    85  // parseConfig parses the supplied configuration (and prevResult) from stdin.
    86  func parseConfig(stdin []byte) (*Config, error) {
    87  	conf := Config{}
    88  
    89  	if err := json.Unmarshal(stdin, &conf); err != nil {
    90  		return nil, fmt.Errorf("failed to parse network configuration: %v", err)
    91  	}
    92  
    93  	log.Debugf("istio-cni: Config is: %+v", conf)
    94  	// Parse previous result. Remove this if your plugin is not chained.
    95  	if conf.RawPrevResult != nil {
    96  		resultBytes, err := json.Marshal(conf.RawPrevResult)
    97  		if err != nil {
    98  			return nil, fmt.Errorf("could not serialize prevResult: %v", err)
    99  		}
   100  		res, err := version.NewResult(conf.CNIVersion, resultBytes)
   101  		if err != nil {
   102  			return nil, fmt.Errorf("could not parse prevResult: %v", err)
   103  		}
   104  		conf.RawPrevResult = nil
   105  		conf.PrevResult, err = cniv1.NewResultFromResult(res)
   106  		if err != nil {
   107  			return nil, fmt.Errorf("could not convert result to current version: %v", err)
   108  		}
   109  	}
   110  	// End previous result parsing
   111  
   112  	return &conf, nil
   113  }
   114  
   115  func getLogLevel(logLevel string) log.Level {
   116  	switch logLevel {
   117  	case "debug":
   118  		return log.DebugLevel
   119  	case "warn":
   120  		return log.WarnLevel
   121  	case "error":
   122  		return log.ErrorLevel
   123  	case "info":
   124  		return log.InfoLevel
   125  	}
   126  	return log.InfoLevel
   127  }
   128  
   129  func GetLoggingOptions(udsAddress string) *log.Options {
   130  	loggingOptions := log.DefaultOptions()
   131  	loggingOptions.OutputPaths = []string{"stderr"}
   132  	loggingOptions.JSONEncoding = true
   133  	if udsAddress != "" {
   134  		loggingOptions.WithTeeToUDS(udsAddress, constants.UDSLogPath)
   135  	}
   136  	return loggingOptions
   137  }
   138  
   139  // CmdAdd is called for ADD requests
   140  func CmdAdd(args *skel.CmdArgs) (err error) {
   141  	// Defer a panic recover, so that in case if panic we can still return
   142  	// a proper error to the runtime.
   143  	defer func() {
   144  		if e := recover(); e != nil {
   145  			msg := fmt.Sprintf("istio-cni panicked during cmdAdd: %v\n%v", e, string(debug.Stack()))
   146  			if err != nil {
   147  				// If we're recovering and there was also an error, then we need to
   148  				// present both.
   149  				msg = fmt.Sprintf("%s: %v", msg, err)
   150  			}
   151  			err = fmt.Errorf(msg)
   152  		}
   153  		if err != nil {
   154  			log.Errorf("istio-cni cmdAdd error: %v", err)
   155  		}
   156  	}()
   157  
   158  	conf, err := parseConfig(args.StdinData)
   159  	if err != nil {
   160  		log.Errorf("istio-cni cmdAdd failed to parse config %v %v", string(args.StdinData), err)
   161  		return err
   162  	}
   163  
   164  	// Create a kube client
   165  	client, err := newK8sClient(*conf)
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	// Actually do the add
   171  	if err := doAddRun(args, conf, client, IptablesInterceptRuleMgr()); err != nil {
   172  		return err
   173  	}
   174  	return pluginResponse(conf)
   175  }
   176  
   177  func doAddRun(args *skel.CmdArgs, conf *Config, kClient kubernetes.Interface, rulesMgr InterceptRuleMgr) error {
   178  	setupLogging(conf)
   179  
   180  	var loggedPrevResult any
   181  	if conf.PrevResult == nil {
   182  		loggedPrevResult = "none"
   183  	} else {
   184  		loggedPrevResult = conf.PrevResult
   185  	}
   186  	log.WithLabels("if", args.IfName).Debugf("istio-cni CmdAdd config: %+v", conf)
   187  	log.Debugf("istio-cni CmdAdd previous result: %+v", loggedPrevResult)
   188  
   189  	// Determine if running under k8s by checking the CNI args
   190  	k8sArgs := K8sArgs{}
   191  	if err := types.LoadArgs(args.Args, &k8sArgs); err != nil {
   192  		return err
   193  	}
   194  
   195  	// Check if the workload is running under Kubernetes.
   196  	podNamespace := string(k8sArgs.K8S_POD_NAMESPACE)
   197  	podName := string(k8sArgs.K8S_POD_NAME)
   198  	log := log.WithLabels("pod", podNamespace+"/"+podName)
   199  	if podNamespace == "" || podName == "" {
   200  		log.Debugf("Not a kubernetes pod")
   201  		return nil
   202  	}
   203  
   204  	for _, excludeNs := range conf.Kubernetes.ExcludeNamespaces {
   205  		if podNamespace == excludeNs {
   206  			log.Infof("pod namespace excluded")
   207  			return nil
   208  		}
   209  	}
   210  
   211  	// Begin ambient plugin logic
   212  	// For ambient pods, this is all the logic we need to run
   213  	if conf.AmbientEnabled {
   214  		log.Debugf("istio-cni ambient cmdAdd podName: %s - checking if ambient enabled", podName)
   215  		podIsAmbient, err := isAmbientPod(kClient, podName, podNamespace)
   216  		if err != nil {
   217  			log.Errorf("istio-cni cmdAdd failed to check ambient: %s", err)
   218  		}
   219  
   220  		var prevResIps []*cniv1.IPConfig
   221  		if conf.PrevResult != nil {
   222  			prevResult := conf.PrevResult.(*cniv1.Result)
   223  			prevResIps = prevResult.IPs
   224  		}
   225  
   226  		// Only send event if this pod "would be" an ambient-watched pod - otherwise skip
   227  		if podIsAmbient {
   228  			cniClient := newCNIClient(conf.CNIEventAddress, constants.CNIAddEventPath)
   229  			if err = PushCNIEvent(cniClient, args, prevResIps, podName, podNamespace); err != nil {
   230  				log.Errorf("istio-cni cmdAdd failed to signal node Istio CNI agent: %s", err)
   231  				return err
   232  			}
   233  			return nil
   234  		}
   235  		log.Debugf("istio-cni ambient cmdAdd podName: %s - not ambient enabled, ignoring", podName)
   236  	}
   237  	// End ambient plugin logic
   238  
   239  	pi := &PodInfo{}
   240  	var k8sErr error
   241  	for attempt := 1; attempt <= podRetrievalMaxRetries; attempt++ {
   242  		pi, k8sErr = getK8sPodInfo(kClient, podName, podNamespace)
   243  		if k8sErr == nil {
   244  			break
   245  		}
   246  		log.Debugf("Failed to get %s/%s pod info: %v", podNamespace, podName, k8sErr)
   247  		time.Sleep(podRetrievalInterval)
   248  	}
   249  	if k8sErr != nil {
   250  		log.Errorf("Failed to get %s/%s pod info: %v", podNamespace, podName, k8sErr)
   251  		return k8sErr
   252  	}
   253  
   254  	// Check if istio-init container is present; in that case exclude pod
   255  	if pi.Containers.Contains(ISTIOINIT) {
   256  		log.Infof("excluded due to being already injected with istio-init container")
   257  		return nil
   258  	}
   259  
   260  	if val, ok := pi.ProxyEnvironments["DISABLE_ENVOY"]; ok {
   261  		if val, err := strconv.ParseBool(val); err == nil && val {
   262  			log.Infof("excluded due to DISABLE_ENVOY on istio-proxy", podNamespace, podName)
   263  			return nil
   264  		}
   265  	}
   266  
   267  	if !pi.Containers.Contains(ISTIOPROXY) {
   268  		log.Infof("excluded because it does not have istio-proxy container (have %v)", sets.SortedList(pi.Containers))
   269  		return nil
   270  	}
   271  
   272  	if pi.ProxyType != "" && pi.ProxyType != "sidecar" {
   273  		log.Infof("excluded %s/%s pod because it has proxy type %s", podNamespace, podName, pi.ProxyType)
   274  		return nil
   275  	}
   276  
   277  	val := pi.Annotations[injectAnnotationKey]
   278  	if lbl, labelPresent := pi.Labels[label.SidecarInject.Name]; labelPresent {
   279  		// The label is the new API; if both are present we prefer the label
   280  		val = lbl
   281  	}
   282  	if val != "" {
   283  		log.Debugf("contains inject annotation: %s", val)
   284  		if injectEnabled, err := strconv.ParseBool(val); err == nil {
   285  			if !injectEnabled {
   286  				log.Infof("excluded due to inject-disabled annotation")
   287  				return nil
   288  			}
   289  		}
   290  	}
   291  
   292  	if _, ok := pi.Annotations[sidecarStatusKey]; !ok {
   293  		log.Infof("excluded due to not containing sidecar annotation")
   294  		return nil
   295  	}
   296  
   297  	log.Debugf("Setting up redirect")
   298  
   299  	redirect, err := NewRedirect(pi)
   300  	if err != nil {
   301  		log.Errorf("redirect failed due to bad params: %v", err)
   302  		return err
   303  	}
   304  
   305  	if err := rulesMgr.Program(podName, args.Netns, redirect); err != nil {
   306  		return err
   307  	}
   308  
   309  	return nil
   310  }
   311  
   312  func setupLogging(conf *Config) {
   313  	if conf.LogUDSAddress != "" {
   314  		// reconfigure log output with tee to UDS if UDS log is enabled.
   315  		if err := log.Configure(GetLoggingOptions(conf.LogUDSAddress)); err != nil {
   316  			log.Error("Failed to configure istio-cni with UDS log")
   317  		}
   318  	}
   319  	log.FindScope("default").SetOutputLevel(getLogLevel(conf.LogLevel))
   320  }
   321  
   322  func pluginResponse(conf *Config) error {
   323  	var result *cniv1.Result
   324  	if conf.PrevResult == nil {
   325  		result = &cniv1.Result{
   326  			CNIVersion: cniv1.ImplementedSpecVersion,
   327  		}
   328  		return types.PrintResult(result, conf.CNIVersion)
   329  	}
   330  
   331  	// Pass through the result for the next plugin
   332  	return types.PrintResult(conf.PrevResult, conf.CNIVersion)
   333  }
   334  
   335  func CmdCheck(args *skel.CmdArgs) (err error) {
   336  	return nil
   337  }
   338  
   339  func CmdDelete(args *skel.CmdArgs) (err error) {
   340  	return nil
   341  }
   342  
   343  func isAmbientPod(client kubernetes.Interface, podName, podNamespace string) (bool, error) {
   344  	pod, err := client.CoreV1().Pods(podNamespace).Get(context.Background(), podName, metav1.GetOptions{})
   345  	if err != nil {
   346  		return false, err
   347  	}
   348  	ns, err := client.CoreV1().Namespaces().Get(context.Background(), podNamespace, metav1.GetOptions{})
   349  	if err != nil {
   350  		return false, err
   351  	}
   352  
   353  	return util.PodRedirectionEnabled(ns, pod), nil
   354  }