Skip to content

Commit

Permalink
Add pkg/authn/kubernetes (#1234)
Browse files Browse the repository at this point in the history
* Add pkg/authn/kubernetes

This takes the Kubernetes client-go parts from k8schain, removes the
forked-upstream credentialprovider bits and rewrites them to include
only exactly what we need.

It also reimplements k8schain in terms of this new K8s helper, and of
wrapped cred helpers using authn.NewKeychainFromHelper.

* run presubmit using go 1.17

* streamline a bit, check for auths matching repo first, then fallback to registry

* don't deprecate k8schain just yet, type alias Options

* Preserve ordering behavior in k8schain -- first matching secret wins, don't override

* pin to k8s 0.22.5

* k8schain -> k8s 0.22.5

* go mod download k8s.io/api
  • Loading branch information
imjasonh authored Jan 8, 2022
1 parent bf65fd7 commit a5c1d03
Show file tree
Hide file tree
Showing 13 changed files with 1,794 additions and 280 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/presubmit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.16.x
go-version: 1.17.x
- run: ./hack/presubmit.sh
3 changes: 3 additions & 0 deletions hack/presubmit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,8 @@ go test ./...

pushd ${PROJECT_ROOT}/pkg/authn/k8schain
trap popd EXIT
go build ./...

pushd ${PROJECT_ROOT}/pkg/authn/kubernetes
trap popd EXIT
go test ./...
2 changes: 1 addition & 1 deletion pkg/authn/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import (

func main() {
// ...
ecrHelper := ecr.ECRHelper{ClientFactory: api.DefaultClientFactory()}
ecrHelper := ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}
img, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.NewKeychainFromHelper(ecrHelper)))
if err != nil {
panic(err)
Expand Down
16 changes: 10 additions & 6 deletions pkg/authn/k8schain/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ module github.com/google/go-containerregistry/pkg/authn/k8schain
go 1.14

require (
github.com/google/go-containerregistry v0.5.2-0.20210609162550-f0ce2270b3b4
github.com/vdemeester/k8s-pkg-credentialprovider v1.21.0-1
k8s.io/api v0.21.1
k8s.io/apimachinery v0.21.1
k8s.io/client-go v0.21.1
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20211027214941-f15886b5ccdc
github.com/chrismellard/docker-credential-acr-env v0.0.0-20210203204924-09e2b5a8ac86
github.com/google/go-containerregistry v0.8.0
github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-00010101000000-000000000000
k8s.io/api v0.22.5
k8s.io/client-go v0.22.5
)

replace github.com/google/go-containerregistry => ../../..
replace (
github.com/google/go-containerregistry => ../../../
github.com/google/go-containerregistry/pkg/authn/kubernetes => ../kubernetes/
)
175 changes: 101 additions & 74 deletions pkg/authn/k8schain/go.sum

Large diffs are not rendered by default.

147 changes: 34 additions & 113 deletions pkg/authn/k8schain/k8schain.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,81 +16,42 @@ package k8schain

import (
"context"
"fmt"
"sync"

ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login"
"github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api"
"github.com/chrismellard/docker-credential-acr-env/pkg/credhelper"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
credentialprovider "github.com/vdemeester/k8s-pkg-credentialprovider"
credentialprovidersecrets "github.com/vdemeester/k8s-pkg-credentialprovider/secrets"
kauth "github.com/google/go-containerregistry/pkg/authn/kubernetes"
"github.com/google/go-containerregistry/pkg/v1/google"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

// Options holds configuration data for guiding credential resolution.
type Options struct {
// Namespace holds the namespace inside of which we are resolving the
// image reference. If empty, "default" is assumed.
Namespace string
// ServiceAccountName holds the serviceaccount as which the container
// will run (scoped to Namespace). If empty, "default" is assumed.
ServiceAccountName string
// ImagePullSecrets holds the names of the Kubernetes secrets (scoped to
// Namespace) containing credential data to use for the image pull.
ImagePullSecrets []string
}

var (
keyring credentialprovider.DockerKeyring
once sync.Once
amazonKeychain authn.Keychain = authn.NewKeychainFromHelper(ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}})
azureKeychain authn.Keychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper())
)

// Options holds configuration data for guiding credential resolution.
type Options = kauth.Options

