istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/ecds.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  package xds
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    22  
    23  	credscontroller "istio.io/istio/pilot/pkg/credentials"
    24  	"istio.io/istio/pilot/pkg/model"
    25  	"istio.io/istio/pilot/pkg/model/credentials"
    26  	"istio.io/istio/pilot/pkg/networking/core"
    27  	"istio.io/istio/pilot/pkg/util/protoconv"
    28  	"istio.io/istio/pkg/cluster"
    29  	"istio.io/istio/pkg/config/schema/kind"
    30  	"istio.io/istio/pkg/util/sets"
    31  )
    32  
    33  // EcdsGenerator generates ECDS configuration.
    34  type EcdsGenerator struct {
    35  	ConfigGenerator  core.ConfigGenerator
    36  	secretController credscontroller.MulticlusterController
    37  }
    38  
    39  var _ model.XdsResourceGenerator = &EcdsGenerator{}
    40  
    41  func ecdsNeedsPush(req *model.PushRequest) bool {
    42  	if req == nil {
    43  		return true
    44  	}
    45  	// If none set, we will always push
    46  	if len(req.ConfigsUpdated) == 0 {
    47  		return true
    48  	}
    49  	// Only push if config updates is triggered by EnvoyFilter, WasmPlugin, or Secret.
    50  	for config := range req.ConfigsUpdated {
    51  		switch config.Kind {
    52  		case kind.EnvoyFilter:
    53  			return true
    54  		case kind.WasmPlugin:
    55  			return true
    56  		case kind.Secret:
    57  			return true
    58  		}
    59  	}
    60  	return false
    61  }
    62  
    63  // onlyReferencedConfigsUpdated indicates whether the PushRequest
    64  // has ONLY referenced resource in ConfigUpdates. For example ONLY
    65  // secret is updated that may be referred by Wasm Plugin.
    66  func onlyReferencedConfigsUpdated(req *model.PushRequest) bool {
    67  	referencedConfigUpdated := false
    68  	for config := range req.ConfigsUpdated {
    69  		switch config.Kind {
    70  		case kind.EnvoyFilter:
    71  			return false
    72  		case kind.WasmPlugin:
    73  			return false
    74  		case kind.Secret:
    75  			referencedConfigUpdated = true
    76  		}
    77  	}
    78  	return referencedConfigUpdated
    79  }
    80  
    81  // Generate returns ECDS resources for a given proxy.
    82  func (e *EcdsGenerator) Generate(proxy *model.Proxy, w *model.WatchedResource, req *model.PushRequest) (model.Resources, model.XdsLogDetails, error) {
    83  	if !ecdsNeedsPush(req) {
    84  		return nil, model.DefaultXdsLogDetails, nil
    85  	}
    86  
    87  	wasmSecrets := referencedSecrets(proxy, req.Push, w.ResourceNames)
    88  
    89  	// When referenced configs are ONLY updated (like secret update), we should push
    90  	// if the referenced config is relevant for ECDS. A secret update is relevant
    91  	// only if it is referred via WASM plugin.
    92  	if onlyReferencedConfigsUpdated(req) {
    93  		updatedSecrets := model.ConfigsOfKind(req.ConfigsUpdated, kind.Secret)
    94  		needsPush := false
    95  		for _, sr := range wasmSecrets {
    96  			if _, found := updatedSecrets[model.ConfigKey{Kind: kind.Secret, Name: sr.Name, Namespace: sr.Namespace}]; found {
    97  				needsPush = true
    98  				break
    99  			}
   100  		}
   101  		if !needsPush {
   102  			return nil, model.DefaultXdsLogDetails, nil
   103  		}
   104  	}
   105  
   106  	var secrets map[string][]byte
   107  	if len(wasmSecrets) > 0 {
   108  		// Generate the pull secrets first, which will be used when populating the extension config.
   109  		if e.secretController != nil {
   110  			var err error
   111  			secretController, err := e.secretController.ForCluster(proxy.Metadata.ClusterID)
   112  			if err != nil {
   113  				log.Warnf("proxy %s is from an unknown cluster, cannot retrieve certificates for Wasm image pull: %v", proxy.ID, err)
   114  				return nil, model.DefaultXdsLogDetails, nil
   115  			}
   116  			// Inserts Wasm pull secrets in ECDS response, which will be used at xds proxy for image pull.
   117  			// Before forwarding to Envoy, xds proxy will remove the secret from ECDS response.
   118  			secrets = e.GeneratePullSecrets(proxy, wasmSecrets, secretController)
   119  		}
   120  	}
   121  
   122  	ec := e.ConfigGenerator.BuildExtensionConfiguration(proxy, req.Push, w.ResourceNames, secrets)
   123  
   124  	if ec == nil {
   125  		return nil, model.DefaultXdsLogDetails, nil
   126  	}
   127  
   128  	resources := make(model.Resources, 0, len(ec))
   129  	for _, c := range ec {
   130  		resources = append(resources, &discovery.Resource{
   131  			Name:     c.Name,
   132  			Resource: protoconv.MessageToAny(c),
   133  		})
   134  	}
   135  
   136  	return resources, model.DefaultXdsLogDetails, nil
   137  }
   138  
   139  func (e *EcdsGenerator) GeneratePullSecrets(proxy *model.Proxy, secretResources []SecretResource,
   140  	secretController credscontroller.Controller,
   141  ) map[string][]byte {
   142  	if proxy.VerifiedIdentity == nil {
   143  		log.Warnf("proxy %s is not authorized to receive secret. Ensure you are connecting over TLS port and are authenticated.", proxy.ID)
   144  		return nil
   145  	}
   146  
   147  	results := make(map[string][]byte)
   148  	for _, sr := range secretResources {
   149  		cred, err := secretController.GetDockerCredential(sr.Name, sr.Namespace)
   150  		if err != nil {
   151  			log.Warnf("Failed to fetch docker credential %s: %v", sr.ResourceName, err)
   152  		} else {
   153  			results[sr.ResourceName] = cred
   154  		}
   155  	}
   156  	return results
   157  }
   158  
   159  func (e *EcdsGenerator) SetCredController(creds credscontroller.MulticlusterController) {
   160  	e.secretController = creds
   161  }
   162  
   163  func referencedSecrets(proxy *model.Proxy, push *model.PushContext, resourceNames []string) []SecretResource {
   164  	// The requirement for the Wasm pull secret:
   165  	// * Wasm pull secrets must be of type `kubernetes.io/dockerconfigjson`.
   166  	// * Secret are referenced by a WasmPlugin which applies to this proxy.
   167  	// TODO: we get the WasmPlugins here to get the secrets reference in order to decide whether ECDS push is needed,
   168  	//       and we will get it again at extension config build. Avoid getting it twice if this becomes a problem.
   169  	watched := sets.New(resourceNames...)
   170  	wasmPlugins := push.WasmPlugins(proxy)
   171  	referencedSecrets := sets.String{}
   172  	for _, wps := range wasmPlugins {
   173  		for _, wp := range wps {
   174  			if watched.Contains(wp.ResourceName) && wp.ImagePullSecret != "" {
   175  				referencedSecrets.Insert(wp.ImagePullSecret)
   176  			}
   177  		}
   178  	}
   179  	var filtered []SecretResource
   180  	for rn := range referencedSecrets {
   181  		sr, err := parseSecretName(rn, proxy.Metadata.ClusterID)
   182  		if err != nil {
   183  			log.Warnf("Failed to parse secret resource name %v: %v", rn, err)
   184  			continue
   185  		}
   186  		filtered = append(filtered, sr)
   187  	}
   188  
   189  	return filtered
   190  }
   191  
   192  // parseSecretName parses secret resource name from WasmPlugin env variable.
   193  // See toSecretResourceName at model/extensions.go about how secret resource name is generated.
   194  func parseSecretName(resourceName string, proxyCluster cluster.ID) (SecretResource, error) {
   195  	// The secret resource name must be formatted as kubernetes://secret-namespace/secret-name.
   196  	if !strings.HasPrefix(resourceName, credentials.KubernetesSecretTypeURI) {
   197  		return SecretResource{}, fmt.Errorf("misformed Wasm pull secret resource name %v", resourceName)
   198  	}
   199  	res := strings.TrimPrefix(resourceName, credentials.KubernetesSecretTypeURI)
   200  	sep := "/"
   201  	split := strings.Split(res, sep)
   202  	if len(split) != 2 {
   203  		return SecretResource{}, fmt.Errorf("misformed Wasm pull secret resource name %v", resourceName)
   204  	}
   205  	return SecretResource{
   206  		SecretResource: credentials.SecretResource{
   207  			ResourceType: credentials.KubernetesSecretType,
   208  			Name:         split[1],
   209  			Namespace:    split[0],
   210  			ResourceName: resourceName,
   211  			Cluster:      proxyCluster,
   212  		},
   213  	}, nil
   214  }