mirror of
https://github.com/containers/skopeo.git
synced 2025-06-28 07:37:41 +00:00
Merge pull request #49 from projectatomic/split-docker
*: split docker.go for future pkg creation
This commit is contained in:
commit
dbdb03eddb
826
docker.go
826
docker.go
@ -1,826 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/docker/pkg/homedir"
|
|
||||||
"github.com/projectatomic/skopeo/dockerutils"
|
|
||||||
"github.com/projectatomic/skopeo/reference"
|
|
||||||
"github.com/projectatomic/skopeo/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
dockerHostname = "docker.io"
|
|
||||||
dockerRegistry = "registry-1.docker.io"
|
|
||||||
dockerAuthRegistry = "https://index.docker.io/v1/"
|
|
||||||
|
|
||||||
dockerCfg = ".docker"
|
|
||||||
dockerCfgFileName = "config.json"
|
|
||||||
dockerCfgObsolete = ".dockercfg"
|
|
||||||
|
|
||||||
baseURL = "%s://%s/v2/"
|
|
||||||
tagsURL = "%s/tags/list"
|
|
||||||
manifestURL = "%s/manifests/%s"
|
|
||||||
blobsURL = "%s/blobs/%s"
|
|
||||||
blobUploadURL = "%s/blobs/uploads/?digest=%s"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
type errFetchManifest struct {
|
|
||||||
statusCode int
|
|
||||||
body []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e errFetchManifest) Error() string {
|
|
||||||
return fmt.Sprintf("error fetching manifest: status code: %d, body: %s", e.statusCode, string(e.body))
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerImage struct {
|
|
||||||
src *dockerImageSource
|
|
||||||
digest string
|
|
||||||
rawManifest []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *dockerImage) RawManifest(version string) ([]byte, error) {
|
|
||||||
// TODO(runcom): unused version param for now, default to docker v2-1
|
|
||||||
if err := i.retrieveRawManifest(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return i.rawManifest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *dockerImage) Manifest() (types.ImageManifest, error) {
|
|
||||||
// TODO(runcom): unused version param for now, default to docker v2-1
|
|
||||||
m, err := i.getSchema1Manifest()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ms1, ok := m.(*manifestSchema1)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("error retrivieng manifest schema1")
|
|
||||||
}
|
|
||||||
tags, err := i.getTags()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
imgManifest, err := makeImageManifest(i.src.ref.FullName(), ms1, i.digest, tags)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return imgManifest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *dockerImage) getTags() ([]string, error) {
|
|
||||||
// FIXME? Breaking the abstraction.
|
|
||||||
url := fmt.Sprintf(tagsURL, i.src.ref.RemoteName())
|
|
||||||
res, err := i.src.c.makeRequest("GET", url, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
// print url also
|
|
||||||
return nil, fmt.Errorf("Invalid status code returned when fetching tags list %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
type tagsRes struct {
|
|
||||||
Tags []string
|
|
||||||
}
|
|
||||||
tags := &tagsRes{}
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(tags); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tags.Tags, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type config struct {
|
|
||||||
Labels map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type v1Image struct {
|
|
||||||
// Config is the configuration of the container received from the client
|
|
||||||
Config *config `json:"config,omitempty"`
|
|
||||||
// DockerVersion specifies version on which image is built
|
|
||||||
DockerVersion string `json:"docker_version,omitempty"`
|
|
||||||
// Created timestamp when image was created
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
// Architecture is the hardware that the image is build and runs on
|
|
||||||
Architecture string `json:"architecture,omitempty"`
|
|
||||||
// OS is the operating system used to build and run the image
|
|
||||||
OS string `json:"os,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeImageManifest(name string, m *manifestSchema1, dgst string, tagList []string) (types.ImageManifest, error) {
|
|
||||||
v1 := &v1Image{}
|
|
||||||
if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), v1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &types.DockerImageManifest{
|
|
||||||
Name: name,
|
|
||||||
Tag: m.Tag,
|
|
||||||
Digest: dgst,
|
|
||||||
RepoTags: tagList,
|
|
||||||
DockerVersion: v1.DockerVersion,
|
|
||||||
Created: v1.Created,
|
|
||||||
Labels: v1.Config.Labels,
|
|
||||||
Architecture: v1.Architecture,
|
|
||||||
Os: v1.OS,
|
|
||||||
Layers: m.GetLayers(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(runcom)
|
|
||||||
func (i *dockerImage) DockerTar() ([]byte, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// will support v1 one day...
|
|
||||||
type manifest interface {
|
|
||||||
String() string
|
|
||||||
GetLayers() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type manifestSchema1 struct {
|
|
||||||
Name string
|
|
||||||
Tag string
|
|
||||||
FSLayers []struct {
|
|
||||||
BlobSum string `json:"blobSum"`
|
|
||||||
} `json:"fsLayers"`
|
|
||||||
History []struct {
|
|
||||||
V1Compatibility string `json:"v1Compatibility"`
|
|
||||||
} `json:"history"`
|
|
||||||
// TODO(runcom) verify the downloaded manifest
|
|
||||||
//Signature []byte `json:"signature"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *manifestSchema1) GetLayers() []string {
|
|
||||||
layers := make([]string, len(m.FSLayers))
|
|
||||||
for i, layer := range m.FSLayers {
|
|
||||||
layers[i] = layer.BlobSum
|
|
||||||
}
|
|
||||||
return layers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *manifestSchema1) String() string {
|
|
||||||
return fmt.Sprintf("%s-%s", sanitize(m.Name), sanitize(m.Tag))
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitize(s string) string {
|
|
||||||
return strings.Replace(s, "/", "-", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerImageSource struct {
|
|
||||||
ref reference.Named
|
|
||||||
tag string
|
|
||||||
c *dockerClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *dockerImageSource) GetManifest() (manifest []byte, unverifiedCanonicalDigest string, err error) {
|
|
||||||
url := fmt.Sprintf(manifestURL, s.ref.RemoteName(), s.tag)
|
|
||||||
// TODO(runcom) set manifest version header! schema1 for now - then schema2 etc etc and v1
|
|
||||||
// TODO(runcom) NO, switch on the resulter manifest like Docker is doing
|
|
||||||
res, err := s.c.makeRequest("GET", url, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
manblob, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return nil, "", errFetchManifest{res.StatusCode, manblob}
|
|
||||||
}
|
|
||||||
return manblob, res.Header.Get("Docker-Content-Digest"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *dockerImageSource) GetLayer(digest string) (io.ReadCloser, error) {
|
|
||||||
url := fmt.Sprintf(blobsURL, s.ref.RemoteName(), digest)
|
|
||||||
logrus.Infof("Downloading %s", url)
|
|
||||||
res, err := s.c.makeRequest("GET", url, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
// print url also
|
|
||||||
return nil, fmt.Errorf("Invalid status code returned when fetching blob %d", res.StatusCode)
|
|
||||||
}
|
|
||||||
return res.Body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *dockerImageSource) GetSignatures() ([][]byte, error) {
|
|
||||||
return [][]byte{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dockerClient is configuration for dealing with a single Docker registry.
|
|
||||||
type dockerClient struct {
|
|
||||||
registry string
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
wwwAuthenticate string // Cache of a value set by ping() if scheme is not empty
|
|
||||||
scheme string // Cache of a value returned by a successful ping() if not empty
|
|
||||||
transport *http.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *dockerClient) makeRequest(method, url string, headers map[string]string, stream io.Reader) (*http.Response, error) {
|
|
||||||
if c.scheme == "" {
|
|
||||||
pr, err := c.ping()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.wwwAuthenticate = pr.WWWAuthenticate
|
|
||||||
c.scheme = pr.scheme
|
|
||||||
}
|
|
||||||
|
|
||||||
url = fmt.Sprintf(baseURL, c.scheme, c.registry) + url
|
|
||||||
req, err := http.NewRequest(method, url, stream)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Docker-Distribution-API-Version", "registry/2.0")
|
|
||||||
for n, h := range headers {
|
|
||||||
req.Header.Add(n, h)
|
|
||||||
}
|
|
||||||
if c.wwwAuthenticate != "" {
|
|
||||||
if err := c.setupRequestAuth(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client := &http.Client{}
|
|
||||||
if c.transport != nil {
|
|
||||||
client.Transport = c.transport
|
|
||||||
}
|
|
||||||
logrus.Debugf("%s %s", method, url)
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *dockerClient) setupRequestAuth(req *http.Request) error {
|
|
||||||
tokens := strings.SplitN(strings.TrimSpace(c.wwwAuthenticate), " ", 2)
|
|
||||||
if len(tokens) != 2 {
|
|
||||||
return fmt.Errorf("expected 2 tokens in WWW-Authenticate: %d, %s", len(tokens), c.wwwAuthenticate)
|
|
||||||
}
|
|
||||||
switch tokens[0] {
|
|
||||||
case "Basic":
|
|
||||||
req.SetBasicAuth(c.username, c.password)
|
|
||||||
return nil
|
|
||||||
case "Bearer":
|
|
||||||
client := &http.Client{}
|
|
||||||
if c.transport != nil {
|
|
||||||
client.Transport = c.transport
|
|
||||||
}
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
hdr := res.Header.Get("WWW-Authenticate")
|
|
||||||
if hdr == "" || res.StatusCode != http.StatusUnauthorized {
|
|
||||||
// no need for bearer? wtf?
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tokens = strings.Split(hdr, " ")
|
|
||||||
tokens = strings.Split(tokens[1], ",")
|
|
||||||
var realm, service, scope string
|
|
||||||
for _, token := range tokens {
|
|
||||||
if strings.HasPrefix(token, "realm") {
|
|
||||||
realm = strings.Trim(token[len("realm="):], "\"")
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(token, "service") {
|
|
||||||
service = strings.Trim(token[len("service="):], "\"")
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(token, "scope") {
|
|
||||||
scope = strings.Trim(token[len("scope="):], "\"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if realm == "" {
|
|
||||||
return fmt.Errorf("missing realm in bearer auth challenge")
|
|
||||||
}
|
|
||||||
if service == "" {
|
|
||||||
return fmt.Errorf("missing service in bearer auth challenge")
|
|
||||||
}
|
|
||||||
// The scope can be empty if we're not getting a token for a specific repo
|
|
||||||
//if scope == "" && repo != "" {
|
|
||||||
if scope == "" {
|
|
||||||
return fmt.Errorf("missing scope in bearer auth challenge")
|
|
||||||
}
|
|
||||||
token, err := c.getBearerToken(realm, service, scope)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("no handler for %s authentication", tokens[0])
|
|
||||||
// support docker bearer with authconfig's Auth string? see docker2aci
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *dockerClient) getBearerToken(realm, service, scope string) (string, error) {
|
|
||||||
authReq, err := http.NewRequest("GET", realm, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
getParams := authReq.URL.Query()
|
|
||||||
getParams.Add("service", service)
|
|
||||||
if scope != "" {
|
|
||||||
getParams.Add("scope", scope)
|
|
||||||
}
|
|
||||||
authReq.URL.RawQuery = getParams.Encode()
|
|
||||||
if c.username != "" && c.password != "" {
|
|
||||||
authReq.SetBasicAuth(c.username, c.password)
|
|
||||||
}
|
|
||||||
// insecure for now to contact the external token service
|
|
||||||
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
|
||||||
client := &http.Client{Transport: tr}
|
|
||||||
res, err := client.Do(authReq)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
switch res.StatusCode {
|
|
||||||
case http.StatusUnauthorized:
|
|
||||||
return "", fmt.Errorf("unable to retrieve auth token: 401 unauthorized")
|
|
||||||
case http.StatusOK:
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unexpected http code: %d, URL: %s", res.StatusCode, authReq.URL)
|
|
||||||
}
|
|
||||||
tokenBlob, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
tokenStruct := struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}{}
|
|
||||||
if err := json.Unmarshal(tokenBlob, &tokenStruct); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// TODO(runcom): reuse tokens?
|
|
||||||
//hostAuthTokens, ok = rb.hostsV2AuthTokens[req.URL.Host]
|
|
||||||
//if !ok {
|
|
||||||
//hostAuthTokens = make(map[string]string)
|
|
||||||
//rb.hostsV2AuthTokens[req.URL.Host] = hostAuthTokens
|
|
||||||
//}
|
|
||||||
//hostAuthTokens[repo] = tokenStruct.Token
|
|
||||||
return tokenStruct.Token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *dockerImage) retrieveRawManifest() error {
|
|
||||||
if i.rawManifest != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
manblob, unverifiedCanonicalDigest, err := i.src.GetManifest()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
i.rawManifest = manblob
|
|
||||||
i.digest = unverifiedCanonicalDigest
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *dockerImage) getSchema1Manifest() (manifest, error) {
|
|
||||||
if err := i.retrieveRawManifest(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mschema1 := &manifestSchema1{}
|
|
||||||
if err := json.Unmarshal(i.rawManifest, mschema1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := fixManifestLayers(mschema1); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// TODO(runcom): verify manifest schema 1, 2 etc
|
|
||||||
//if len(m.FSLayers) != len(m.History) {
|
|
||||||
//return nil, fmt.Errorf("length of history not equal to number of layers for %q", ref.String())
|
|
||||||
//}
|
|
||||||
//if len(m.FSLayers) == 0 {
|
|
||||||
//return nil, fmt.Errorf("no FSLayers in manifest for %q", ref.String())
|
|
||||||
//}
|
|
||||||
return mschema1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *dockerImage) Layers(layers ...string) error {
|
|
||||||
m, err := i.getSchema1Manifest()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tmpDir, err := ioutil.TempDir(".", "layers-"+m.String()+"-")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dest := NewDirImageDestination(tmpDir)
|
|
||||||
data, err := json.Marshal(m)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := dest.PutManifest(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(layers) == 0 {
|
|
||||||
layers = m.GetLayers()
|
|
||||||
}
|
|
||||||
for _, l := range layers {
|
|
||||||
if !strings.HasPrefix(l, "sha256:") {
|
|
||||||
l = "sha256:" + l
|
|
||||||
}
|
|
||||||
if err := i.getLayer(dest, l); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *dockerImage) getLayer(dest types.ImageDestination, digest string) error {
|
|
||||||
stream, err := i.src.GetLayer(digest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer stream.Close()
|
|
||||||
return dest.PutLayer(digest, stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
|
|
||||||
func newDockerClient(refHostname, certPath string, tlsVerify bool) (*dockerClient, error) {
|
|
||||||
var registry string
|
|
||||||
if refHostname == dockerHostname {
|
|
||||||
registry = dockerRegistry
|
|
||||||
} else {
|
|
||||||
registry = refHostname
|
|
||||||
}
|
|
||||||
username, password, err := getAuth(refHostname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var tr *http.Transport
|
|
||||||
if certPath != "" || !tlsVerify {
|
|
||||||
tlsc := &tls.Config{}
|
|
||||||
|
|
||||||
if certPath != "" {
|
|
||||||
cert, err := tls.LoadX509KeyPair(filepath.Join(certPath, "cert.pem"), filepath.Join(certPath, "key.pem"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error loading x509 key pair: %s", err)
|
|
||||||
}
|
|
||||||
tlsc.Certificates = append(tlsc.Certificates, cert)
|
|
||||||
}
|
|
||||||
tlsc.InsecureSkipVerify = !tlsVerify
|
|
||||||
tr = &http.Transport{
|
|
||||||
TLSClientConfig: tlsc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &dockerClient{
|
|
||||||
registry: registry,
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
transport: tr,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseDockerImageName converts a string into a reference and tag value.
|
|
||||||
func parseDockerImageName(img string) (reference.Named, string, error) {
|
|
||||||
ref, err := reference.ParseNamed(img)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
if reference.IsNameOnly(ref) {
|
|
||||||
ref = reference.WithDefaultTag(ref)
|
|
||||||
}
|
|
||||||
var tag string
|
|
||||||
switch x := ref.(type) {
|
|
||||||
case reference.Canonical:
|
|
||||||
tag = x.Digest().String()
|
|
||||||
case reference.NamedTagged:
|
|
||||||
tag = x.Tag()
|
|
||||||
}
|
|
||||||
return ref, tag, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newDockerImageSource is the same as NewDockerImageSource, only it returns the more specific *dockerImageSource type.
|
|
||||||
func newDockerImageSource(img, certPath string, tlsVerify bool) (*dockerImageSource, error) {
|
|
||||||
ref, tag, err := parseDockerImageName(img)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c, err := newDockerClient(ref.Hostname(), certPath, tlsVerify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &dockerImageSource{
|
|
||||||
ref: ref,
|
|
||||||
tag: tag,
|
|
||||||
c: c,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDockerImageSource creates a new ImageSource for the specified image and connection specification.
|
|
||||||
func NewDockerImageSource(img, certPath string, tlsVerify bool) (types.ImageSource, error) {
|
|
||||||
return newDockerImageSource(img, certPath, tlsVerify)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDockerImage(img, certPath string, tlsVerify bool) (types.Image, error) {
|
|
||||||
s, err := newDockerImageSource(img, certPath, tlsVerify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &dockerImage{src: s}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDefaultConfigDir(confPath string) string {
|
|
||||||
return filepath.Join(homedir.Get(), confPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerAuthConfigObsolete struct {
|
|
||||||
Auth string `json:"auth"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerAuthConfig struct {
|
|
||||||
Auth string `json:"auth,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerConfigFile struct {
|
|
||||||
AuthConfigs map[string]dockerAuthConfig `json:"auths"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeDockerAuth(s string) (string, string, error) {
|
|
||||||
decoded, err := base64.StdEncoding.DecodeString(s)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(string(decoded), ":", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
// if it's invalid just skip, as docker does
|
|
||||||
return "", "", nil
|
|
||||||
}
|
|
||||||
user := parts[0]
|
|
||||||
password := strings.Trim(parts[1], "\x00")
|
|
||||||
return user, password, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAuth(hostname string) (string, string, error) {
|
|
||||||
// TODO(runcom): get this from *cli.Context somehow
|
|
||||||
//if username != "" && password != "" {
|
|
||||||
//return username, password, nil
|
|
||||||
//}
|
|
||||||
if hostname == dockerHostname {
|
|
||||||
hostname = dockerAuthRegistry
|
|
||||||
}
|
|
||||||
dockerCfgPath := filepath.Join(getDefaultConfigDir(".docker"), dockerCfgFileName)
|
|
||||||
if _, err := os.Stat(dockerCfgPath); err == nil {
|
|
||||||
j, err := ioutil.ReadFile(dockerCfgPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
var dockerAuth dockerConfigFile
|
|
||||||
if err := json.Unmarshal(j, &dockerAuth); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
// try the normal case
|
|
||||||
if c, ok := dockerAuth.AuthConfigs[hostname]; ok {
|
|
||||||
return decodeDockerAuth(c.Auth)
|
|
||||||
}
|
|
||||||
} else if os.IsNotExist(err) {
|
|
||||||
oldDockerCfgPath := filepath.Join(getDefaultConfigDir(dockerCfgObsolete))
|
|
||||||
if _, err := os.Stat(oldDockerCfgPath); err != nil {
|
|
||||||
return "", "", nil //missing file is not an error
|
|
||||||
}
|
|
||||||
j, err := ioutil.ReadFile(oldDockerCfgPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
var dockerAuthOld map[string]dockerAuthConfigObsolete
|
|
||||||
if err := json.Unmarshal(j, &dockerAuthOld); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
if c, ok := dockerAuthOld[hostname]; ok {
|
|
||||||
return decodeDockerAuth(c.Auth)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if file is there but we can't stat it for any reason other
|
|
||||||
// than it doesn't exist then stop
|
|
||||||
return "", "", fmt.Errorf("%s - %v", dockerCfgPath, err)
|
|
||||||
}
|
|
||||||
return "", "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiErr struct {
|
|
||||||
Code string
|
|
||||||
Message string
|
|
||||||
Detail interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type pingResponse struct {
|
|
||||||
WWWAuthenticate string
|
|
||||||
APIVersion string
|
|
||||||
scheme string
|
|
||||||
errors []apiErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *dockerClient) ping() (*pingResponse, error) {
|
|
||||||
client := &http.Client{}
|
|
||||||
if c.transport != nil {
|
|
||||||
client.Transport = c.transport
|
|
||||||
}
|
|
||||||
ping := func(scheme string) (*pingResponse, error) {
|
|
||||||
url := fmt.Sprintf(baseURL, scheme, c.registry)
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
logrus.Debugf("Ping %s err %#v", url, err)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
logrus.Debugf("Ping %s status %d", scheme+"://"+c.registry+"/v2/", resp.StatusCode)
|
|
||||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
|
|
||||||
return nil, fmt.Errorf("error pinging repository, response code %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
pr := &pingResponse{}
|
|
||||||
pr.WWWAuthenticate = resp.Header.Get("WWW-Authenticate")
|
|
||||||
pr.APIVersion = resp.Header.Get("Docker-Distribution-Api-Version")
|
|
||||||
pr.scheme = scheme
|
|
||||||
if resp.StatusCode == http.StatusUnauthorized {
|
|
||||||
type APIErrors struct {
|
|
||||||
Errors []apiErr
|
|
||||||
}
|
|
||||||
errs := &APIErrors{}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(errs); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pr.errors = errs.Errors
|
|
||||||
}
|
|
||||||
return pr, nil
|
|
||||||
}
|
|
||||||
scheme := "https"
|
|
||||||
pr, err := ping(scheme)
|
|
||||||
if err != nil {
|
|
||||||
scheme = "http"
|
|
||||||
pr, err = ping(scheme)
|
|
||||||
if err == nil {
|
|
||||||
return pr, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func fixManifestLayers(manifest *manifestSchema1) error {
|
|
||||||
type imageV1 struct {
|
|
||||||
ID string
|
|
||||||
Parent string
|
|
||||||
}
|
|
||||||
imgs := make([]*imageV1, len(manifest.FSLayers))
|
|
||||||
for i := range manifest.FSLayers {
|
|
||||||
img := &imageV1{}
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(manifest.History[i].V1Compatibility), img); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
imgs[i] = img
|
|
||||||
if err := validateV1ID(img.ID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if imgs[len(imgs)-1].Parent != "" {
|
|
||||||
return errors.New("Invalid parent ID in the base layer of the image.")
|
|
||||||
}
|
|
||||||
// check general duplicates to error instead of a deadlock
|
|
||||||
idmap := make(map[string]struct{})
|
|
||||||
var lastID string
|
|
||||||
for _, img := range imgs {
|
|
||||||
// skip IDs that appear after each other, we handle those later
|
|
||||||
if _, exists := idmap[img.ID]; img.ID != lastID && exists {
|
|
||||||
return fmt.Errorf("ID %+v appears multiple times in manifest", img.ID)
|
|
||||||
}
|
|
||||||
lastID = img.ID
|
|
||||||
idmap[lastID] = struct{}{}
|
|
||||||
}
|
|
||||||
// backwards loop so that we keep the remaining indexes after removing items
|
|
||||||
for i := len(imgs) - 2; i >= 0; i-- {
|
|
||||||
if imgs[i].ID == imgs[i+1].ID { // repeated ID. remove and continue
|
|
||||||
manifest.FSLayers = append(manifest.FSLayers[:i], manifest.FSLayers[i+1:]...)
|
|
||||||
manifest.History = append(manifest.History[:i], manifest.History[i+1:]...)
|
|
||||||
} else if imgs[i].Parent != imgs[i+1].ID {
|
|
||||||
return fmt.Errorf("Invalid parent ID. Expected %v, got %v.", imgs[i+1].ID, imgs[i].Parent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateV1ID(id string) error {
|
|
||||||
if ok := validHex.MatchString(id); !ok {
|
|
||||||
return fmt.Errorf("image ID %q is invalid", id)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerImageDestination struct {
|
|
||||||
ref reference.Named
|
|
||||||
tag string
|
|
||||||
c *dockerClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDockerImageDestination creates a new ImageDestination for the specified image and connection specification.
|
|
||||||
func NewDockerImageDestination(img, certPath string, tlsVerify bool) (types.ImageDestination, error) {
|
|
||||||
ref, tag, err := parseDockerImageName(img)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c, err := newDockerClient(ref.Hostname(), certPath, tlsVerify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &dockerImageDestination{
|
|
||||||
ref: ref,
|
|
||||||
tag: tag,
|
|
||||||
c: c,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dockerImageDestination) CanonicalDockerReference() (string, error) {
|
|
||||||
return fmt.Sprintf("%s:%s", d.ref.Name(), d.tag), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dockerImageDestination) PutManifest(manifest []byte) error {
|
|
||||||
// FIXME: This only allows upload by digest, not creating a tag. See the
|
|
||||||
// corresponding comment in NewOpenshiftImageDestination.
|
|
||||||
digest, err := dockerutils.ManifestDigest(manifest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
url := fmt.Sprintf(manifestURL, d.ref.RemoteName(), digest)
|
|
||||||
|
|
||||||
headers := map[string]string{}
|
|
||||||
mimeType := dockerutils.GuessManifestMIMEType(manifest)
|
|
||||||
if mimeType != "" {
|
|
||||||
headers["Content-Type"] = mimeType
|
|
||||||
}
|
|
||||||
res, err := d.c.makeRequest("PUT", url, headers, bytes.NewReader(manifest))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusCreated {
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err == nil {
|
|
||||||
logrus.Debugf("Error body %s", string(body))
|
|
||||||
}
|
|
||||||
logrus.Debugf("Error uploading manifest, status %d, %#v", res.StatusCode, res)
|
|
||||||
return fmt.Errorf("Error uploading manifest to %s, status %d", url, res.StatusCode)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dockerImageDestination) PutLayer(digest string, stream io.Reader) error {
|
|
||||||
checkURL := fmt.Sprintf(blobsURL, d.ref.RemoteName(), digest)
|
|
||||||
|
|
||||||
logrus.Debugf("Checking %s", checkURL)
|
|
||||||
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode == http.StatusOK && res.Header.Get("Docker-Content-Digest") == digest {
|
|
||||||
logrus.Debugf("... already exists, not uploading")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
logrus.Debugf("... failed, status %d", res.StatusCode)
|
|
||||||
|
|
||||||
// FIXME? Chunked upload, progress reporting, etc.
|
|
||||||
uploadURL := fmt.Sprintf(blobUploadURL, d.ref.RemoteName(), digest)
|
|
||||||
logrus.Debugf("Uploading %s", uploadURL)
|
|
||||||
// FIXME: Set Content-Length?
|
|
||||||
res, err = d.c.makeRequest("POST", uploadURL, map[string]string{"Content-Type": "application/octet-stream"}, stream)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusCreated {
|
|
||||||
logrus.Debugf("Error uploading, status %d", res.StatusCode)
|
|
||||||
return fmt.Errorf("Error uploading to %s, status %d", uploadURL, res.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error {
|
|
||||||
if len(signatures) != 0 {
|
|
||||||
return fmt.Errorf("Pushing signatures to a Docker Registry is not supported")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
360
docker_client.go
Normal file
360
docker_client.go
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/pkg/homedir"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dockerHostname = "docker.io"
|
||||||
|
dockerRegistry = "registry-1.docker.io"
|
||||||
|
dockerAuthRegistry = "https://index.docker.io/v1/"
|
||||||
|
|
||||||
|
dockerCfg = ".docker"
|
||||||
|
dockerCfgFileName = "config.json"
|
||||||
|
dockerCfgObsolete = ".dockercfg"
|
||||||
|
|
||||||
|
baseURL = "%s://%s/v2/"
|
||||||
|
tagsURL = "%s/tags/list"
|
||||||
|
manifestURL = "%s/manifests/%s"
|
||||||
|
blobsURL = "%s/blobs/%s"
|
||||||
|
blobUploadURL = "%s/blobs/uploads/?digest=%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
// dockerClient is configuration for dealing with a single Docker registry.
|
||||||
|
type dockerClient struct {
|
||||||
|
registry string
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
wwwAuthenticate string // Cache of a value set by ping() if scheme is not empty
|
||||||
|
scheme string // Cache of a value returned by a successful ping() if not empty
|
||||||
|
transport *http.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
|
||||||
|
func newDockerClient(refHostname, certPath string, tlsVerify bool) (*dockerClient, error) {
|
||||||
|
var registry string
|
||||||
|
if refHostname == dockerHostname {
|
||||||
|
registry = dockerRegistry
|
||||||
|
} else {
|
||||||
|
registry = refHostname
|
||||||
|
}
|
||||||
|
username, password, err := getAuth(refHostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var tr *http.Transport
|
||||||
|
if certPath != "" || !tlsVerify {
|
||||||
|
tlsc := &tls.Config{}
|
||||||
|
|
||||||
|
if certPath != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(filepath.Join(certPath, "cert.pem"), filepath.Join(certPath, "key.pem"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error loading x509 key pair: %s", err)
|
||||||
|
}
|
||||||
|
tlsc.Certificates = append(tlsc.Certificates, cert)
|
||||||
|
}
|
||||||
|
tlsc.InsecureSkipVerify = !tlsVerify
|
||||||
|
tr = &http.Transport{
|
||||||
|
TLSClientConfig: tlsc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &dockerClient{
|
||||||
|
registry: registry,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
transport: tr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) makeRequest(method, url string, headers map[string]string, stream io.Reader) (*http.Response, error) {
|
||||||
|
if c.scheme == "" {
|
||||||
|
pr, err := c.ping()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.wwwAuthenticate = pr.WWWAuthenticate
|
||||||
|
c.scheme = pr.scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
url = fmt.Sprintf(baseURL, c.scheme, c.registry) + url
|
||||||
|
req, err := http.NewRequest(method, url, stream)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Docker-Distribution-API-Version", "registry/2.0")
|
||||||
|
for n, h := range headers {
|
||||||
|
req.Header.Add(n, h)
|
||||||
|
}
|
||||||
|
if c.wwwAuthenticate != "" {
|
||||||
|
if err := c.setupRequestAuth(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client := &http.Client{}
|
||||||
|
if c.transport != nil {
|
||||||
|
client.Transport = c.transport
|
||||||
|
}
|
||||||
|
logrus.Debugf("%s %s", method, url)
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) setupRequestAuth(req *http.Request) error {
|
||||||
|
tokens := strings.SplitN(strings.TrimSpace(c.wwwAuthenticate), " ", 2)
|
||||||
|
if len(tokens) != 2 {
|
||||||
|
return fmt.Errorf("expected 2 tokens in WWW-Authenticate: %d, %s", len(tokens), c.wwwAuthenticate)
|
||||||
|
}
|
||||||
|
switch tokens[0] {
|
||||||
|
case "Basic":
|
||||||
|
req.SetBasicAuth(c.username, c.password)
|
||||||
|
return nil
|
||||||
|
case "Bearer":
|
||||||
|
client := &http.Client{}
|
||||||
|
if c.transport != nil {
|
||||||
|
client.Transport = c.transport
|
||||||
|
}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hdr := res.Header.Get("WWW-Authenticate")
|
||||||
|
if hdr == "" || res.StatusCode != http.StatusUnauthorized {
|
||||||
|
// no need for bearer? wtf?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tokens = strings.Split(hdr, " ")
|
||||||
|
tokens = strings.Split(tokens[1], ",")
|
||||||
|
var realm, service, scope string
|
||||||
|
for _, token := range tokens {
|
||||||
|
if strings.HasPrefix(token, "realm") {
|
||||||
|
realm = strings.Trim(token[len("realm="):], "\"")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(token, "service") {
|
||||||
|
service = strings.Trim(token[len("service="):], "\"")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(token, "scope") {
|
||||||
|
scope = strings.Trim(token[len("scope="):], "\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if realm == "" {
|
||||||
|
return fmt.Errorf("missing realm in bearer auth challenge")
|
||||||
|
}
|
||||||
|
if service == "" {
|
||||||
|
return fmt.Errorf("missing service in bearer auth challenge")
|
||||||
|
}
|
||||||
|
// The scope can be empty if we're not getting a token for a specific repo
|
||||||
|
//if scope == "" && repo != "" {
|
||||||
|
if scope == "" {
|
||||||
|
return fmt.Errorf("missing scope in bearer auth challenge")
|
||||||
|
}
|
||||||
|
token, err := c.getBearerToken(realm, service, scope)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no handler for %s authentication", tokens[0])
|
||||||
|
// support docker bearer with authconfig's Auth string? see docker2aci
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) getBearerToken(realm, service, scope string) (string, error) {
|
||||||
|
authReq, err := http.NewRequest("GET", realm, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
getParams := authReq.URL.Query()
|
||||||
|
getParams.Add("service", service)
|
||||||
|
if scope != "" {
|
||||||
|
getParams.Add("scope", scope)
|
||||||
|
}
|
||||||
|
authReq.URL.RawQuery = getParams.Encode()
|
||||||
|
if c.username != "" && c.password != "" {
|
||||||
|
authReq.SetBasicAuth(c.username, c.password)
|
||||||
|
}
|
||||||
|
// insecure for now to contact the external token service
|
||||||
|
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
||||||
|
client := &http.Client{Transport: tr}
|
||||||
|
res, err := client.Do(authReq)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
switch res.StatusCode {
|
||||||
|
case http.StatusUnauthorized:
|
||||||
|
return "", fmt.Errorf("unable to retrieve auth token: 401 unauthorized")
|
||||||
|
case http.StatusOK:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unexpected http code: %d, URL: %s", res.StatusCode, authReq.URL)
|
||||||
|
}
|
||||||
|
tokenBlob, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
tokenStruct := struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}{}
|
||||||
|
if err := json.Unmarshal(tokenBlob, &tokenStruct); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// TODO(runcom): reuse tokens?
|
||||||
|
//hostAuthTokens, ok = rb.hostsV2AuthTokens[req.URL.Host]
|
||||||
|
//if !ok {
|
||||||
|
//hostAuthTokens = make(map[string]string)
|
||||||
|
//rb.hostsV2AuthTokens[req.URL.Host] = hostAuthTokens
|
||||||
|
//}
|
||||||
|
//hostAuthTokens[repo] = tokenStruct.Token
|
||||||
|
return tokenStruct.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuth(hostname string) (string, string, error) {
|
||||||
|
// TODO(runcom): get this from *cli.Context somehow
|
||||||
|
//if username != "" && password != "" {
|
||||||
|
//return username, password, nil
|
||||||
|
//}
|
||||||
|
if hostname == dockerHostname {
|
||||||
|
hostname = dockerAuthRegistry
|
||||||
|
}
|
||||||
|
dockerCfgPath := filepath.Join(getDefaultConfigDir(".docker"), dockerCfgFileName)
|
||||||
|
if _, err := os.Stat(dockerCfgPath); err == nil {
|
||||||
|
j, err := ioutil.ReadFile(dockerCfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
var dockerAuth dockerConfigFile
|
||||||
|
if err := json.Unmarshal(j, &dockerAuth); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
// try the normal case
|
||||||
|
if c, ok := dockerAuth.AuthConfigs[hostname]; ok {
|
||||||
|
return decodeDockerAuth(c.Auth)
|
||||||
|
}
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
oldDockerCfgPath := filepath.Join(getDefaultConfigDir(dockerCfgObsolete))
|
||||||
|
if _, err := os.Stat(oldDockerCfgPath); err != nil {
|
||||||
|
return "", "", nil //missing file is not an error
|
||||||
|
}
|
||||||
|
j, err := ioutil.ReadFile(oldDockerCfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
var dockerAuthOld map[string]dockerAuthConfigObsolete
|
||||||
|
if err := json.Unmarshal(j, &dockerAuthOld); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if c, ok := dockerAuthOld[hostname]; ok {
|
||||||
|
return decodeDockerAuth(c.Auth)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if file is there but we can't stat it for any reason other
|
||||||
|
// than it doesn't exist then stop
|
||||||
|
return "", "", fmt.Errorf("%s - %v", dockerCfgPath, err)
|
||||||
|
}
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiErr struct {
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
Detail interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type pingResponse struct {
|
||||||
|
WWWAuthenticate string
|
||||||
|
APIVersion string
|
||||||
|
scheme string
|
||||||
|
errors []apiErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *dockerClient) ping() (*pingResponse, error) {
|
||||||
|
client := &http.Client{}
|
||||||
|
if c.transport != nil {
|
||||||
|
client.Transport = c.transport
|
||||||
|
}
|
||||||
|
ping := func(scheme string) (*pingResponse, error) {
|
||||||
|
url := fmt.Sprintf(baseURL, scheme, c.registry)
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
logrus.Debugf("Ping %s err %#v", url, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
logrus.Debugf("Ping %s status %d", scheme+"://"+c.registry+"/v2/", resp.StatusCode)
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
|
||||||
|
return nil, fmt.Errorf("error pinging repository, response code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
pr := &pingResponse{}
|
||||||
|
pr.WWWAuthenticate = resp.Header.Get("WWW-Authenticate")
|
||||||
|
pr.APIVersion = resp.Header.Get("Docker-Distribution-Api-Version")
|
||||||
|
pr.scheme = scheme
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
|
type APIErrors struct {
|
||||||
|
Errors []apiErr
|
||||||
|
}
|
||||||
|
errs := &APIErrors{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(errs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pr.errors = errs.Errors
|
||||||
|
}
|
||||||
|
return pr, nil
|
||||||
|
}
|
||||||
|
scheme := "https"
|
||||||
|
pr, err := ping(scheme)
|
||||||
|
if err != nil {
|
||||||
|
scheme = "http"
|
||||||
|
pr, err = ping(scheme)
|
||||||
|
if err == nil {
|
||||||
|
return pr, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultConfigDir(confPath string) string {
|
||||||
|
return filepath.Join(homedir.Get(), confPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerAuthConfigObsolete struct {
|
||||||
|
Auth string `json:"auth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerAuthConfig struct {
|
||||||
|
Auth string `json:"auth,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerConfigFile struct {
|
||||||
|
AuthConfigs map[string]dockerAuthConfig `json:"auths"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDockerAuth(s string) (string, string, error) {
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(string(decoded), ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
// if it's invalid just skip, as docker does
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
user := parts[0]
|
||||||
|
password := strings.Trim(parts[1], "\x00")
|
||||||
|
return user, password, nil
|
||||||
|
}
|
284
docker_image.go
Normal file
284
docker_image.go
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/projectatomic/skopeo/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type dockerImage struct {
|
||||||
|
src *dockerImageSource
|
||||||
|
digest string
|
||||||
|
rawManifest []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDockerImage(img, certPath string, tlsVerify bool) (types.Image, error) {
|
||||||
|
s, err := newDockerImageSource(img, certPath, tlsVerify)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dockerImage{src: s}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *dockerImage) RawManifest(version string) ([]byte, error) {
|
||||||
|
// TODO(runcom): unused version param for now, default to docker v2-1
|
||||||
|
if err := i.retrieveRawManifest(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return i.rawManifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *dockerImage) Manifest() (types.ImageManifest, error) {
|
||||||
|
// TODO(runcom): unused version param for now, default to docker v2-1
|
||||||
|
m, err := i.getSchema1Manifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ms1, ok := m.(*manifestSchema1)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("error retrivieng manifest schema1")
|
||||||
|
}
|
||||||
|
tags, err := i.getTags()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
imgManifest, err := makeImageManifest(i.src.ref.FullName(), ms1, i.digest, tags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return imgManifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *dockerImage) getTags() ([]string, error) {
|
||||||
|
// FIXME? Breaking the abstraction.
|
||||||
|
url := fmt.Sprintf(tagsURL, i.src.ref.RemoteName())
|
||||||
|
res, err := i.src.c.makeRequest("GET", url, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
// print url also
|
||||||
|
return nil, fmt.Errorf("Invalid status code returned when fetching tags list %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
type tagsRes struct {
|
||||||
|
Tags []string
|
||||||
|
}
|
||||||
|
tags := &tagsRes{}
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(tags); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tags.Tags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Labels map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type v1Image struct {
|
||||||
|
// Config is the configuration of the container received from the client
|
||||||
|
Config *config `json:"config,omitempty"`
|
||||||
|
// DockerVersion specifies version on which image is built
|
||||||
|
DockerVersion string `json:"docker_version,omitempty"`
|
||||||
|
// Created timestamp when image was created
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
// Architecture is the hardware that the image is build and runs on
|
||||||
|
Architecture string `json:"architecture,omitempty"`
|
||||||
|
// OS is the operating system used to build and run the image
|
||||||
|
OS string `json:"os,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeImageManifest(name string, m *manifestSchema1, dgst string, tagList []string) (types.ImageManifest, error) {
|
||||||
|
v1 := &v1Image{}
|
||||||
|
if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), v1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &types.DockerImageManifest{
|
||||||
|
Name: name,
|
||||||
|
Tag: m.Tag,
|
||||||
|
Digest: dgst,
|
||||||
|
RepoTags: tagList,
|
||||||
|
DockerVersion: v1.DockerVersion,
|
||||||
|
Created: v1.Created,
|
||||||
|
Labels: v1.Config.Labels,
|
||||||
|
Architecture: v1.Architecture,
|
||||||
|
Os: v1.OS,
|
||||||
|
Layers: m.GetLayers(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(runcom)
|
||||||
|
func (i *dockerImage) DockerTar() ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// will support v1 one day...
|
||||||
|
type manifest interface {
|
||||||
|
String() string
|
||||||
|
GetLayers() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type manifestSchema1 struct {
|
||||||
|
Name string
|
||||||
|
Tag string
|
||||||
|
FSLayers []struct {
|
||||||
|
BlobSum string `json:"blobSum"`
|
||||||
|
} `json:"fsLayers"`
|
||||||
|
History []struct {
|
||||||
|
V1Compatibility string `json:"v1Compatibility"`
|
||||||
|
} `json:"history"`
|
||||||
|
// TODO(runcom) verify the downloaded manifest
|
||||||
|
//Signature []byte `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manifestSchema1) GetLayers() []string {
|
||||||
|
layers := make([]string, len(m.FSLayers))
|
||||||
|
for i, layer := range m.FSLayers {
|
||||||
|
layers[i] = layer.BlobSum
|
||||||
|
}
|
||||||
|
return layers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manifestSchema1) String() string {
|
||||||
|
return fmt.Sprintf("%s-%s", sanitize(m.Name), sanitize(m.Tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitize(s string) string {
|
||||||
|
return strings.Replace(s, "/", "-", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *dockerImage) retrieveRawManifest() error {
|
||||||
|
if i.rawManifest != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
manblob, unverifiedCanonicalDigest, err := i.src.GetManifest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.rawManifest = manblob
|
||||||
|
i.digest = unverifiedCanonicalDigest
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *dockerImage) getSchema1Manifest() (manifest, error) {
|
||||||
|
if err := i.retrieveRawManifest(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mschema1 := &manifestSchema1{}
|
||||||
|
if err := json.Unmarshal(i.rawManifest, mschema1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := fixManifestLayers(mschema1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// TODO(runcom): verify manifest schema 1, 2 etc
|
||||||
|
//if len(m.FSLayers) != len(m.History) {
|
||||||
|
//return nil, fmt.Errorf("length of history not equal to number of layers for %q", ref.String())
|
||||||
|
//}
|
||||||
|
//if len(m.FSLayers) == 0 {
|
||||||
|
//return nil, fmt.Errorf("no FSLayers in manifest for %q", ref.String())
|
||||||
|
//}
|
||||||
|
return mschema1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *dockerImage) Layers(layers ...string) error {
|
||||||
|
m, err := i.getSchema1Manifest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmpDir, err := ioutil.TempDir(".", "layers-"+m.String()+"-")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dest := NewDirImageDestination(tmpDir)
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := dest.PutManifest(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(layers) == 0 {
|
||||||
|
layers = m.GetLayers()
|
||||||
|
}
|
||||||
|
for _, l := range layers {
|
||||||
|
if !strings.HasPrefix(l, "sha256:") {
|
||||||
|
l = "sha256:" + l
|
||||||
|
}
|
||||||
|
if err := i.getLayer(dest, l); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *dockerImage) getLayer(dest types.ImageDestination, digest string) error {
|
||||||
|
stream, err := i.src.GetLayer(digest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
return dest.PutLayer(digest, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixManifestLayers(manifest *manifestSchema1) error {
|
||||||
|
type imageV1 struct {
|
||||||
|
ID string
|
||||||
|
Parent string
|
||||||
|
}
|
||||||
|
imgs := make([]*imageV1, len(manifest.FSLayers))
|
||||||
|
for i := range manifest.FSLayers {
|
||||||
|
img := &imageV1{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(manifest.History[i].V1Compatibility), img); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
imgs[i] = img
|
||||||
|
if err := validateV1ID(img.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if imgs[len(imgs)-1].Parent != "" {
|
||||||
|
return errors.New("Invalid parent ID in the base layer of the image.")
|
||||||
|
}
|
||||||
|
// check general duplicates to error instead of a deadlock
|
||||||
|
idmap := make(map[string]struct{})
|
||||||
|
var lastID string
|
||||||
|
for _, img := range imgs {
|
||||||
|
// skip IDs that appear after each other, we handle those later
|
||||||
|
if _, exists := idmap[img.ID]; img.ID != lastID && exists {
|
||||||
|
return fmt.Errorf("ID %+v appears multiple times in manifest", img.ID)
|
||||||
|
}
|
||||||
|
lastID = img.ID
|
||||||
|
idmap[lastID] = struct{}{}
|
||||||
|
}
|
||||||
|
// backwards loop so that we keep the remaining indexes after removing items
|
||||||
|
for i := len(imgs) - 2; i >= 0; i-- {
|
||||||
|
if imgs[i].ID == imgs[i+1].ID { // repeated ID. remove and continue
|
||||||
|
manifest.FSLayers = append(manifest.FSLayers[:i], manifest.FSLayers[i+1:]...)
|
||||||
|
manifest.History = append(manifest.History[:i], manifest.History[i+1:]...)
|
||||||
|
} else if imgs[i].Parent != imgs[i+1].ID {
|
||||||
|
return fmt.Errorf("Invalid parent ID. Expected %v, got %v.", imgs[i+1].ID, imgs[i].Parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateV1ID(id string) error {
|
||||||
|
if ok := validHex.MatchString(id); !ok {
|
||||||
|
return fmt.Errorf("image ID %q is invalid", id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
110
docker_image_dest.go
Normal file
110
docker_image_dest.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/projectatomic/skopeo/dockerutils"
|
||||||
|
"github.com/projectatomic/skopeo/reference"
|
||||||
|
"github.com/projectatomic/skopeo/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dockerImageDestination struct {
|
||||||
|
ref reference.Named
|
||||||
|
tag string
|
||||||
|
c *dockerClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDockerImageDestination creates a new ImageDestination for the specified image and connection specification.
|
||||||
|
func NewDockerImageDestination(img, certPath string, tlsVerify bool) (types.ImageDestination, error) {
|
||||||
|
ref, tag, err := parseDockerImageName(img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c, err := newDockerClient(ref.Hostname(), certPath, tlsVerify)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dockerImageDestination{
|
||||||
|
ref: ref,
|
||||||
|
tag: tag,
|
||||||
|
c: c,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dockerImageDestination) CanonicalDockerReference() (string, error) {
|
||||||
|
return fmt.Sprintf("%s:%s", d.ref.Name(), d.tag), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dockerImageDestination) PutManifest(manifest []byte) error {
|
||||||
|
// FIXME: This only allows upload by digest, not creating a tag. See the
|
||||||
|
// corresponding comment in NewOpenshiftImageDestination.
|
||||||
|
digest, err := dockerutils.ManifestDigest(manifest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf(manifestURL, d.ref.RemoteName(), digest)
|
||||||
|
|
||||||
|
headers := map[string]string{}
|
||||||
|
mimeType := dockerutils.GuessManifestMIMEType(manifest)
|
||||||
|
if mimeType != "" {
|
||||||
|
headers["Content-Type"] = mimeType
|
||||||
|
}
|
||||||
|
res, err := d.c.makeRequest("PUT", url, headers, bytes.NewReader(manifest))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusCreated {
|
||||||
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err == nil {
|
||||||
|
logrus.Debugf("Error body %s", string(body))
|
||||||
|
}
|
||||||
|
logrus.Debugf("Error uploading manifest, status %d, %#v", res.StatusCode, res)
|
||||||
|
return fmt.Errorf("Error uploading manifest to %s, status %d", url, res.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dockerImageDestination) PutLayer(digest string, stream io.Reader) error {
|
||||||
|
checkURL := fmt.Sprintf(blobsURL, d.ref.RemoteName(), digest)
|
||||||
|
|
||||||
|
logrus.Debugf("Checking %s", checkURL)
|
||||||
|
res, err := d.c.makeRequest("HEAD", checkURL, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode == http.StatusOK && res.Header.Get("Docker-Content-Digest") == digest {
|
||||||
|
logrus.Debugf("... already exists, not uploading")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logrus.Debugf("... failed, status %d", res.StatusCode)
|
||||||
|
|
||||||
|
// FIXME? Chunked upload, progress reporting, etc.
|
||||||
|
uploadURL := fmt.Sprintf(blobUploadURL, d.ref.RemoteName(), digest)
|
||||||
|
logrus.Debugf("Uploading %s", uploadURL)
|
||||||
|
// FIXME: Set Content-Length?
|
||||||
|
res, err = d.c.makeRequest("POST", uploadURL, map[string]string{"Content-Type": "application/octet-stream"}, stream)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusCreated {
|
||||||
|
logrus.Debugf("Error uploading, status %d", res.StatusCode)
|
||||||
|
return fmt.Errorf("Error uploading to %s, status %d", uploadURL, res.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error {
|
||||||
|
if len(signatures) != 0 {
|
||||||
|
return fmt.Errorf("Pushing signatures to a Docker Registry is not supported")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
86
docker_image_src.go
Normal file
86
docker_image_src.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/projectatomic/skopeo/reference"
|
||||||
|
"github.com/projectatomic/skopeo/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type errFetchManifest struct {
|
||||||
|
statusCode int
|
||||||
|
body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e errFetchManifest) Error() string {
|
||||||
|
return fmt.Sprintf("error fetching manifest: status code: %d, body: %s", e.statusCode, string(e.body))
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerImageSource struct {
|
||||||
|
ref reference.Named
|
||||||
|
tag string
|
||||||
|
c *dockerClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDockerImageSource is the same as NewDockerImageSource, only it returns the more specific *dockerImageSource type.
|
||||||
|
func newDockerImageSource(img, certPath string, tlsVerify bool) (*dockerImageSource, error) {
|
||||||
|
ref, tag, err := parseDockerImageName(img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c, err := newDockerClient(ref.Hostname(), certPath, tlsVerify)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &dockerImageSource{
|
||||||
|
ref: ref,
|
||||||
|
tag: tag,
|
||||||
|
c: c,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDockerImageSource creates a new ImageSource for the specified image and connection specification.
|
||||||
|
func NewDockerImageSource(img, certPath string, tlsVerify bool) (types.ImageSource, error) {
|
||||||
|
return newDockerImageSource(img, certPath, tlsVerify)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dockerImageSource) GetManifest() (manifest []byte, unverifiedCanonicalDigest string, err error) {
|
||||||
|
url := fmt.Sprintf(manifestURL, s.ref.RemoteName(), s.tag)
|
||||||
|
// TODO(runcom) set manifest version header! schema1 for now - then schema2 etc etc and v1
|
||||||
|
// TODO(runcom) NO, switch on the resulter manifest like Docker is doing
|
||||||
|
res, err := s.c.makeRequest("GET", url, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
manblob, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, "", errFetchManifest{res.StatusCode, manblob}
|
||||||
|
}
|
||||||
|
return manblob, res.Header.Get("Docker-Content-Digest"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dockerImageSource) GetLayer(digest string) (io.ReadCloser, error) {
|
||||||
|
url := fmt.Sprintf(blobsURL, s.ref.RemoteName(), digest)
|
||||||
|
logrus.Infof("Downloading %s", url)
|
||||||
|
res, err := s.c.makeRequest("GET", url, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
// print url also
|
||||||
|
return nil, fmt.Errorf("Invalid status code returned when fetching blob %d", res.StatusCode)
|
||||||
|
}
|
||||||
|
return res.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dockerImageSource) GetSignatures() ([][]byte, error) {
|
||||||
|
return [][]byte{}, nil
|
||||||
|
}
|
22
docker_utils.go
Normal file
22
docker_utils.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/projectatomic/skopeo/reference"
|
||||||
|
|
||||||
|
// parseDockerImageName converts a string into a reference and tag value.
|
||||||
|
func parseDockerImageName(img string) (reference.Named, string, error) {
|
||||||
|
ref, err := reference.ParseNamed(img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if reference.IsNameOnly(ref) {
|
||||||
|
ref = reference.WithDefaultTag(ref)
|
||||||
|
}
|
||||||
|
var tag string
|
||||||
|
switch x := ref.(type) {
|
||||||
|
case reference.Canonical:
|
||||||
|
tag = x.Digest().String()
|
||||||
|
case reference.NamedTagged:
|
||||||
|
tag = x.Tag()
|
||||||
|
}
|
||||||
|
return ref, tag, nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user