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  }