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 }