Skip to content

Commit

Permalink
feat: Add functionality for setting the returned credentials (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmidyson authored Nov 16, 2022
1 parent 69bd521 commit bb9a610
Show file tree
Hide file tree
Showing 10 changed files with 588 additions and 3 deletions.
57 changes: 57 additions & 0 deletions cmd/kubelet-image-credential-provider-shim/get_credentials.go
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
}
1 change: 1 addition & 0 deletions cmd/kubelet-image-credential-provider-shim/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func main() {
}

rootCmd.AddCommand(newInstallCmd(logger))
rootCmd.AddCommand(newGetCredentialsCmd())

if err := rootCmd.Execute(); err != nil {
logger.Fatal(err)
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module github.com/mesosphere/kubelet-image-credential-provider-shim
go 1.19

require (
github.com/distribution/distribution/v3 v3.0.0-20221111170714-3b8fbf975279
github.com/kelseyhightower/envconfig v1.4.0
github.com/otiai10/copy v1.9.0
github.com/sirupsen/logrus v1.9.0
Expand Down Expand Up @@ -36,13 +37,14 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20220913175220-63ea55921009 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/distribution/v3 v3.0.0-20221111170714-3b8fbf975279 h1:+lFUfSfK1/rMGIUUAwu6O+t4WGRwBU1EpaQTcN8KaeM=
github.com/distribution/distribution/v3 v3.0.0-20221111170714-3b8fbf975279/go.mod h1:4x0IxAMsdeCSTr9UopCvp6MnryD2nyRLycsOrgvveAs=
github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand Down Expand Up @@ -215,6 +217,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4=
github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
Expand Down Expand Up @@ -354,8 +358,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down
160 changes: 160 additions & 0 deletions pkg/credentialprovider/shim/shim.go
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
}
39 changes: 39 additions & 0 deletions pkg/urlglobber/doc.go
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
39 changes: 39 additions & 0 deletions pkg/urlglobber/globber.go
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
}
Loading

0 comments on commit bb9a610

Please sign in to comment.