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 }