Check push permissions before building images (#622)

* Check push permissions before building images

* Fix doc comment

* improve error messages
This commit is contained in:
Jason Hall 2019-03-19 13:39:59 -04:00 committed by dlorenc
parent 28bfb75a31
commit 3fa411ceb9
7 changed files with 128 additions and 17 deletions

4
Gopkg.lock generated
View File

@ -430,7 +430,7 @@
version = "v0.2.0"
[[projects]]
digest = "1:3edac9d0a5f7e0e636f85bd7d3105df6180af528ab7e6a88f00b1ae6fc0bf947"
digest = "1:d40a26f0daf07f3b5c916356a3e10fabbf97d5166f77e57aa3983013ab57004c"
name = "github.com/google/go-containerregistry"
packages = [
"pkg/authn",
@ -450,7 +450,7 @@
"pkg/v1/v1util",
]
pruneopts = "NUT"
revision = "8c1640add99804503b4126abc718931a4d93c31a"
revision = "8621d738a07bc74b2adeafd175a3c738423577a0"
[[projects]]
digest = "1:f4f203acd8b11b8747bdcd91696a01dbc95ccb9e2ca2db6abf81c3a4f5e950ce"

View File

@ -37,7 +37,7 @@ required = [
[[constraint]]
name = "github.com/google/go-containerregistry"
revision = "8c1640add99804503b4126abc718931a4d93c31a"
revision = "8621d738a07bc74b2adeafd175a3c738423577a0"
[[override]]
name = "k8s.io/apimachinery"

View File

@ -24,12 +24,11 @@ import (
"strings"
"time"
"github.com/GoogleContainerTools/kaniko/pkg/timing"
"github.com/GoogleContainerTools/kaniko/pkg/buildcontext"
"github.com/GoogleContainerTools/kaniko/pkg/config"
"github.com/GoogleContainerTools/kaniko/pkg/constants"
"github.com/GoogleContainerTools/kaniko/pkg/executor"
"github.com/GoogleContainerTools/kaniko/pkg/timing"
"github.com/GoogleContainerTools/kaniko/pkg/util"
"github.com/genuinetools/amicontained/container"
"github.com/pkg/errors"
@ -79,6 +78,9 @@ var RootCmd = &cobra.Command{
}
logrus.Warn("kaniko is being run outside of a container. This can have dangerous effects on your system")
}
if err := executor.CheckPushPermissions(opts); err != nil {
exit(errors.Wrap(err, "error checking push permissions -- make sure you entered the correct tag name, and that you are authenticated correctly, and try again"))
}
if err := os.Chdir("/"); err != nil {
exit(errors.Wrap(err, "error changing to root dir"))
}

View File

@ -47,6 +47,30 @@ func (w *withUserAgent) RoundTrip(r *http.Request) (*http.Response, error) {
return w.t.RoundTrip(r)
}
// CheckPushPermissionos checks that the configured credentials can be used to
// push to every specified destination.
func CheckPushPermissions(opts *config.KanikoOptions) error {
if opts.NoPush {
return nil
}
checked := map[string]bool{}
for _, destination := range opts.Destinations {
destRef, err := name.NewTag(destination, name.WeakValidation)
if err != nil {
return errors.Wrap(err, "getting tag for destination")
}
if checked[destRef.Context().RepositoryStr()] {
continue
}
if err := remote.CheckPushPermission(destRef, creds.GetKeychain(), http.DefaultTransport); err != nil {
return errors.Wrapf(err, "checking push permission for %q", destRef)
}
checked[destRef.Context().RepositoryStr()] = true
}
return nil
}
// DoPush is responsible for pushing image to the destinations specified in opts
func DoPush(image v1.Image, opts *config.KanikoOptions) error {
t := timing.Start("Total Push Time")

View File

@ -23,7 +23,7 @@ import (
// Manifest represents the OCI image manifest in a structured way.
type Manifest struct {
SchemaVersion int64 `json:"schemaVersion"`
SchemaVersion int64 `json:"schemaVersion,omitempty"`
MediaType types.MediaType `json:"mediaType"`
Config Descriptor `json:"config"`
Layers []Descriptor `json:"layers"`

View File

@ -0,0 +1,56 @@
package remote
import (
"net/http"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
)
// CheckPushPermission returns an error if the given keychain cannot authorize
// a push operation to the given ref.
//
// This can be useful to check whether the caller has permission to push an
// image before doing work to construct the image.
//
// TODO(#412): Remove the need for this method.
func CheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTripper) error {
auth, err := kc.Resolve(ref.Context().Registry)
if err != nil {
return err
}
scopes := []string{ref.Scope(transport.PushScope)}
tr, err := transport.New(ref.Context().Registry, auth, t, scopes)
if err != nil {
return err
}
// TODO(jasonhall): Against GCR, just doing the token handshake is
// enough, but this doesn't extend to Dockerhub
// (https://github.com/docker/hub-feedback/issues/1771), so we actually
// need to initiate an upload to tell whether the credentials can
// authorize a push. Figure out how to return early here when we can,
// to avoid a roundtrip for spec-compliant registries.
w := writer{
ref: ref,
client: &http.Client{Transport: tr},
}
loc, _, err := w.initiateUpload("", "")
if loc != "" {
// Since we're only initiating the upload to check whether we
// can, we should attempt to cancel it, in case initiating
// reserves some resources on the server. We shouldn't wait for
// cancelling to complete, and we don't care if it fails.
go w.cancelUpload(loc)
}
return err
}
func (w *writer) cancelUpload(loc string) {
req, err := http.NewRequest(http.MethodDelete, loc, nil)
if err != nil {
return
}
_, _ = w.client.Do(req)
}

View File

@ -28,31 +28,41 @@ import (
// WriteToFile writes in the compressed format to a tarball, on disk.
// This is just syntactic sugar wrapping tarball.Write with a new file.
func WriteToFile(p string, tag name.Tag, img v1.Image) error {
func WriteToFile(p string, ref name.Reference, img v1.Image) error {
w, err := os.Create(p)
if err != nil {
return err
}
defer w.Close()
return Write(tag, img, w)
return Write(ref, img, w)
}
// MultiWriteToFile writes in the compressed format to a tarball, on disk.
// This is just syntactic sugar wrapping tarball.MultiWrite with a new file.
func MultiWriteToFile(p string, tagToImage map[name.Tag]v1.Image) error {
var refToImage map[name.Reference]v1.Image = make(map[name.Reference]v1.Image, len(tagToImage))
for i, d := range tagToImage {
refToImage[i] = d
}
return MultiRefWriteToFile(p, refToImage)
}
// MultiRefWriteToFile writes in the compressed format to a tarball, on disk.
// This is just syntactic sugar wrapping tarball.MultiRefWrite with a new file.
func MultiRefWriteToFile(p string, refToImage map[name.Reference]v1.Image) error {
w, err := os.Create(p)
if err != nil {
return err
}
defer w.Close()
return MultiWrite(tagToImage, w)
return MultiRefWrite(refToImage, w)
}
// Write is a wrapper to write a single image and tag to a tarball.
func Write(tag name.Tag, img v1.Image, w io.Writer) error {
return MultiWrite(map[name.Tag]v1.Image{tag: img}, w)
func Write(ref name.Reference, img v1.Image, w io.Writer) error {
return MultiRefWrite(map[name.Reference]v1.Image{ref: img}, w)
}
// MultiWrite writes the contents of each image to the provided reader, in the compressed format.
@ -61,10 +71,23 @@ func Write(tag name.Tag, img v1.Image, w io.Writer) error {
// One file for each layer, named after the layer's SHA.
// One file for the config blob, named after its SHA.
func MultiWrite(tagToImage map[name.Tag]v1.Image, w io.Writer) error {
var refToImage map[name.Reference]v1.Image = make(map[name.Reference]v1.Image, len(tagToImage))
for i, d := range tagToImage {
refToImage[i] = d
}
return MultiRefWrite(refToImage, w)
}
// MultiRefWrite writes the contents of each image to the provided reader, in the compressed format.
// The contents are written in the following format:
// One manifest.json file at the top level containing information about several images.
// One file for each layer, named after the layer's SHA.
// One file for the config blob, named after its SHA.
func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error {
tf := tar.NewWriter(w)
defer tf.Close()
imageToTags := dedupTagToImage(tagToImage)
imageToTags := dedupRefToImage(refToImage)
var td tarDescriptor
for img, tags := range imageToTags {
@ -135,14 +158,20 @@ func MultiWrite(tagToImage map[name.Tag]v1.Image, w io.Writer) error {
return writeTarEntry(tf, "manifest.json", bytes.NewReader(tdBytes), int64(len(tdBytes)))
}
func dedupTagToImage(tagToImage map[name.Tag]v1.Image) map[v1.Image][]string {
func dedupRefToImage(refToImage map[name.Reference]v1.Image) map[v1.Image][]string {
imageToTags := make(map[v1.Image][]string)
for tag, img := range tagToImage {
if tags, ok := imageToTags[img]; ok {
imageToTags[img] = append(tags, tag.String())
for ref, img := range refToImage {
if tag, ok := ref.(name.Tag); ok {
if tags, ok := imageToTags[img]; ok && tags != nil {
imageToTags[img] = append(tags, tag.String())
} else {
imageToTags[img] = []string{tag.String()}
}
} else {
imageToTags[img] = []string{tag.String()}
if _, ok := imageToTags[img]; !ok {
imageToTags[img] = nil
}
}
}