k8s.io/kubernetes@v1.29.3/pkg/credentialprovider/plugin/plugin.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package plugin 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "fmt" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "strings" 28 "sync" 29 "time" 30 31 "golang.org/x/sync/singleflight" 32 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 "k8s.io/apimachinery/pkg/runtime/serializer" 36 "k8s.io/apimachinery/pkg/runtime/serializer/json" 37 "k8s.io/client-go/tools/cache" 38 "k8s.io/klog/v2" 39 credentialproviderapi "k8s.io/kubelet/pkg/apis/credentialprovider" 40 "k8s.io/kubelet/pkg/apis/credentialprovider/install" 41 credentialproviderv1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1" 42 credentialproviderv1alpha1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" 43 credentialproviderv1beta1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1beta1" 44 "k8s.io/kubernetes/pkg/credentialprovider" 45 kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config" 46 kubeletconfigv1 "k8s.io/kubernetes/pkg/kubelet/apis/config/v1" 47 kubeletconfigv1alpha1 "k8s.io/kubernetes/pkg/kubelet/apis/config/v1alpha1" 48 kubeletconfigv1beta1 "k8s.io/kubernetes/pkg/kubelet/apis/config/v1beta1" 49 "k8s.io/utils/clock" 50 ) 51 52 const ( 53 globalCacheKey = "global" 54 cachePurgeInterval = time.Minute * 15 55 ) 56 57 var ( 58 scheme = runtime.NewScheme() 59 codecs = serializer.NewCodecFactory(scheme) 60 61 apiVersions = map[string]schema.GroupVersion{ 62 credentialproviderv1alpha1.SchemeGroupVersion.String(): credentialproviderv1alpha1.SchemeGroupVersion, 63 credentialproviderv1beta1.SchemeGroupVersion.String(): credentialproviderv1beta1.SchemeGroupVersion, 64 credentialproviderv1.SchemeGroupVersion.String(): credentialproviderv1.SchemeGroupVersion, 65 } 66 ) 67 68 func init() { 69 install.Install(scheme) 70 kubeletconfig.AddToScheme(scheme) 71 kubeletconfigv1alpha1.AddToScheme(scheme) 72 kubeletconfigv1beta1.AddToScheme(scheme) 73 kubeletconfigv1.AddToScheme(scheme) 74 } 75 76 // RegisterCredentialProviderPlugins is called from kubelet to register external credential provider 77 // plugins according to the CredentialProviderConfig config file. 78 func RegisterCredentialProviderPlugins(pluginConfigFile, pluginBinDir string) error { 79 if _, err := os.Stat(pluginBinDir); err != nil { 80 if os.IsNotExist(err) { 81 return fmt.Errorf("plugin binary directory %s did not exist", pluginBinDir) 82 } 83 84 return fmt.Errorf("error inspecting binary directory %s: %w", pluginBinDir, err) 85 } 86 87 credentialProviderConfig, err := readCredentialProviderConfigFile(pluginConfigFile) 88 if err != nil { 89 return err 90 } 91 92 errs := validateCredentialProviderConfig(credentialProviderConfig) 93 if len(errs) > 0 { 94 return fmt.Errorf("failed to validate credential provider config: %v", errs.ToAggregate()) 95 } 96 97 // Register metrics for credential providers 98 registerMetrics() 99 100 for _, provider := range credentialProviderConfig.Providers { 101 pluginBin := filepath.Join(pluginBinDir, provider.Name) 102 if _, err := os.Stat(pluginBin); err != nil { 103 if os.IsNotExist(err) { 104 return fmt.Errorf("plugin binary executable %s did not exist", pluginBin) 105 } 106 107 return fmt.Errorf("error inspecting binary executable %s: %w", pluginBin, err) 108 } 109 110 plugin, err := newPluginProvider(pluginBinDir, provider) 111 if err != nil { 112 return fmt.Errorf("error initializing plugin provider %s: %w", provider.Name, err) 113 } 114 115 credentialprovider.RegisterCredentialProvider(provider.Name, plugin) 116 } 117 118 return nil 119 } 120 121 // newPluginProvider returns a new pluginProvider based on the credential provider config. 122 func newPluginProvider(pluginBinDir string, provider kubeletconfig.CredentialProvider) (*pluginProvider, error) { 123 mediaType := "application/json" 124 info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType) 125 if !ok { 126 return nil, fmt.Errorf("unsupported media type %q", mediaType) 127 } 128 129 gv, ok := apiVersions[provider.APIVersion] 130 if !ok { 131 return nil, fmt.Errorf("invalid apiVersion: %q", provider.APIVersion) 132 } 133 134 clock := clock.RealClock{} 135 136 return &pluginProvider{ 137 clock: clock, 138 matchImages: provider.MatchImages, 139 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: clock}), 140 defaultCacheDuration: provider.DefaultCacheDuration.Duration, 141 lastCachePurge: clock.Now(), 142 plugin: &execPlugin{ 143 name: provider.Name, 144 apiVersion: provider.APIVersion, 145 encoder: codecs.EncoderForVersion(info.Serializer, gv), 146 pluginBinDir: pluginBinDir, 147 args: provider.Args, 148 envVars: provider.Env, 149 environ: os.Environ, 150 }, 151 }, nil 152 } 153 154 // pluginProvider is the plugin-based implementation of the DockerConfigProvider interface. 155 type pluginProvider struct { 156 clock clock.Clock 157 158 sync.Mutex 159 160 group singleflight.Group 161 162 // matchImages defines the matching image URLs this plugin should operate against. 163 // The plugin provider will not return any credentials for images that do not match 164 // against this list of match URLs. 165 matchImages []string 166 167 // cache stores DockerConfig entries with an expiration time based on the cache duration 168 // returned from the credential provider plugin. 169 cache cache.Store 170 // defaultCacheDuration is the default duration credentials are cached in-memory if the auth plugin 171 // response did not provide a cache duration for credentials. 172 defaultCacheDuration time.Duration 173 174 // plugin is the exec implementation of the credential providing plugin. 175 plugin Plugin 176 177 // lastCachePurge is the last time cache is cleaned for expired entries. 178 lastCachePurge time.Time 179 } 180 181 // cacheEntry is the cache object that will be stored in cache.Store. 182 type cacheEntry struct { 183 key string 184 credentials credentialprovider.DockerConfig 185 expiresAt time.Time 186 } 187 188 // cacheKeyFunc extracts AuthEntry.MatchKey as the cache key function for the plugin provider. 189 func cacheKeyFunc(obj interface{}) (string, error) { 190 key := obj.(*cacheEntry).key 191 return key, nil 192 } 193 194 // cacheExpirationPolicy defines implements cache.ExpirationPolicy, determining expiration based on the expiresAt timestamp. 195 type cacheExpirationPolicy struct { 196 clock clock.Clock 197 } 198 199 // IsExpired returns true if the current time is after cacheEntry.expiresAt, which is determined by the 200 // cache duration returned from the credential provider plugin response. 201 func (c *cacheExpirationPolicy) IsExpired(entry *cache.TimestampedEntry) bool { 202 return c.clock.Now().After(entry.Obj.(*cacheEntry).expiresAt) 203 } 204 205 // Provide returns a credentialprovider.DockerConfig based on the credentials returned 206 // from cache or the exec plugin. 207 func (p *pluginProvider) Provide(image string) credentialprovider.DockerConfig { 208 if !p.isImageAllowed(image) { 209 return credentialprovider.DockerConfig{} 210 } 211 212 cachedConfig, found, err := p.getCachedCredentials(image) 213 if err != nil { 214 klog.Errorf("Failed to get cached docker config: %v", err) 215 return credentialprovider.DockerConfig{} 216 } 217 218 if found { 219 return cachedConfig 220 } 221 222 // ExecPlugin is wrapped in single flight to exec plugin once for concurrent same image request. 223 // The caveat here is we don't know cacheKeyType yet, so if cacheKeyType is registry/global and credentials saved in cache 224 // on per registry/global basis then exec will be called for all requests if requests are made concurrently. 225 // foo.bar.registry 226 // foo.bar.registry/image1 227 // foo.bar.registry/image2 228 res, err, _ := p.group.Do(image, func() (interface{}, error) { 229 return p.plugin.ExecPlugin(context.Background(), image) 230 }) 231 232 if err != nil { 233 klog.Errorf("Failed getting credential from external registry credential provider: %v", err) 234 return credentialprovider.DockerConfig{} 235 } 236 237 response, ok := res.(*credentialproviderapi.CredentialProviderResponse) 238 if !ok { 239 klog.Errorf("Invalid response type returned by external credential provider") 240 return credentialprovider.DockerConfig{} 241 } 242 243 var cacheKey string 244 switch cacheKeyType := response.CacheKeyType; cacheKeyType { 245 case credentialproviderapi.ImagePluginCacheKeyType: 246 cacheKey = image 247 case credentialproviderapi.RegistryPluginCacheKeyType: 248 registry := parseRegistry(image) 249 cacheKey = registry 250 case credentialproviderapi.GlobalPluginCacheKeyType: 251 cacheKey = globalCacheKey 252 default: 253 klog.Errorf("credential provider plugin did not return a valid cacheKeyType: %q", cacheKeyType) 254 return credentialprovider.DockerConfig{} 255 } 256 257 dockerConfig := make(credentialprovider.DockerConfig, len(response.Auth)) 258 for matchImage, authConfig := range response.Auth { 259 dockerConfig[matchImage] = credentialprovider.DockerConfigEntry{ 260 Username: authConfig.Username, 261 Password: authConfig.Password, 262 } 263 } 264 265 // cache duration was explicitly 0 so don't cache this response at all. 266 if response.CacheDuration != nil && response.CacheDuration.Duration == 0 { 267 return dockerConfig 268 } 269 270 var expiresAt time.Time 271 // nil cache duration means use the default cache duration 272 if response.CacheDuration == nil { 273 if p.defaultCacheDuration == 0 { 274 return dockerConfig 275 } 276 expiresAt = p.clock.Now().Add(p.defaultCacheDuration) 277 } else { 278 expiresAt = p.clock.Now().Add(response.CacheDuration.Duration) 279 } 280 281 cachedEntry := &cacheEntry{ 282 key: cacheKey, 283 credentials: dockerConfig, 284 expiresAt: expiresAt, 285 } 286 287 if err := p.cache.Add(cachedEntry); err != nil { 288 klog.Errorf("Error adding auth entry to cache: %v", err) 289 } 290 291 return dockerConfig 292 } 293 294 // Enabled always returns true since registration of the plugin via kubelet implies it should be enabled. 295 func (p *pluginProvider) Enabled() bool { 296 return true 297 } 298 299 // isImageAllowed returns true if the image matches against the list of allowed matches by the plugin. 300 func (p *pluginProvider) isImageAllowed(image string) bool { 301 for _, matchImage := range p.matchImages { 302 if matched, _ := credentialprovider.URLsMatchStr(matchImage, image); matched { 303 return true 304 } 305 } 306 307 return false 308 } 309 310 // getCachedCredentials returns a credentialprovider.DockerConfig if cached from the plugin. 311 func (p *pluginProvider) getCachedCredentials(image string) (credentialprovider.DockerConfig, bool, error) { 312 p.Lock() 313 if p.clock.Now().After(p.lastCachePurge.Add(cachePurgeInterval)) { 314 // NewExpirationCache purges expired entries when List() is called 315 // The expired entry in the cache is removed only when Get or List called on it. 316 // List() is called on some interval to remove those expired entries on which Get is never called. 317 _ = p.cache.List() 318 p.lastCachePurge = p.clock.Now() 319 } 320 p.Unlock() 321 322 obj, found, err := p.cache.GetByKey(image) 323 if err != nil { 324 return nil, false, err 325 } 326 327 if found { 328 return obj.(*cacheEntry).credentials, true, nil 329 } 330 331 registry := parseRegistry(image) 332 obj, found, err = p.cache.GetByKey(registry) 333 if err != nil { 334 return nil, false, err 335 } 336 337 if found { 338 return obj.(*cacheEntry).credentials, true, nil 339 } 340 341 obj, found, err = p.cache.GetByKey(globalCacheKey) 342 if err != nil { 343 return nil, false, err 344 } 345 346 if found { 347 return obj.(*cacheEntry).credentials, true, nil 348 } 349 350 return nil, false, nil 351 } 352 353 // Plugin is the interface calling ExecPlugin. This is mainly for testability 354 // so tests don't have to actually exec any processes. 355 type Plugin interface { 356 ExecPlugin(ctx context.Context, image string) (*credentialproviderapi.CredentialProviderResponse, error) 357 } 358 359 // execPlugin is the implementation of the Plugin interface that execs a credential provider plugin based 360 // on it's name provided in CredentialProviderConfig. It is assumed that the executable is available in the 361 // plugin directory provided by the kubelet. 362 type execPlugin struct { 363 name string 364 apiVersion string 365 encoder runtime.Encoder 366 args []string 367 envVars []kubeletconfig.ExecEnvVar 368 pluginBinDir string 369 environ func() []string 370 } 371 372 // ExecPlugin executes the plugin binary with arguments and environment variables specified in CredentialProviderConfig: 373 // 374 // $ ENV_NAME=ENV_VALUE <plugin-name> args[0] args[1] <<<request 375 // 376 // The plugin is expected to receive the CredentialProviderRequest API via stdin from the kubelet and 377 // return CredentialProviderResponse via stdout. 378 func (e *execPlugin) ExecPlugin(ctx context.Context, image string) (*credentialproviderapi.CredentialProviderResponse, error) { 379 klog.V(5).Infof("Getting image %s credentials from external exec plugin %s", image, e.name) 380 381 authRequest := &credentialproviderapi.CredentialProviderRequest{Image: image} 382 data, err := e.encodeRequest(authRequest) 383 if err != nil { 384 return nil, fmt.Errorf("failed to encode auth request: %w", err) 385 } 386 387 stdout := &bytes.Buffer{} 388 stderr := &bytes.Buffer{} 389 stdin := bytes.NewBuffer(data) 390 391 // Use a catch-all timeout of 1 minute for all exec-based plugins, this should leave enough 392 // head room in case a plugin needs to retry a failed request while ensuring an exec plugin 393 // does not run forever. In the future we may want this timeout to be tweakable from the plugin 394 // config file. 395 ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) 396 defer cancel() 397 398 cmd := exec.CommandContext(ctx, filepath.Join(e.pluginBinDir, e.name), e.args...) 399 cmd.Stdout, cmd.Stderr, cmd.Stdin = stdout, stderr, stdin 400 401 var configEnvVars []string 402 for _, v := range e.envVars { 403 configEnvVars = append(configEnvVars, fmt.Sprintf("%s=%s", v.Name, v.Value)) 404 } 405 406 // Append current system environment variables, to the ones configured in the 407 // credential provider file. Failing to do so may result in unsuccessful execution 408 // of the provider binary, see https://github.com/kubernetes/kubernetes/issues/102750 409 // also, this behaviour is inline with Credential Provider Config spec 410 cmd.Env = mergeEnvVars(e.environ(), configEnvVars) 411 412 if err = e.runPlugin(ctx, cmd, image); err != nil { 413 return nil, fmt.Errorf("%w: %s", err, stderr.String()) 414 } 415 416 data = stdout.Bytes() 417 // check that the response apiVersion matches what is expected 418 gvk, err := json.DefaultMetaFactory.Interpret(data) 419 if err != nil { 420 return nil, fmt.Errorf("error reading GVK from response: %w", err) 421 } 422 423 if gvk.GroupVersion().String() != e.apiVersion { 424 return nil, fmt.Errorf("apiVersion from credential plugin response did not match expected apiVersion:%s, actual apiVersion:%s", e.apiVersion, gvk.GroupVersion().String()) 425 } 426 427 response, err := e.decodeResponse(data) 428 if err != nil { 429 // err is explicitly not wrapped since it may contain credentials in the response. 430 return nil, errors.New("error decoding credential provider plugin response from stdout") 431 } 432 433 return response, nil 434 } 435 436 func (e *execPlugin) runPlugin(ctx context.Context, cmd *exec.Cmd, image string) error { 437 startTime := time.Now() 438 defer func() { 439 kubeletCredentialProviderPluginDuration.WithLabelValues(e.name).Observe(time.Since(startTime).Seconds()) 440 }() 441 442 err := cmd.Run() 443 if ctx.Err() != nil { 444 kubeletCredentialProviderPluginErrors.WithLabelValues(e.name).Inc() 445 return fmt.Errorf("error execing credential provider plugin %s for image %s: %w", e.name, image, ctx.Err()) 446 } 447 if err != nil { 448 kubeletCredentialProviderPluginErrors.WithLabelValues(e.name).Inc() 449 return fmt.Errorf("error execing credential provider plugin %s for image %s: %w", e.name, image, err) 450 } 451 return nil 452 } 453 454 // encodeRequest encodes the internal CredentialProviderRequest type into the v1alpha1 version in json 455 func (e *execPlugin) encodeRequest(request *credentialproviderapi.CredentialProviderRequest) ([]byte, error) { 456 data, err := runtime.Encode(e.encoder, request) 457 if err != nil { 458 return nil, fmt.Errorf("error encoding request: %w", err) 459 } 460 461 return data, nil 462 } 463 464 // decodeResponse decodes data into the internal CredentialProviderResponse type 465 func (e *execPlugin) decodeResponse(data []byte) (*credentialproviderapi.CredentialProviderResponse, error) { 466 obj, gvk, err := codecs.UniversalDecoder().Decode(data, nil, nil) 467 if err != nil { 468 return nil, err 469 } 470 471 if gvk.Kind != "CredentialProviderResponse" { 472 return nil, fmt.Errorf("failed to decode CredentialProviderResponse, unexpected Kind: %q", gvk.Kind) 473 } 474 475 if gvk.Group != credentialproviderapi.GroupName { 476 return nil, fmt.Errorf("failed to decode CredentialProviderResponse, unexpected Group: %s", gvk.Group) 477 } 478 479 if internalResponse, ok := obj.(*credentialproviderapi.CredentialProviderResponse); ok { 480 return internalResponse, nil 481 } 482 483 return nil, fmt.Errorf("unable to convert %T to *CredentialProviderResponse", obj) 484 } 485 486 // parseRegistry extracts the registry hostname of an image (including port if specified). 487 func parseRegistry(image string) string { 488 imageParts := strings.Split(image, "/") 489 return imageParts[0] 490 } 491 492 // mergedEnvVars overlays system defined env vars with credential provider env vars, 493 // it gives priority to the credential provider vars allowing user to override system 494 // env vars 495 func mergeEnvVars(sysEnvVars, credProviderVars []string) []string { 496 mergedEnvVars := sysEnvVars 497 for _, credProviderVar := range credProviderVars { 498 mergedEnvVars = append(mergedEnvVars, credProviderVar) 499 } 500 return mergedEnvVars 501 }