Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add functionality for setting the returned credentials #21

Merged
merged 2 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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