Skip to content
This repository has been archived by the owner on Sep 5, 2019. It is now read-only.

Commit

Permalink
update authentication to pass creds to go-containerregistry directly
Browse files Browse the repository at this point in the history
  • Loading branch information
aaron-prindle committed Dec 20, 2018
1 parent 51afcdb commit b8d0318
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 89 deletions.
196 changes: 110 additions & 86 deletions pkg/reconciler/build/resources/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@ package resources
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
mrand "math/rand"
"os"
"path/filepath"
"strconv"
"sync"
Expand All @@ -44,8 +43,10 @@ import (

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1"

containerregv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"

v1alpha1 "github.com/knative/build/pkg/apis/build/v1alpha1"
"github.com/knative/build/pkg/credentials"
"github.com/knative/build/pkg/credentials/dockercreds"
Expand Down Expand Up @@ -601,41 +602,68 @@ func (c *Cache) set(sha string, ep []string) {
c.mtx.Unlock()
}

type AuthToken struct {
type authToken struct {
AccessToken string
Endpoint string
}

type dockerJSON struct {
Auths map[string]registryAuth `json:"auths,omitempty"`
Auths map[string]authEntry `json:"auths"`
HTTPHeaders map[string]string `json:"HttpHeaders"`
}

// authEntry is a helper for JSON parsing an "auth" entry of config.json
// This is not meant for direct consumption.
type authEntry struct {
Auth string `json:"auth"`
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email,omitempty"`
}

type dockerCfg map[string]cfgEntry

type cfgEntry struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Auth string `json:"auth"`
}

type registryAuth struct {
Auth string `json:"auth"`
Email string `json:"email"`
type dockerSecret struct {
Type string `json:"type"`
ProjectID string `json:"project_id"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
ClientEmail string `json:"client_email"`
ClientID string `json:"client_id"`
AuthURI string `json:"auth_uri"`
TokenURI string `json:"token_uri"`
AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"`
ClientX509CertURL string `json:"client_x509_cert_url"`
}

func getGCRAuthorizationKey() ([]AuthToken, error) {
func getGCRAuthorizationKey() ([]authToken, error) {
ts, err := google.DefaultTokenSource(context.TODO(), "https://www.googleapis.com/auth/cloud-platform")
if err != nil {
return []AuthToken{}, err
return []authToken{}, err
}

token, err := ts.Token()
if err != nil {
return []AuthToken{}, err
return []authToken{}, err
}

if !token.Valid() {
return []AuthToken{}, fmt.Errorf("token was invalid")
return []authToken{}, fmt.Errorf("token was invalid")
}

if token.Type() != "Bearer" {
return []AuthToken{}, fmt.Errorf(fmt.Sprintf("expected token type \"Bearer\" but got \"%s\"", token.Type()))
return []authToken{}, fmt.Errorf(fmt.Sprintf("expected token type \"Bearer\" but got \"%s\"", token.Type()))
}

return []AuthToken{
AuthToken{
return []authToken{
authToken{
AccessToken: token.AccessToken,
Endpoint: "https://us.gcr.io"}, //TODO(aaron-prindle) make this work for all regions
}, nil
Expand Down Expand Up @@ -684,13 +712,15 @@ func WaitForSecret(kubeclient kubernetes.Interface, name string, namespace strin
})
}

// GetRemoteEntrypoint accepts a cache of image lookups, as well as the image
// GetRemoteEntrypointAnon accepts a cache of image lookups, as well as the image
// to look for. If the cache does not contain the image, it will lookup the
// metadata from the images registry, and then commit that to the cache
func GetRemoteEntrypointAnon(cache *Cache, image string) ([]string, error) {
// TODO(aaron-prindle) currently cache is checked twice in code path
if ep, ok := cache.get(image); ok {
return ep, nil
}

// verify the image name, then download the remote config file
ref, err := name.ParseReference(image, name.WeakValidation)
if err != nil {
Expand All @@ -704,110 +734,117 @@ func GetRemoteEntrypointAnon(cache *Cache, image string) ([]string, error) {
if err != nil {
return nil, fmt.Errorf("couldn't get config for image %s: %v", image, err)
}
// cache.set(image, cfg.ContainerConfig.Entrypoint)
cache.set(image, cfg.ContainerConfig.Entrypoint)
return cfg.ContainerConfig.Entrypoint, nil
}

// GetRemoteEntrypoint accepts a cache of image lookups, as well as the image
// to look for. If the cache does not contain the image, it will lookup the
// metadata from the images registry, and then commit that to the cache
func GetRemoteEntrypoint(cache *Cache, image string, kubeclient kubernetes.Interface, build *v1alpha1.Build, dockercfgenv string) ([]string, error) {
func GetRemoteEntrypoint(cache *Cache, image string, kubeclient kubernetes.Interface, build *v1alpha1.Build) ([]string, error) {

// try first w/o auth
// if it doesn't succeed, try the other methods
if ep, ok := cache.get(image); ok {
return ep, nil
}

// try first w/o auth, if it doesn't succeed, try the other methods
out, err := GetRemoteEntrypointAnon(cache, image)
if err == nil {
return out, nil
}
if err != nil {
// failing getting w/o anonymous creds continues to try
fmt.Printf("GetRemoteEntrypointAnon FAILED: %v", err)
}

serviceAccountName := build.Spec.ServiceAccountName
var img v1.Image
var img containerregv1.Image
var imgopt remote.ImageOption

ref, err := name.ParseReference(image, name.WeakValidation)
if err != nil {
return nil, fmt.Errorf("couldn't parse image %s: %v", image, err)
}

if serviceAccountName == "" || serviceAccountName == "default" {
// GKE metadata server authentication
tokens, err := getGCRAuthorizationKey()
if err != nil {
return nil, err
}

path := filepath.Join(os.Getenv("HOME"), ".docker")
if _, err := os.Stat(path); os.IsNotExist(err) {
os.Mkdir(path, 0644)
}

if ep, ok := cache.get(image); ok {
return ep, nil
}

// verify the image name, then download the remote config file
ref, err := name.ParseReference(image, name.WeakValidation)
if err != nil {
return nil, fmt.Errorf("couldn't parse image %s: %v", image, err)
}
// TODO(aaron-prindle) have retry setup for the various methods
img, err = remote.Image(ref, remote.WithAuth(
&authn.Bearer{Token: tokens[0].AccessToken}))
if err != nil {
return nil, fmt.Errorf("couldn't get container image info from registry %s: %v", image, err)
}
imgopt = remote.WithAuth(
&authn.Bearer{Token: tokens[0].AccessToken})

} else {
// TODO(aaron-prindle) make sure to try all imagePullSecrets/registries
// TODO(aaron-prindle) see if there is a better way than blocking
WaitForServiceAccount(kubeclient, serviceAccountName, build.Namespace, "desc")
sa, err := kubeclient.CoreV1().ServiceAccounts(build.Namespace).Get(serviceAccountName, metav1.GetOptions{})
if err != nil {
return nil, err // TODO(aaron-prindle) better err msg?
return nil, err // TODO(aaron-prindle) better err msg? wrap errors?
}

for _, secret := range sa.ImagePullSecrets {
// TODO(aaron-prindle) see if there is a better way than blocking
WaitForSecret(kubeclient, secret.Name, build.Namespace, "desc")
scrt, err := kubeclient.CoreV1().Secrets(build.Namespace).Get(secret.Name, metav1.GetOptions{})
if err != nil {
return nil, err // TODO(aaron-prindle) better err msg?
}

// path := filepath.Join(dockercfgenv, ".docker")
path := filepath.Join(os.Getenv("HOME"), ".docker")
if _, err := os.Stat(path); os.IsNotExist(err) {
os.Mkdir(path, 0644)
return nil, err // TODO(aaron-prindle) better err msg? wrap errors?
}
// TODO(aaron-prindle) see if there is a way to pass the auth to go-containerregisty
// to avoid writing .docker/config.json file
// parallelism might be a concern w/ a file
// path = filepath.Join(dockercfgenv, ".docker", "config.json")

// TODO(aaron-prindle) support .dockerconfigjson and .dockercfg
// TODO(aaron-prindle) check scrt.Type directly for type checking?
if _, ok := scrt.Data[".dockerconfigjson"]; ok {
path = filepath.Join(os.Getenv("HOME"), ".docker", "config.json")
err = ioutil.WriteFile(path, scrt.Data[".dockerconfigjson"], 0644)
if err != nil {
dockerconfigjson := scrt.Data[".dockerconfigjson"]
var dat dockerJSON
if err := json.Unmarshal(dockerconfigjson, &dat); err != nil {
return nil, err
}
for _, authEnt := range dat.Auths {
decodedauth, err := base64.StdEncoding.DecodeString(authEnt.Auth)
if err != nil {
return nil, err
}
decodedauthstr := string(decodedauth)
decodedauthstrcut := decodedauthstr[10:]

// convert to yaml and strip beginning part `_json_key:`
var dockerscrt dockerSecret
if err := json.Unmarshal([]byte(decodedauthstrcut), &dockerscrt); err != nil {
return nil, err
}

// dockerscrt.PrivateKey
imgopt = remote.WithAuth(
&authn.Basic{
Username: "_json_key",
Password: decodedauthstrcut,
})
}
} else if _, ok := scrt.Data[".dockercfg"]; ok {
return nil, fmt.Errorf(".dockercfg is currently not supported")
dockercfg := scrt.Data[".dockercfg"]
var dat dockerCfg
if err := json.Unmarshal(dockercfg, &dat); err != nil {
return nil, err
}
// for registry, cfgEnt := range dat {
for _, cfgEnt := range dat {
imgopt = remote.WithAuth(
&authn.Basic{
Username: cfgEnt.Username,
Password: cfgEnt.Password,
})
}
} else {
// TODO(aaron-prindle) warn/error? that no docker info in secret
return nil, fmt.Errorf("no usable secret values was found for image %s", image)
}
}
if ep, ok := cache.get(image); ok {
return ep, nil
}

// verify the image name, then download the remote config file
ref, err := name.ParseReference(image, name.WeakValidation)
if err != nil {
return nil, fmt.Errorf("couldn't parse image %s: %v", image, err)
}
img, err = remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
if err != nil {
return nil, fmt.Errorf("couldn't get container image info from registry %s: %v", image, err)
}
}

img, err = remote.Image(ref, imgopt)
if err != nil {
return nil, fmt.Errorf("error with remote.Image %v", err)
}
cfg, err := img.ConfigFile()
if err != nil {
return nil, fmt.Errorf("couldn't get config for image %s: %v", image, err)
Expand All @@ -816,16 +853,6 @@ func GetRemoteEntrypoint(cache *Cache, image string, kubeclient kubernetes.Inter
return cfg.Config.Entrypoint, nil
}

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func RandStringBytes(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[mrand.Intn(len(letterBytes))]
}
return string(b)
}

// TODO(aaron-prindle) setup the cache properly
var cache = NewCache()

Expand All @@ -836,13 +863,10 @@ var cache = NewCache()
func RedirectSteps(steps []corev1.Container, kubeclient kubernetes.Interface, build *v1alpha1.Build) error {
// For each step with no entrypoint set, try to populate it with the info
// from the remote registry
dockercfgenv := RandStringBytes(10)
dockercfgenv = "/" + dockercfgenv
// gen random string
for i := range steps {
step := &steps[i]
if len(step.Command) == 0 {
ep, err := GetRemoteEntrypoint(cache, step.Image, kubeclient, build, dockercfgenv)
ep, err := GetRemoteEntrypoint(cache, step.Image, kubeclient, build)
if err != nil {
return fmt.Errorf("could not get entrypoint from registry for %s: %v", step.Image, err)
}
Expand Down
Loading

0 comments on commit b8d0318

Please sign in to comment.