mirror of
https://github.com/GoogleContainerTools/kaniko
synced 2026-10-20 09:57:14 +02:00
Look for usable on-cluster credentials using k8schain (#243)
* dep ensure and use k8schain * checkpoint * fix vendoring, stuff builds * Use k8schain for pushes too * Use NewNoClient * update ggcr dep * Move k8schain usage to image_util.go
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// package k8schain exposes an implementation of the authn.Keychain interface
|
||||
// based on the semantics the Kubelet follows when pulling the images for a
|
||||
// Pod in Kubernetes.
|
||||
package k8schain
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package k8schain
|
||||
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
credentialprovidersecrets "k8s.io/kubernetes/pkg/credentialprovider/secrets"
|
||||
|
||||
// Credential providers
|
||||
_ "k8s.io/kubernetes/pkg/credentialprovider/aws"
|
||||
_ "k8s.io/kubernetes/pkg/credentialprovider/azure"
|
||||
_ "k8s.io/kubernetes/pkg/credentialprovider/gcp"
|
||||
// TODO(mattmoor): This doesn't seem to build, figure out why `dep ensure`
|
||||
// is not working and add constraints.
|
||||
// _ "k8s.io/kubernetes/pkg/credentialprovider/rancher"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// origKeyRing is a variable so that testing can adjust it.
|
||||
var origKeyRing = credentialprovider.NewDockerKeyring()
|
||||
|
||||
// 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(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 []v1.Secret
|
||||
if client != nil {
|
||||
for _, name := range opt.ImagePullSecrets {
|
||||
ps, err := client.CoreV1().Secrets(opt.Namespace).Get(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(opt.ServiceAccountName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, localObj := range sa.ImagePullSecrets {
|
||||
ps, err := client.CoreV1().Secrets(opt.Namespace).Get(localObj.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pullSecrets = append(pullSecrets, *ps)
|
||||
}
|
||||
}
|
||||
|
||||
// Third, extend the default keyring with the pull secrets.
|
||||
kr, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets, origKeyRing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &keychain{
|
||||
keyring: kr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewInCluster returns a new authn.Keychain suitable for resolving image references as
|
||||
// scoped by the provided Options, constructing a kubernetes.Interface based on in-cluster
|
||||
// authentication.
|
||||
func NewInCluster(opt Options) (authn.Keychain, error) {
|
||||
clusterConfig, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(clusterConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return New(client, opt)
|
||||
}
|
||||
|
||||
// NewNoClient returns a new authn.Keychain that supports the portions of the K8s keychain
|
||||
// that don't read ImagePullSecrets. This limits it to roughly the Node-identity-based
|
||||
// authentication schemes in Kubernetes pkg/credentialprovider. This version of the
|
||||
// k8schain drops the requirement that we run as a K8s serviceaccount with access to all
|
||||
// of the on-cluster secrets. This drop in fidelity also diminishes its value as a stand-in
|
||||
// for Kubernetes authentication, but this actually targets a different use-case. What
|
||||
// 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() (authn.Keychain, error) {
|
||||
return New(nil, Options{})
|
||||
}
|
||||
|
||||
type lazyProvider credentialprovider.LazyAuthConfiguration
|
||||
|
||||
// Authorization implements Authenticator.
|
||||
func (lp lazyProvider) Authorization() (string, error) {
|
||||
authConfig := credentialprovider.LazyProvide(credentialprovider.LazyAuthConfiguration(lp))
|
||||
if authConfig.Auth != "" {
|
||||
return "Basic " + authConfig.Auth, nil
|
||||
}
|
||||
if authConfig.Username != "" {
|
||||
basic := authn.Basic{
|
||||
Username: authConfig.Username,
|
||||
Password: authConfig.Password,
|
||||
}
|
||||
return basic.Authorization()
|
||||
}
|
||||
return authn.Anonymous.Authorization()
|
||||
}
|
||||
|
||||
type keychain struct {
|
||||
keyring credentialprovider.DockerKeyring
|
||||
}
|
||||
|
||||
// Resolve implements authn.Keychain
|
||||
func (kc *keychain) Resolve(reg name.Registry) (authn.Authenticator, error) {
|
||||
// TODO(mattmoor): Lookup expects an image reference and we only have a registry,
|
||||
// find something better than this.
|
||||
creds, found := kc.keyring.Lookup(reg.String() + "/foo/bar")
|
||||
if !found || len(creds) < 1 {
|
||||
return authn.Anonymous, nil
|
||||
}
|
||||
// TODO(mattmoor): How to support multiple credentials?
|
||||
return lazyProvider(creds[0]), nil
|
||||
}
|
||||
+7
-7
@@ -21,7 +21,7 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
@@ -43,7 +43,7 @@ func configDir() (string, error) {
|
||||
return dc, nil
|
||||
}
|
||||
if h := dockerUserHomeDir(); h != "" {
|
||||
return path.Join(dockerUserHomeDir(), ".docker"), nil
|
||||
return filepath.Join(dockerUserHomeDir(), ".docker"), nil
|
||||
}
|
||||
return "", errNoHomeDir
|
||||
}
|
||||
@@ -100,19 +100,19 @@ var (
|
||||
func (dk *defaultKeychain) Resolve(reg name.Registry) (Authenticator, error) {
|
||||
dir, err := configDir()
|
||||
if err != nil {
|
||||
log.Printf("Unable to determine config dir, falling back on anonymous: %v", err)
|
||||
log.Printf("Unable to determine config dir: %v", err)
|
||||
return Anonymous, nil
|
||||
}
|
||||
file := path.Join(dir, "config.json")
|
||||
file := filepath.Join(dir, "config.json")
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Printf("Unable to read %q, falling back on anonymous: %v", file, err)
|
||||
log.Printf("Unable to read %q: %v", file, err)
|
||||
return Anonymous, nil
|
||||
}
|
||||
|
||||
var cf cfg
|
||||
if err := json.Unmarshal(content, &cf); err != nil {
|
||||
log.Printf("Unable to parse %q, falling back on anonymous: %v", file, err)
|
||||
log.Printf("Unable to parse %q: %v", file, err)
|
||||
return Anonymous, nil
|
||||
}
|
||||
|
||||
@@ -147,6 +147,6 @@ func (dk *defaultKeychain) Resolve(reg name.Registry) (Authenticator, error) {
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("No matching credentials found for %v, falling back on anonymous", reg)
|
||||
// Fallback on anonymous.
|
||||
return Anonymous, nil
|
||||
}
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package authn
|
||||
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
||||
type multiKeychain struct {
|
||||
keychains []Keychain
|
||||
}
|
||||
|
||||
// Assert that our multi-keychain implements Keychain.
|
||||
var _ (Keychain) = (*multiKeychain)(nil)
|
||||
|
||||
// NewMultiKeychain composes a list of keychains into one new keychain.
|
||||
func NewMultiKeychain(kcs ...Keychain) Keychain {
|
||||
return &multiKeychain{keychains: kcs}
|
||||
}
|
||||
|
||||
// Resolve implements Keychain.
|
||||
func (mk *multiKeychain) Resolve(reg name.Registry) (Authenticator, error) {
|
||||
for _, kc := range mk.keychains {
|
||||
auth, err := kc.Resolve(reg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if auth != Anonymous {
|
||||
return auth, nil
|
||||
}
|
||||
}
|
||||
return Anonymous, nil
|
||||
}
|
||||
+1
-1
@@ -79,7 +79,7 @@ func NewRepository(name string, strict Strictness) (Repository, error) {
|
||||
if len(parts) == 2 && (strings.ContainsRune(parts[0], '.') || strings.ContainsRune(parts[0], ':')) {
|
||||
// The first part of the repository is treated as the registry domain
|
||||
// iff it contains a '.' or ':' character, otherwise it is all repository
|
||||
// and the domain defaults to DockerHub.
|
||||
// and the domain defaults to Docker Hub.
|
||||
registry = parts[0]
|
||||
repo = parts[1]
|
||||
}
|
||||
|
||||
+10
-15
@@ -15,6 +15,7 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
@@ -24,7 +25,8 @@ import (
|
||||
// on a remote image
|
||||
func WithTransport(t http.RoundTripper) ImageOption {
|
||||
return func(i *imageOpener) error {
|
||||
return i.setTransport(t)
|
||||
i.transport = t
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +34,8 @@ func WithTransport(t http.RoundTripper) ImageOption {
|
||||
// on a remote image
|
||||
func WithAuth(auth authn.Authenticator) ImageOption {
|
||||
return func(i *imageOpener) error {
|
||||
return i.setAuth(auth)
|
||||
i.auth = auth
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,18 +47,10 @@ func WithAuthFromKeychain(keys authn.Keychain) ImageOption {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.setAuth(auth)
|
||||
if auth == authn.Anonymous {
|
||||
log.Println("No matching credentials were found, falling back on anonymous")
|
||||
}
|
||||
i.auth = auth
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Set client on image using provided transport, and the default authenticator
|
||||
func (i *imageOpener) setTransport(t http.RoundTripper) error {
|
||||
i.transport = t
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set client on image using provided authenticator, and the default transport
|
||||
func (i *imageOpener) setAuth(auth authn.Authenticator) error {
|
||||
i.auth = auth
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user