// New returns a new authn.Keychain suitable for resolving image references as
// scoped by the provided Options. It speaks to Kubernetes through the provided
// client interface.
func New(ctx context.Context, client kubernetes.Interface, opt Options) (authn.Keychain, error) {
if opt.Namespace == "" {
opt.Namespace = "default"
}
if opt.ServiceAccountName == "" {
opt.ServiceAccountName = "default"
}

// Implement a Kubernetes-style authentication keychain.
// This needs to support roughly the following kinds of authentication:
// 1) The implicit authentication from k8s.io/kubernetes/pkg/credentialprovider
// 2) The explicit authentication from imagePullSecrets on Pod
// 3) The semi-implicit authentication where imagePullSecrets are on the
// Pod's service account.

// First, fetch all of the explicitly declared pull secrets
var pullSecrets []corev1.Secret
if client != nil {
for _, name := range opt.ImagePullSecrets {
ps, err := client.CoreV1().Secrets(opt.Namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, err
}
pullSecrets = append(pullSecrets, *ps)
}

// Second, fetch all of the pull secrets attached to our service account.
sa, err := client.CoreV1().ServiceAccounts(opt.Namespace).Get(ctx, opt.ServiceAccountName, metav1.GetOptions{})
if err != nil {
return nil, err
}
for _, localObj := range sa.ImagePullSecrets {
ps, err := client.CoreV1().Secrets(opt.Namespace).Get(ctx, localObj.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
pullSecrets = append(pullSecrets, *ps)
}
k8s, err := kauth.New(ctx, client, kauth.Options(opt))
if err != nil {
return nil, err
}

return NewFromPullSecrets(ctx, pullSecrets)
return authn.NewMultiKeychain(
authn.DefaultKeychain,
google.Keychain,
amazonKeychain,
azureKeychain,
k8s,
), nil
}

// NewInCluster returns a new authn.Keychain suitable for resolving image references as
Expand Down Expand Up @@ -118,67 +79,27 @@ func NewInCluster(ctx context.Context, opt Options) (authn.Keychain, error) {
// remains is an interesting sweet spot: this variant can serve as a credential provider
// for all of the major public clouds, but in library form (vs. an executable you exec).
func NewNoClient(ctx context.Context) (authn.Keychain, error) {
return New(ctx, nil, Options{})
return authn.NewMultiKeychain(
authn.DefaultKeychain,
google.Keychain,
amazonKeychain,
azureKeychain,
), nil
}

// NewFromPullSecrets returns a new authn.Keychain suitable for resolving image references as
// scoped by the pull secrets.
func NewFromPullSecrets(ctx context.Context, pullSecrets []corev1.Secret) (authn.Keychain, error) {
once.Do(func() {
keyring = credentialprovider.NewDockerKeyring()
})

// Extend the default keyring with the pull secrets.
kr, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets, keyring)
k8s, err := kauth.NewFromPullSecrets(ctx, pullSecrets)
if err != nil {
return nil, err
}
return &keychain{
keyring: kr,
}, nil
}

type lazyProvider struct {
kc *keychain
image string
}

// Authorization implements Authenticator.
func (lp lazyProvider) Authorization() (*authn.AuthConfig, error) {
creds, found := lp.kc.keyring.Lookup(lp.image)
if !found || len(creds) < 1 {
return nil, fmt.Errorf("keychain returned no credentials for %q", lp.image)
}
authConfig := creds[0]
return &authn.AuthConfig{
Username: authConfig.Username,
Password: authConfig.Password,
Auth: authConfig.Auth,
IdentityToken: authConfig.IdentityToken,
RegistryToken: authConfig.RegistryToken,
}, nil
}

type keychain struct {
keyring credentialprovider.DockerKeyring
}

// Resolve implements authn.Keychain
func (kc *keychain) Resolve(target authn.Resource) (authn.Authenticator, error) {
var image string
if repo, ok := target.(name.Repository); ok {
image = repo.String()
} else {
// Lookup expects an image reference and we only have a registry.
image = target.RegistryStr() + "/foo/bar"
}

if creds, found := kc.keyring.Lookup(image); !found || len(creds) < 1 {
return authn.Anonymous, nil
}
// TODO(mattmoor): How to support multiple credentials?
return lazyProvider{
kc: kc,
image: image,
}, nil
return authn.NewMultiKeychain(
authn.DefaultKeychain,
google.Keychain,
amazonKeychain,
azureKeychain,
k8s,
), nil
}
23 changes: 0 additions & 23 deletions pkg/authn/k8schain/k8schain_aws.go

This file was deleted.

23 changes: 0 additions & 23 deletions pkg/authn/k8schain/k8schain_azure.go

This file was deleted.

23 changes: 0 additions & 23 deletions pkg/authn/k8schain/k8schain_gcp.go

This file was deleted.

46 changes: 46 additions & 0 deletions pkg/authn/kubernetes/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module github.com/google/go-containerregistry/pkg/authn/kubernetes

go 1.17

require (
github.com/google/go-containerregistry v0.8.0
k8s.io/api v0.22.5
k8s.io/apimachinery v0.22.5
k8s.io/client-go v0.22.5
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.12+incompatible // indirect
github.com/docker/docker v20.10.12+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)
Loading

0 comments on commit a5c1d03

Please sign in to comment.