Use docker and notary API for pull

Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
This commit is contained in:
Riyaz Faizullabhoy 2017-05-11 10:11:15 -07:00
parent ba07bbfb0d
commit abf0028ee8
2 changed files with 211 additions and 37 deletions

View File

@ -10,7 +10,6 @@ import (
"io"
"io/ioutil"
"os"
"os/exec"
"strings"
log "github.com/Sirupsen/logrus"
@ -161,48 +160,22 @@ func dockerRm(container string) error {
func dockerPull(image string, trustedPull bool) error {
log.Debugf("docker pull: %s", image)
docker, err := exec.LookPath("docker")
if err != nil {
return errors.New("Docker does not seem to be installed")
}
var args = []string{"pull"}
if trustedPull {
log.Debugf("pulling %s with content trust", image)
args = append(args, "--disable-content-trust=false")
trustedImg, err := TrustedReference(image)
if err != nil {
return fmt.Errorf("Trusted pull for %s failed: %v", image, err)
}
image = trustedImg.String()
}
args = append(args, image)
cmd := exec.Command(docker, args...)
stderrPipe, err := cmd.StderrPipe()
cli, err := dockerClient()
if err != nil {
return errors.New("could not initialize Docker API client")
}
if _, err := cli.ImagePull(context.Background(), image, types.ImagePullOptions{}); err != nil {
return err
}
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
err = cmd.Start()
if err != nil {
return err
}
_, err = ioutil.ReadAll(stdoutPipe)
if err != nil {
return err
}
stderr, err := ioutil.ReadAll(stderrPipe)
if err != nil {
return err
}
err = cmd.Wait()
if err != nil {
return fmt.Errorf("%v: %s", err, stderr)
}
log.Debugf("docker pull: %s...Done", image)
return nil
}

201
cmd/moby/trust.go Normal file
View File

@ -0,0 +1,201 @@
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/cli/trust"
notaryClient "github.com/docker/notary/client"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/opencontainers/go-digest"
)
// TrustedReference parses an image string, and does a notary lookup to verify and retrieve the signed digest reference
func TrustedReference(image string) (reference.Reference, error) {
ref, err := reference.ParseAnyReference(image)
if err != nil {
return nil, err
}
// to mimic docker pull: if we have a digest already, it's implicitly trusted
if digestRef, ok := ref.(reference.Digested); ok {
return digestRef, nil
}
// to mimic docker pull: if we have a digest already, it's implicitly trusted
if canonicalRef, ok := ref.(reference.Canonical); ok {
return canonicalRef, nil
}
namedRef, ok := ref.(reference.Named)
if !ok {
return nil, errors.New("failed to resolve image digest using content trust: reference is not named")
}
taggedRef, ok := namedRef.(reference.NamedTagged)
if !ok {
return nil, errors.New("failed to resolve image digest using content trust: reference is not tagged")
}
gun := taggedRef.Name()
targetName := taggedRef.Tag()
server, err := getTrustServer(gun)
if err != nil {
return nil, err
}
rt, err := GetReadOnlyAuthTransport(server, []string{gun}, "", "", "")
if err != nil {
return nil, err
}
nRepo, err := notaryClient.NewNotaryRepository(
"",
gun,
server,
rt,
nil,
trustpinning.TrustPinConfig{},
)
if err != nil {
return nil, err
}
target, err := nRepo.GetTargetByName(targetName, trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return nil, err
}
// Only get the tag if it's in the top level targets role or the releases delegation role
// ignore it if it's in any other delegation roles
if target.Role != trust.ReleasesRole && target.Role != data.CanonicalTargetsRole {
return nil, errors.New("not signed in valid role")
}
h, ok := target.Hashes["sha256"]
if !ok {
return nil, errors.New("no valid hash, expecting sha256")
}
dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h))
// Allow returning canonical reference with tag and digest
return reference.WithDigest(taggedRef, dgst)
}
func getTrustServer(gun string) (string, error) {
if strings.HasPrefix(gun, "docker.io/") {
return "https://notary.docker.io", nil
}
return "", errors.New("non-hub images not yet supported")
}
type credentialStore struct {
username string
password string
refreshTokens map[string]string
}
func (tcs *credentialStore) Basic(url *url.URL) (string, string) {
return tcs.username, tcs.password
}
// refresh tokens are the long lived tokens that can be used instead of a password
func (tcs *credentialStore) RefreshToken(u *url.URL, service string) string {
return tcs.refreshTokens[service]
}
func (tcs *credentialStore) SetRefreshToken(u *url.URL, service string, token string) {
if tcs.refreshTokens != nil {
tcs.refreshTokens[service] = token
}
}
// GetReadOnlyAuthTransport gets the Auth Transport used to communicate with notary
func GetReadOnlyAuthTransport(server string, scopes []string, username, password, rootCAPath string) (http.RoundTripper, error) {
httpsTransport, err := httpsTransport(rootCAPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/v2/", server), nil)
if err != nil {
return nil, err
}
pingClient := &http.Client{
Transport: httpsTransport,
Timeout: 5 * time.Second,
}
resp, err := pingClient.Do(req)
if err != nil {
return nil, err
}
challengeManager := challenge.NewSimpleManager()
if err := challengeManager.AddResponse(resp); err != nil {
return nil, err
}
creds := credentialStore{
username: username,
password: password,
refreshTokens: make(map[string]string),
}
var scopeObjs []auth.Scope
for _, scopeName := range scopes {
scopeObjs = append(scopeObjs, auth.RepositoryScope{
Repository: scopeName,
Actions: []string{"pull"},
})
}
// allow setting multiple scopes so we don't have to reauth
tokenHandler := auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{
Transport: httpsTransport,
Credentials: &creds,
Scopes: scopeObjs,
})
authedTransport := transport.NewTransport(httpsTransport, auth.NewAuthorizer(challengeManager, tokenHandler))
return authedTransport, nil
}
func httpsTransport(caFile string) (*http.Transport, error) {
tlsConfig := &tls.Config{}
transport := http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: tlsConfig,
}
// Override with the system cert pool if the caFile was empty
if caFile == "" {
systemCertPool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
transport.TLSClientConfig.RootCAs = systemCertPool
} else {
certPool := x509.NewCertPool()
pems, err := ioutil.ReadFile(caFile)
if err != nil {
return nil, err
}
certPool.AppendCertsFromPEM(pems)
transport.TLSClientConfig.RootCAs = certPool
}
return &transport, nil
}