-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add functionality for setting the returned credentials (#21)
- Loading branch information
1 parent
69bd521
commit bb9a610
Showing
10 changed files
with
588 additions
and
3 deletions.
There are no files selected for viewing
57 changes: 57 additions & 0 deletions
57
cmd/kubelet-image-credential-provider-shim/get_credentials.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// Copyright 2022 D2iQ, Inc. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/spf13/cobra" | ||
|
||
"github.com/mesosphere/kubelet-image-credential-provider-shim/pkg/credentialprovider/plugin" | ||
"github.com/mesosphere/kubelet-image-credential-provider-shim/pkg/credentialprovider/shim" | ||
) | ||
|
||
type getCredentialsOptions struct { | ||
configFile string | ||
} | ||
|
||
func (o *getCredentialsOptions) AddFlags(cmd *cobra.Command) { | ||
cmd.Flags().StringVarP( | ||
&o.configFile, "config", "c", o.configFile, | ||
"path to the configuration file for the Kubelet credential provider shim", | ||
) | ||
} | ||
|
||
func defaultCredentialsOptions() *getCredentialsOptions { | ||
return &getCredentialsOptions{ | ||
configFile: "/etc/kubernetes/image-credential-provider/kubelet-image-credential-provider-shim.yaml", | ||
} | ||
} | ||
|
||
func newGetCredentialsCmd() *cobra.Command { | ||
opts := defaultCredentialsOptions() | ||
|
||
cmd := &cobra.Command{ | ||
Use: "get-credentials", | ||
Short: "Get authentication credentials", | ||
Args: cobra.NoArgs, | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
provider, err := shim.NewProviderFromConfigFile(opts.configFile) | ||
if err != nil { | ||
return fmt.Errorf("error initializing shim credential provider: %w", err) | ||
} | ||
|
||
err = plugin.NewProvider(provider).Run(context.Background()) | ||
if err != nil { | ||
return fmt.Errorf("error running shim credential provider: %w", err) | ||
} | ||
|
||
return nil | ||
}, | ||
} | ||
opts.AddFlags(cmd) | ||
|
||
return cmd | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
// Copyright 2022 D2iQ, Inc. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package shim | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"os" | ||
|
||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
"k8s.io/apimachinery/pkg/runtime/serializer" | ||
credentialproviderv1beta1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1beta1" | ||
|
||
"github.com/mesosphere/kubelet-image-credential-provider-shim/apis/config/v1alpha1" | ||
"github.com/mesosphere/kubelet-image-credential-provider-shim/pkg/credentialprovider/plugin" | ||
"github.com/mesosphere/kubelet-image-credential-provider-shim/pkg/urlglobber" | ||
) | ||
|
||
var ( | ||
scheme = runtime.NewScheme() | ||
codecs = serializer.NewCodecFactory(scheme) | ||
|
||
ErrUnsupportedMirrorCredentialStrategy = errors.New("unsupported mirror credential strategy") | ||
) | ||
|
||
//nolint:gochecknoinits // init is idiomatically used to set up schemes | ||
func init() { | ||
_ = v1alpha1.AddToScheme(scheme) | ||
} | ||
|
||
type shimProvider struct { | ||
cfg *v1alpha1.KubeletImageCredentialProviderShimConfig | ||
} | ||
|
||
func NewProviderFromConfigFile(fName string) (plugin.CredentialProvider, error) { | ||
data, err := os.ReadFile(fName) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read config file %q: %w", fName, err) | ||
} | ||
|
||
obj, _, err := codecs.UniversalDecoder(v1alpha1.GroupVersion).Decode(data, nil, nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to decode config file %q: %w", fName, err) | ||
} | ||
|
||
config, ok := obj.(*v1alpha1.KubeletImageCredentialProviderShimConfig) | ||
if !ok { | ||
return nil, fmt.Errorf( | ||
"failed to convert %T to *KubeletImageCredentialProviderShimConfig", | ||
obj, | ||
) | ||
} | ||
|
||
return &shimProvider{cfg: config}, nil | ||
} | ||
|
||
func (p shimProvider) GetCredentials( | ||
_ context.Context, | ||
img string, | ||
_ []string, | ||
) (*credentialproviderv1beta1.CredentialProviderResponse, error) { | ||
globbedDomain, err := urlglobber.GlobbedDomainForImage(img) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
authMap := map[string]credentialproviderv1beta1.AuthConfig{} | ||
|
||
mirrorAuth, mirrorAuthFound := p.getMirrorCredentials(img) | ||
|
||
originAuth, originAuthFound := p.getOriginCredentials(img) | ||
|
||
if originAuthFound { | ||
authMap[img] = originAuth | ||
} | ||
|
||
if p.cfg.Mirror != nil && mirrorAuthFound { | ||
switch p.cfg.Mirror.MirrorCredentialsStrategy { | ||
case v1alpha1.MirrorCredentialsOnly: | ||
// Only return mirror credentials by setting the image auth for the full image name whether it is already set or | ||
// not. | ||
authMap[img] = mirrorAuth | ||
case v1alpha1.MirrorCredentialsLast: | ||
// Set mirror credentials for globbed domain to ensure that the mirror credentials are used last (glob matches | ||
// have lowest precedence). | ||
// | ||
// This means that the kubelet will first try the mirror credentials, which containerd will try against both the | ||
// configured mirror in containerd and the origin registry (which should fail as incorrect credentials for this | ||
// registry) if the image is not found in the mirror. | ||
// | ||
// If containerd fails to pull using the mirror credentials, then the kubelet will try the origin credentials, | ||
// which containerd will try first against the configured mirror (which should fail as incorrect credentials for | ||
// this registry) and then against the origin registry. | ||
authMap[globbedDomain] = mirrorAuth | ||
case v1alpha1.MirrorCredentialsFirst: | ||
// Set mirror credentials for image to ensure that the mirror credentials are used first, and set any existing | ||
// origin credentials for the globbed domain to ensure they are used last (glob matches have lowest precedence). | ||
// | ||
// This means that the kubelet will first try the origin credentials, which containerd will try against both the | ||
// configured mirror in containerd (which should fail as incorrect credentials for this registry) and the origin | ||
// registry. | ||
// | ||
// If containerd fails to pull using the origin credentials, then the kubelet will try the mirror credentials, | ||
// which containerd will try first against the configured mirror and then against the origin registry (which | ||
// should fail as incorrect credentials for this registry) if the image is not found in the mirror. | ||
existing, found := authMap[img] | ||
if found { | ||
authMap[globbedDomain] = existing | ||
} | ||
authMap[img] = mirrorAuth | ||
default: | ||
return nil, fmt.Errorf( | ||
"%w: %q", | ||
ErrUnsupportedMirrorCredentialStrategy, | ||
p.cfg.Mirror.MirrorCredentialsStrategy, | ||
) | ||
} | ||
} | ||
|
||
return &credentialproviderv1beta1.CredentialProviderResponse{ | ||
CacheKeyType: credentialproviderv1beta1.ImagePluginCacheKeyType, | ||
CacheDuration: &metav1.Duration{Duration: 0}, | ||
Auth: authMap, | ||
}, nil | ||
} | ||
|
||
func (p shimProvider) getMirrorCredentials( | ||
img string, //nolint:unparam,revive // Placeholder for now. | ||
) (credentialproviderv1beta1.AuthConfig, bool) { //nolint:unparam // Placeholder for now | ||
// If mirror is not configured then return no credentials for the mirror. | ||
if p.cfg.Mirror == nil { | ||
return credentialproviderv1beta1.AuthConfig{}, false | ||
} | ||
|
||
// TODO Call relevant credential provider plugin based on the image domain replaced with the mirror URL to get the | ||
// credentials for the mirror. | ||
|
||
return credentialproviderv1beta1.AuthConfig{}, false | ||
} | ||
|
||
func (p shimProvider) getOriginCredentials( | ||
img string, //nolint:unparam,revive // Placeholder for now | ||
) (credentialproviderv1beta1.AuthConfig, bool) { //nolint:unparam // Placeholder for now | ||
// If only mirror credentials should be used then return no credentials for the origin. | ||
if isRegistryCredentialsOnly(p.cfg.Mirror) { | ||
return credentialproviderv1beta1.AuthConfig{}, false | ||
} | ||
|
||
// TODO Call relevant credential provider plugin based on the image to get the credentials for the origin. | ||
|
||
return credentialproviderv1beta1.AuthConfig{}, false | ||
} | ||
|
||
func isRegistryCredentialsOnly(cfg *v1alpha1.MirrorConfig) bool { | ||
return cfg != nil && | ||
cfg.MirrorCredentialsStrategy == v1alpha1.MirrorCredentialsOnly | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// Copyright 2022 D2iQ, Inc. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
// This package provides utilities to work with URL globs for credential providers. | ||
// | ||
// Duplicating documentation from | ||
// https://github.com/kubernetes/kubelet/blob/v0.25.4/pkg/apis/credentialprovider/v1beta1/types.go#L73-L101 | ||
// for visibility: | ||
// | ||
// auth is a map containing authentication information passed into the kubelet. | ||
// Each key is a match image string (more on this below). The corresponding authConfig value | ||
// should be valid for all images that match against this key. A plugin should set | ||
// this field to null if no valid credentials can be returned for the requested image. | ||
// | ||
// Each key in the map is a pattern which can optionally contain a port and a path. | ||
// Globs can be used in the domain, but not in the port or the path. Globs are supported | ||
// as subdomains like '*.k8s.io' or 'k8s.*.io', and top-level-domains such as 'k8s.*'. | ||
// Matching partial subdomains like 'app*.k8s.io' is also supported. Each glob can only match | ||
// a single subdomain segment, so *.io does not match *.k8s.io. | ||
// | ||
// The kubelet will match images against the key when all of the below are true: | ||
// - Both contain the same number of domain parts and each part matches. | ||
// - The URL path of an imageMatch must be a prefix of the target image URL path. | ||
// - If the imageMatch contains a port, then the port must match in the image as well. | ||
// | ||
// When multiple keys are returned, the kubelet will traverse all keys in reverse order so that: | ||
// - longer keys come before shorter keys with the same prefix | ||
// - non-wildcard keys come before wildcard keys with the same prefix. | ||
// | ||
// For any given match, the kubelet will attempt an image pull with the provided credentials, | ||
// stopping after the first successfully authenticated pull. | ||
// | ||
// Example keys: | ||
// - 123456789.dkr.ecr.us-east-1.amazonaws.com | ||
// - *.azurecr.io | ||
// - gcr.io | ||
// - *.*.registry.io | ||
// - registry.io:8080/path | ||
package urlglobber |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// Copyright 2022 D2iQ, Inc. All rights reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package urlglobber | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net" | ||
"regexp" | ||
|
||
"github.com/distribution/distribution/v3/reference" | ||
) | ||
|
||
var ( | ||
domainSegmentRE = regexp.MustCompile(`[^.]+`) | ||
|
||
ErrInvalidImageReference = errors.New("invalid image reference") | ||
) | ||
|
||
func GlobbedDomainForImage(img string) (string, error) { | ||
namedImg, err := reference.ParseNormalizedNamed(img) | ||
if err != nil { | ||
return "", fmt.Errorf("%w: %v", ErrInvalidImageReference, err) | ||
} | ||
|
||
domain := reference.Domain(namedImg) | ||
|
||
domainWithoutPort, port, errSplitPort := net.SplitHostPort(domain) | ||
if errSplitPort == nil { | ||
domain = domainWithoutPort | ||
} | ||
globbedDomain := domainSegmentRE.ReplaceAllLiteralString(domain, "*") | ||
if errSplitPort == nil { | ||
globbedDomain = net.JoinHostPort(globbedDomain, port) | ||
} | ||
|
||
return globbedDomain, nil | ||
} |
Oops, something went wrong.