Merge pull request #17924 from aaronlehmann/content-addressability
Content addressability
This commit is contained in:
commit
a2ab05098d
155 changed files with 11531 additions and 6137 deletions
|
@ -18,16 +18,15 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api"
|
||||
Cli "github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/graph/tags"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/fileutils"
|
||||
"github.com/docker/docker/pkg/httputils"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/ulimit"
|
||||
|
@ -35,6 +34,7 @@ import (
|
|||
"github.com/docker/docker/pkg/urlutil"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
tagpkg "github.com/docker/docker/tag"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
|
@ -323,7 +323,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
|
|||
// Since the build was successful, now we must tag any of the resolved
|
||||
// images from the above Dockerfile rewrite.
|
||||
for _, resolved := range resolvedTags {
|
||||
if err := cli.tagTrusted(resolved.repoInfo, resolved.digestRef, resolved.tagRef); err != nil {
|
||||
if err := cli.tagTrusted(resolved.digestRef, resolved.tagRef); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -333,16 +333,12 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
|
|||
|
||||
// validateTag checks if the given image name can be resolved.
|
||||
func validateTag(rawRepo string) (string, error) {
|
||||
repository, tag := parsers.ParseRepositoryTag(rawRepo)
|
||||
if err := registry.ValidateRepositoryName(repository); err != nil {
|
||||
ref, err := reference.ParseNamed(rawRepo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(tag) == 0 {
|
||||
return rawRepo, nil
|
||||
}
|
||||
|
||||
if err := tags.ValidateTagName(tag); err != nil {
|
||||
if err := registry.ValidateRepositoryName(ref); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
@ -565,15 +561,16 @@ func (td *trustedDockerfile) Close() error {
|
|||
// resolvedTag records the repository, tag, and resolved digest reference
|
||||
// from a Dockerfile rewrite.
|
||||
type resolvedTag struct {
|
||||
repoInfo *registry.RepositoryInfo
|
||||
digestRef, tagRef registry.Reference
|
||||
repoInfo *registry.RepositoryInfo
|
||||
digestRef reference.Canonical
|
||||
tagRef reference.NamedTagged
|
||||
}
|
||||
|
||||
// rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in
|
||||
// "FROM <image>" instructions to a digest reference. `translator` is a
|
||||
// function that takes a repository name and tag reference and returns a
|
||||
// trusted digest reference.
|
||||
func rewriteDockerfileFrom(dockerfileName string, translator func(string, registry.Reference) (registry.Reference, error)) (newDockerfile *trustedDockerfile, resolvedTags []*resolvedTag, err error) {
|
||||
func rewriteDockerfileFrom(dockerfileName string, translator func(reference.NamedTagged) (reference.Canonical, error)) (newDockerfile *trustedDockerfile, resolvedTags []*resolvedTag, err error) {
|
||||
dockerfile, err := os.Open(dockerfileName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to open Dockerfile: %v", err)
|
||||
|
@ -607,29 +604,39 @@ func rewriteDockerfileFrom(dockerfileName string, translator func(string, regist
|
|||
matches := dockerfileFromLinePattern.FindStringSubmatch(line)
|
||||
if matches != nil && matches[1] != "scratch" {
|
||||
// Replace the line with a resolved "FROM repo@digest"
|
||||
repo, tag := parsers.ParseRepositoryTag(matches[1])
|
||||
if tag == "" {
|
||||
tag = tags.DefaultTag
|
||||
}
|
||||
|
||||
repoInfo, err := registry.ParseRepositoryInfo(repo)
|
||||
ref, err := reference.ParseNamed(matches[1])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to parse repository info %q: %v", repo, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ref := registry.ParseReference(tag)
|
||||
digested := false
|
||||
switch ref.(type) {
|
||||
case reference.Tagged:
|
||||
case reference.Digested:
|
||||
digested = true
|
||||
default:
|
||||
ref, err = reference.WithTag(ref, tagpkg.DefaultTag)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !ref.HasDigest() && isTrusted() {
|
||||
trustedRef, err := translator(repo, ref)
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to parse repository info %q: %v", ref.String(), err)
|
||||
}
|
||||
|
||||
if !digested && isTrusted() {
|
||||
trustedRef, err := translator(ref.(reference.NamedTagged))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.ImageName(repo)))
|
||||
line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String()))
|
||||
resolvedTags = append(resolvedTags, &resolvedTag{
|
||||
repoInfo: repoInfo,
|
||||
digestRef: trustedRef,
|
||||
tagRef: ref,
|
||||
tagRef: ref.(reference.NamedTagged),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,15 @@ package client
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
Cli "github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/opts"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
)
|
||||
|
@ -32,20 +33,35 @@ func (cli *DockerCli) CmdCommit(args ...string) error {
|
|||
cmd.ParseFlags(args, true)
|
||||
|
||||
var (
|
||||
name = cmd.Arg(0)
|
||||
repository, tag = parsers.ParseRepositoryTag(cmd.Arg(1))
|
||||
name = cmd.Arg(0)
|
||||
repositoryAndTag = cmd.Arg(1)
|
||||
repositoryName string
|
||||
tag string
|
||||
)
|
||||
|
||||
//Check if the given image name can be resolved
|
||||
if repository != "" {
|
||||
if err := registry.ValidateRepositoryName(repository); err != nil {
|
||||
if repositoryAndTag != "" {
|
||||
ref, err := reference.ParseNamed(repositoryAndTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := registry.ValidateRepositoryName(ref); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repositoryName = ref.Name()
|
||||
|
||||
switch x := ref.(type) {
|
||||
case reference.Digested:
|
||||
return errors.New("cannot commit to digest reference")
|
||||
case reference.Tagged:
|
||||
tag = x.Tag()
|
||||
}
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("container", name)
|
||||
v.Set("repo", repository)
|
||||
v.Set("repo", repositoryName)
|
||||
v.Set("tag", tag)
|
||||
v.Set("comment", *flComment)
|
||||
v.Set("author", *flAuthor)
|
||||
|
|
|
@ -9,12 +9,12 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
Cli "github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/graph/tags"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
tagpkg "github.com/docker/docker/tag"
|
||||
)
|
||||
|
||||
func (cli *DockerCli) pullImage(image string) error {
|
||||
|
@ -23,16 +23,28 @@ func (cli *DockerCli) pullImage(image string) error {
|
|||
|
||||
func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error {
|
||||
v := url.Values{}
|
||||
repos, tag := parsers.ParseRepositoryTag(image)
|
||||
// pull only the image tagged 'latest' if no tag was specified
|
||||
if tag == "" {
|
||||
tag = tags.DefaultTag
|
||||
|
||||
ref, err := reference.ParseNamed(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Set("fromImage", repos)
|
||||
|
||||
var tag string
|
||||
switch x := ref.(type) {
|
||||
case reference.Digested:
|
||||
tag = x.Digest().String()
|
||||
case reference.Tagged:
|
||||
tag = x.Tag()
|
||||
default:
|
||||
// pull only the image tagged 'latest' if no tag was specified
|
||||
tag = tagpkg.DefaultTag
|
||||
}
|
||||
|
||||
v.Set("fromImage", ref.Name())
|
||||
v.Set("tag", tag)
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := registry.ParseRepositoryInfo(repos)
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -94,39 +106,46 @@ func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runc
|
|||
defer containerIDFile.Close()
|
||||
}
|
||||
|
||||
repo, tag := parsers.ParseRepositoryTag(config.Image)
|
||||
if tag == "" {
|
||||
tag = tags.DefaultTag
|
||||
ref, err := reference.ParseNamed(config.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref := registry.ParseReference(tag)
|
||||
var trustedRef registry.Reference
|
||||
|
||||
if isTrusted() && !ref.HasDigest() {
|
||||
var err error
|
||||
trustedRef, err = cli.trustedReference(repo, ref)
|
||||
isDigested := false
|
||||
switch ref.(type) {
|
||||
case reference.Tagged:
|
||||
case reference.Digested:
|
||||
isDigested = true
|
||||
default:
|
||||
ref, err = reference.WithTag(ref, tagpkg.DefaultTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Image = trustedRef.ImageName(repo)
|
||||
}
|
||||
|
||||
var trustedRef reference.Canonical
|
||||
|
||||
if isTrusted() && !isDigested {
|
||||
var err error
|
||||
trustedRef, err = cli.trustedReference(ref.(reference.NamedTagged))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.Image = trustedRef.String()
|
||||
}
|
||||
|
||||
//create the container
|
||||
serverResp, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, nil)
|
||||
//if image not found try to pull it
|
||||
if serverResp.statusCode == 404 && strings.Contains(err.Error(), config.Image) {
|
||||
fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", ref.ImageName(repo))
|
||||
fmt.Fprintf(cli.err, "Unable to find image '%s' locally\n", ref.String())
|
||||
|
||||
// we don't want to write to stdout anything apart from container.ID
|
||||
if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if trustedRef != nil && !ref.HasDigest() {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cli.tagTrusted(repoInfo, trustedRef, ref); err != nil {
|
||||
if trustedRef != nil && !isDigested {
|
||||
if err := cli.tagTrusted(trustedRef, ref.(reference.NamedTagged)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,18 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
Cli "github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/opts"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/parsers/filters"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/units"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
// CmdImages lists the images in a specified repository, or all top-level images if no repository is specified.
|
||||
|
@ -78,9 +78,9 @@ func (cli *DockerCli) CmdImages(args ...string) error {
|
|||
w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0)
|
||||
if !*quiet {
|
||||
if *showDigests {
|
||||
fmt.Fprintln(w, "REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tVIRTUAL SIZE")
|
||||
fmt.Fprintln(w, "REPOSITORY\tTAG\tDIGEST\tIMAGE ID\tCREATED\tSIZE")
|
||||
} else {
|
||||
fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tVIRTUAL SIZE")
|
||||
fmt.Fprintln(w, "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,21 +101,31 @@ func (cli *DockerCli) CmdImages(args ...string) error {
|
|||
// combine the tags and digests lists
|
||||
tagsAndDigests := append(repoTags, repoDigests...)
|
||||
for _, repoAndRef := range tagsAndDigests {
|
||||
repo, ref := parsers.ParseRepositoryTag(repoAndRef)
|
||||
// default tag and digest to none - if there's a value, it'll be set below
|
||||
// default repo, tag, and digest to none - if there's a value, it'll be set below
|
||||
repo := "<none>"
|
||||
tag := "<none>"
|
||||
digest := "<none>"
|
||||
if utils.DigestReference(ref) {
|
||||
digest = ref
|
||||
} else {
|
||||
tag = ref
|
||||
|
||||
if !strings.HasPrefix(repoAndRef, "<none>") {
|
||||
ref, err := reference.ParseNamed(repoAndRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo = ref.Name()
|
||||
|
||||
switch x := ref.(type) {
|
||||
case reference.Digested:
|
||||
digest = x.Digest().String()
|
||||
case reference.Tagged:
|
||||
tag = x.Tag()
|
||||
}
|
||||
}
|
||||
|
||||
if !*quiet {
|
||||
if *showDigests {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", repo, tag, digest, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.VirtualSize)))
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", repo, tag, digest, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.Size)))
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.VirtualSize)))
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\n", repo, tag, ID, units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(image.Created), 0))), units.HumanSize(float64(image.Size)))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(w, ID)
|
||||
|
|
|
@ -6,10 +6,10 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
Cli "github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/opts"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/urlutil"
|
||||
"github.com/docker/docker/registry"
|
||||
)
|
||||
|
@ -47,8 +47,11 @@ func (cli *DockerCli) CmdImport(args ...string) error {
|
|||
|
||||
if repository != "" {
|
||||
//Check if the given image name can be resolved
|
||||
repo, _ := parsers.ParseRepositoryTag(repository)
|
||||
if err := registry.ValidateRepositoryName(repo); err != nil {
|
||||
ref, err := reference.ParseNamed(repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := registry.ValidateRepositoryName(ref); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,8 +62,8 @@ func (c *containerContext) Image() string {
|
|||
return "<no image>"
|
||||
}
|
||||
if c.trunc {
|
||||
if stringid.TruncateID(c.c.ImageID) == stringid.TruncateID(c.c.Image) {
|
||||
return stringutils.Truncate(c.c.Image, 12)
|
||||
if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
|
||||
return trunc
|
||||
}
|
||||
}
|
||||
return c.c.Image
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
Cli "github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/graph/tags"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/registry"
|
||||
tagpkg "github.com/docker/docker/tag"
|
||||
)
|
||||
|
||||
var errTagCantBeUsed = errors.New("tag can't be used with --all-tags/-a")
|
||||
|
||||
// CmdPull pulls an image or a repository from the registry.
|
||||
//
|
||||
// Usage: docker pull [OPTIONS] IMAGENAME[:TAG|@DIGEST]
|
||||
|
@ -23,18 +26,38 @@ func (cli *DockerCli) CmdPull(args ...string) error {
|
|||
cmd.ParseFlags(args, true)
|
||||
remote := cmd.Arg(0)
|
||||
|
||||
taglessRemote, tag := parsers.ParseRepositoryTag(remote)
|
||||
if tag == "" && !*allTags {
|
||||
tag = tags.DefaultTag
|
||||
fmt.Fprintf(cli.out, "Using default tag: %s\n", tag)
|
||||
} else if tag != "" && *allTags {
|
||||
return fmt.Errorf("tag can't be used with --all-tags/-a")
|
||||
distributionRef, err := reference.ParseNamed(remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var tag string
|
||||
switch x := distributionRef.(type) {
|
||||
case reference.Digested:
|
||||
if *allTags {
|
||||
return errTagCantBeUsed
|
||||
}
|
||||
tag = x.Digest().String()
|
||||
case reference.Tagged:
|
||||
if *allTags {
|
||||
return errTagCantBeUsed
|
||||
}
|
||||
tag = x.Tag()
|
||||
default:
|
||||
if !*allTags {
|
||||
tag = tagpkg.DefaultTag
|
||||
distributionRef, err = reference.WithTag(distributionRef, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cli.out, "Using default tag: %s\n", tag)
|
||||
}
|
||||
}
|
||||
|
||||
ref := registry.ParseReference(tag)
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := registry.ParseRepositoryInfo(taglessRemote)
|
||||
repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -46,7 +69,7 @@ func (cli *DockerCli) CmdPull(args ...string) error {
|
|||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("fromImage", ref.ImageName(taglessRemote))
|
||||
v.Set("fromImage", distributionRef.String())
|
||||
|
||||
_, _, err = cli.clientRequestAttemptLogin("POST", "/images/create?"+v.Encode(), nil, cli.out, repoInfo.Index, "pull")
|
||||
return err
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
Cli "github.com/docker/docker/cli"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/registry"
|
||||
)
|
||||
|
||||
|
@ -20,10 +21,21 @@ func (cli *DockerCli) CmdPush(args ...string) error {
|
|||
|
||||
cmd.ParseFlags(args, true)
|
||||
|
||||
remote, tag := parsers.ParseRepositoryTag(cmd.Arg(0))
|
||||
ref, err := reference.ParseNamed(cmd.Arg(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var tag string
|
||||
switch x := ref.(type) {
|
||||
case reference.Digested:
|
||||
return errors.New("cannot push a digest reference")
|
||||
case reference.Tagged:
|
||||
tag = x.Tag()
|
||||
}
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := registry.ParseRepositoryInfo(remote)
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -48,6 +60,6 @@ func (cli *DockerCli) CmdPush(args ...string) error {
|
|||
v := url.Values{}
|
||||
v.Set("tag", tag)
|
||||
|
||||
_, _, err = cli.clientRequestAttemptLogin("POST", "/images/"+remote+"/push?"+v.Encode(), nil, cli.out, repoInfo.Index, "push")
|
||||
_, _, err = cli.clientRequestAttemptLogin("POST", "/images/"+ref.Name()+"/push?"+v.Encode(), nil, cli.out, repoInfo.Index, "push")
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
|
||||
Cli "github.com/docker/docker/cli"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/stringutils"
|
||||
"github.com/docker/docker/registry"
|
||||
)
|
||||
|
@ -38,10 +37,7 @@ func (cli *DockerCli) CmdSearch(args ...string) error {
|
|||
v := url.Values{}
|
||||
v.Set("term", name)
|
||||
|
||||
// Resolve the Repository name from fqn to hostname + name
|
||||
taglessRemote, _ := parsers.ParseRepositoryTag(name)
|
||||
|
||||
indexInfo, err := registry.ParseIndexInfo(taglessRemote)
|
||||
indexInfo, err := registry.ParseSearchIndexInfo(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
Cli "github.com/docker/docker/cli"
|
||||
flag "github.com/docker/docker/pkg/mflag"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/registry"
|
||||
)
|
||||
|
||||
|
@ -19,16 +20,28 @@ func (cli *DockerCli) CmdTag(args ...string) error {
|
|||
|
||||
cmd.ParseFlags(args, true)
|
||||
|
||||
var (
|
||||
repository, tag = parsers.ParseRepositoryTag(cmd.Arg(1))
|
||||
v = url.Values{}
|
||||
)
|
||||
|
||||
//Check if the given image name can be resolved
|
||||
if err := registry.ValidateRepositoryName(repository); err != nil {
|
||||
v := url.Values{}
|
||||
ref, err := reference.ParseNamed(cmd.Arg(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Set("repo", repository)
|
||||
|
||||
_, isDigested := ref.(reference.Digested)
|
||||
if isDigested {
|
||||
return errors.New("refusing to create a tag with a digest reference")
|
||||
}
|
||||
|
||||
tag := ""
|
||||
tagged, isTagged := ref.(reference.Tagged)
|
||||
if isTagged {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
|
||||
//Check if the given image name can be resolved
|
||||
if err := registry.ValidateRepositoryName(ref); err != nil {
|
||||
return err
|
||||
}
|
||||
v.Set("repo", ref.Name())
|
||||
v.Set("tag", tag)
|
||||
|
||||
if *force {
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
|
@ -163,12 +164,12 @@ func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, aut
|
|||
}
|
||||
|
||||
creds := simpleCredentialStore{auth: authConfig}
|
||||
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.CanonicalName, "push", "pull")
|
||||
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.CanonicalName.Name(), "push", "pull")
|
||||
basicHandler := auth.NewBasicHandler(creds)
|
||||
modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)))
|
||||
tr := transport.NewTransport(base, modifiers...)
|
||||
|
||||
return client.NewNotaryRepository(cli.trustDirectory(), repoInfo.CanonicalName, server, tr, cli.getPassphraseRetriever())
|
||||
return client.NewNotaryRepository(cli.trustDirectory(), repoInfo.CanonicalName.Name(), server, tr, cli.getPassphraseRetriever())
|
||||
}
|
||||
|
||||
func convertTarget(t client.Target) (target, error) {
|
||||
|
@ -219,8 +220,8 @@ func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever {
|
|||
}
|
||||
}
|
||||
|
||||
func (cli *DockerCli) trustedReference(repo string, ref registry.Reference) (registry.Reference, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(repo)
|
||||
func (cli *DockerCli) trustedReference(ref reference.NamedTagged) (reference.Canonical, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -234,7 +235,7 @@ func (cli *DockerCli) trustedReference(repo string, ref registry.Reference) (reg
|
|||
return nil, err
|
||||
}
|
||||
|
||||
t, err := notaryRepo.GetTargetByName(ref.String())
|
||||
t, err := notaryRepo.GetTargetByName(ref.Tag())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -244,18 +245,17 @@ func (cli *DockerCli) trustedReference(repo string, ref registry.Reference) (reg
|
|||
|
||||
}
|
||||
|
||||
return registry.DigestReference(r.digest), nil
|
||||
return reference.WithDigest(ref, r.digest)
|
||||
}
|
||||
|
||||
func (cli *DockerCli) tagTrusted(repoInfo *registry.RepositoryInfo, trustedRef, ref registry.Reference) error {
|
||||
fullName := trustedRef.ImageName(repoInfo.LocalName)
|
||||
fmt.Fprintf(cli.out, "Tagging %s as %s\n", fullName, ref.ImageName(repoInfo.LocalName))
|
||||
func (cli *DockerCli) tagTrusted(trustedRef reference.Canonical, ref reference.NamedTagged) error {
|
||||
fmt.Fprintf(cli.out, "Tagging %s as %s\n", trustedRef.String(), ref.String())
|
||||
tv := url.Values{}
|
||||
tv.Set("repo", repoInfo.LocalName)
|
||||
tv.Set("tag", ref.String())
|
||||
tv.Set("repo", trustedRef.Name())
|
||||
tv.Set("tag", ref.Tag())
|
||||
tv.Set("force", "1")
|
||||
|
||||
if _, _, err := readBody(cli.call("POST", "/images/"+fullName+"/tag?"+tv.Encode(), nil, nil)); err != nil {
|
||||
if _, _, err := readBody(cli.call("POST", "/images/"+trustedRef.String()+"/tag?"+tv.Encode(), nil, nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -317,7 +317,7 @@ func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registr
|
|||
refs = append(refs, r)
|
||||
}
|
||||
|
||||
v.Set("fromImage", repoInfo.LocalName)
|
||||
v.Set("fromImage", repoInfo.LocalName.Name())
|
||||
for i, r := range refs {
|
||||
displayTag := r.reference.String()
|
||||
if displayTag != "" {
|
||||
|
@ -333,7 +333,12 @@ func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registr
|
|||
|
||||
// If reference is not trusted, tag by trusted reference
|
||||
if !r.reference.HasDigest() {
|
||||
if err := cli.tagTrusted(repoInfo, registry.DigestReference(r.digest), r.reference); err != nil {
|
||||
tagged, err := reference.WithTag(repoInfo.LocalName, r.reference.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trustedRef, err := reference.WithDigest(repoInfo.LocalName, r.digest)
|
||||
if err := cli.tagTrusted(trustedRef, tagged); err != nil {
|
||||
return err
|
||||
|
||||
}
|
||||
|
@ -386,7 +391,7 @@ func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string,
|
|||
v := url.Values{}
|
||||
v.Set("tag", tag)
|
||||
|
||||
_, _, err := cli.clientRequestAttemptLogin("POST", "/images/"+repoInfo.LocalName+"/push?"+v.Encode(), nil, streamOut, repoInfo.Index, "push")
|
||||
_, _, err := cli.clientRequestAttemptLogin("POST", "/images/"+repoInfo.LocalName.Name()+"/push?"+v.Encode(), nil, streamOut, repoInfo.Index, "push")
|
||||
// Close stream channel to finish target parsing
|
||||
if err := streamOut.Close(); err != nil {
|
||||
return err
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/builder"
|
||||
|
@ -17,17 +19,14 @@ import (
|
|||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/daemon/daemonbuilder"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/graph"
|
||||
"github.com/docker/docker/graph/tags"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/chrootarchive"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/ulimit"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
tagpkg "github.com/docker/docker/tag"
|
||||
"github.com/docker/docker/utils"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
@ -110,26 +109,55 @@ func (s *router) postImagesCreate(ctx context.Context, w http.ResponseWriter, r
|
|||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if image != "" { //pull
|
||||
if tag == "" {
|
||||
image, tag = parsers.ParseRepositoryTag(image)
|
||||
}
|
||||
metaHeaders := map[string][]string{}
|
||||
for k, v := range r.Header {
|
||||
if strings.HasPrefix(k, "X-Meta-") {
|
||||
metaHeaders[k] = v
|
||||
// Special case: "pull -a" may send an image name with a
|
||||
// trailing :. This is ugly, but let's not break API
|
||||
// compatibility.
|
||||
image = strings.TrimSuffix(image, ":")
|
||||
|
||||
var ref reference.Named
|
||||
ref, err = reference.ParseNamed(image)
|
||||
if err == nil {
|
||||
if tag != "" {
|
||||
// The "tag" could actually be a digest.
|
||||
var dgst digest.Digest
|
||||
dgst, err = digest.ParseDigest(tag)
|
||||
if err == nil {
|
||||
ref, err = reference.WithDigest(ref, dgst)
|
||||
} else {
|
||||
ref, err = reference.WithTag(ref, tag)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
metaHeaders := map[string][]string{}
|
||||
for k, v := range r.Header {
|
||||
if strings.HasPrefix(k, "X-Meta-") {
|
||||
metaHeaders[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
err = s.daemon.PullImage(ref, metaHeaders, authConfig, output)
|
||||
}
|
||||
}
|
||||
|
||||
imagePullConfig := &graph.ImagePullConfig{
|
||||
MetaHeaders: metaHeaders,
|
||||
AuthConfig: authConfig,
|
||||
OutStream: output,
|
||||
}
|
||||
|
||||
err = s.daemon.PullImage(image, tag, imagePullConfig)
|
||||
} else { //import
|
||||
if tag == "" {
|
||||
repo, tag = parsers.ParseRepositoryTag(repo)
|
||||
var newRef reference.Named
|
||||
if repo != "" {
|
||||
var err error
|
||||
newRef, err = reference.ParseNamed(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch newRef.(type) {
|
||||
case reference.Digested:
|
||||
return errors.New("cannot import digest reference")
|
||||
}
|
||||
|
||||
if tag != "" {
|
||||
newRef, err = reference.WithTag(newRef, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
src := r.Form.Get("fromSrc")
|
||||
|
@ -143,7 +171,7 @@ func (s *router) postImagesCreate(ctx context.Context, w http.ResponseWriter, r
|
|||
return err
|
||||
}
|
||||
|
||||
err = s.daemon.ImportImage(src, repo, tag, message, r.Body, output, newConfig)
|
||||
err = s.daemon.ImportImage(src, newRef, message, r.Body, output, newConfig)
|
||||
}
|
||||
if err != nil {
|
||||
if !output.Flushed() {
|
||||
|
@ -183,19 +211,25 @@ func (s *router) postImagesPush(ctx context.Context, w http.ResponseWriter, r *h
|
|||
}
|
||||
}
|
||||
|
||||
name := vars["name"]
|
||||
ref, err := reference.ParseNamed(vars["name"])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tag := r.Form.Get("tag")
|
||||
if tag != "" {
|
||||
// Push by digest is not supported, so only tags are supported.
|
||||
ref, err = reference.WithTag(ref, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
output := ioutils.NewWriteFlusher(w)
|
||||
defer output.Close()
|
||||
imagePushConfig := &graph.ImagePushConfig{
|
||||
MetaHeaders: metaHeaders,
|
||||
AuthConfig: authConfig,
|
||||
Tag: r.Form.Get("tag"),
|
||||
OutStream: output,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := s.daemon.PushImage(name, imagePushConfig); err != nil {
|
||||
if err := s.daemon.PushImage(ref, metaHeaders, authConfig, output); err != nil {
|
||||
if !output.Flushed() {
|
||||
return err
|
||||
}
|
||||
|
@ -428,7 +462,7 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R
|
|||
}
|
||||
|
||||
for _, rt := range repoAndTags {
|
||||
if err := s.daemon.TagImage(rt.repo, rt.tag, string(imgID), true); err != nil {
|
||||
if err := s.daemon.TagImage(rt, imgID, true); err != nil {
|
||||
return errf(err)
|
||||
}
|
||||
}
|
||||
|
@ -436,43 +470,38 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R
|
|||
return nil
|
||||
}
|
||||
|
||||
// repoAndTag is a helper struct for holding the parsed repositories and tags of
|
||||
// the input "t" argument.
|
||||
type repoAndTag struct {
|
||||
repo, tag string
|
||||
}
|
||||
|
||||
// sanitizeRepoAndTags parses the raw "t" parameter received from the client
|
||||
// to a slice of repoAndTag.
|
||||
// It also validates each repoName and tag.
|
||||
func sanitizeRepoAndTags(names []string) ([]repoAndTag, error) {
|
||||
func sanitizeRepoAndTags(names []string) ([]reference.Named, error) {
|
||||
var (
|
||||
repoAndTags []repoAndTag
|
||||
repoAndTags []reference.Named
|
||||
// This map is used for deduplicating the "-t" paramter.
|
||||
uniqNames = make(map[string]struct{})
|
||||
)
|
||||
for _, repo := range names {
|
||||
name, tag := parsers.ParseRepositoryTag(repo)
|
||||
if name == "" {
|
||||
if repo == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := registry.ValidateRepositoryName(name); err != nil {
|
||||
ref, err := reference.ParseNamed(repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nameWithTag := name
|
||||
if len(tag) > 0 {
|
||||
if err := tags.ValidateTagName(tag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nameWithTag += ":" + tag
|
||||
} else {
|
||||
nameWithTag += ":" + tags.DefaultTag
|
||||
if _, isDigested := ref.(reference.Digested); isDigested {
|
||||
return nil, errors.New("build tag cannot be a digest")
|
||||
}
|
||||
|
||||
if _, isTagged := ref.(reference.Tagged); !isTagged {
|
||||
ref, err = reference.WithTag(ref, tagpkg.DefaultTag)
|
||||
}
|
||||
|
||||
nameWithTag := ref.String()
|
||||
|
||||
if _, exists := uniqNames[nameWithTag]; !exists {
|
||||
uniqNames[nameWithTag] = struct{}{}
|
||||
repoAndTags = append(repoAndTags, repoAndTag{repo: name, tag: tag})
|
||||
repoAndTags = append(repoAndTags, ref)
|
||||
}
|
||||
}
|
||||
return repoAndTags, nil
|
||||
|
@ -484,7 +513,7 @@ func (s *router) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *ht
|
|||
}
|
||||
|
||||
// FIXME: The filter parameter could just be a match filter
|
||||
images, err := s.daemon.ListImages(r.Form.Get("filters"), r.Form.Get("filter"), httputils.BoolValue(r, "all"))
|
||||
images, err := s.daemon.Images(r.Form.Get("filters"), r.Form.Get("filter"), httputils.BoolValue(r, "all"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -508,9 +537,17 @@ func (s *router) postImagesTag(ctx context.Context, w http.ResponseWriter, r *ht
|
|||
}
|
||||
repo := r.Form.Get("repo")
|
||||
tag := r.Form.Get("tag")
|
||||
name := vars["name"]
|
||||
newTag, err := reference.WithName(repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag != "" {
|
||||
if newTag, err = reference.WithTag(newTag, tag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
force := httputils.BoolValue(r, "force")
|
||||
if err := s.daemon.TagImage(repo, tag, name, force); err != nil {
|
||||
if err := s.daemon.TagImage(newTag, vars["name"], force); err != nil {
|
||||
return err
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
|
|
@ -125,7 +125,7 @@ type Docker interface {
|
|||
// Remove removes a container specified by `id`.
|
||||
Remove(id string, cfg *daemon.ContainerRmConfig) error
|
||||
// Commit creates a new Docker image from an existing Docker container.
|
||||
Commit(string, *daemon.ContainerCommitConfig) (*image.Image, error)
|
||||
Commit(string, *daemon.ContainerCommitConfig) (string, error)
|
||||
// Copy copies/extracts a source FileInfo to a destination path inside a container
|
||||
// specified by a container object.
|
||||
// TODO: make an Extract method instead of passing `decompress`
|
||||
|
|
|
@ -277,9 +277,9 @@ func Commit(containerName string, d *daemon.Daemon, c *CommitConfig) (string, er
|
|||
MergeConfigs: true,
|
||||
}
|
||||
|
||||
img, err := d.Commit(containerName, commitCfg)
|
||||
imgID, err := d.Commit(containerName, commitCfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return img.ID, nil
|
||||
return imgID, nil
|
||||
}
|
||||
|
|
|
@ -83,13 +83,13 @@ func (b *Builder) commit(id string, autoCmd *stringutils.StrSlice, comment strin
|
|||
}
|
||||
|
||||
// Commit the container
|
||||
image, err := b.docker.Commit(id, commitCfg)
|
||||
imageID, err := b.docker.Commit(id, commitCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.docker.Retain(b.id, image.ID)
|
||||
b.activeImages = append(b.activeImages, image.ID)
|
||||
b.image = image.ID
|
||||
b.docker.Retain(b.id, imageID)
|
||||
b.activeImages = append(b.activeImages, imageID)
|
||||
b.image = imageID
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -412,7 +412,7 @@ func containsWildcards(name string) bool {
|
|||
}
|
||||
|
||||
func (b *Builder) processImageFrom(img *image.Image) error {
|
||||
b.image = img.ID
|
||||
b.image = img.ID().String()
|
||||
|
||||
if img.Config != nil {
|
||||
b.runConfig = img.Config
|
||||
|
|
106
daemon/commit.go
106
daemon/commit.go
|
@ -1,10 +1,16 @@
|
|||
package daemon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/dockerversion"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/runconfig"
|
||||
|
@ -25,15 +31,15 @@ type ContainerCommitConfig struct {
|
|||
|
||||
// Commit creates a new filesystem image from the current state of a container.
|
||||
// The image can optionally be tagged into a repository.
|
||||
func (daemon *Daemon) Commit(name string, c *ContainerCommitConfig) (*image.Image, error) {
|
||||
func (daemon *Daemon) Commit(name string, c *ContainerCommitConfig) (string, error) {
|
||||
container, err := daemon.Get(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
// It is not possible to commit a running container on Windows
|
||||
if runtime.GOOS == "windows" && container.IsRunning() {
|
||||
return nil, fmt.Errorf("Windows does not support commit of a running container")
|
||||
return "", fmt.Errorf("Windows does not support commit of a running container")
|
||||
}
|
||||
|
||||
if c.Pause && !container.isPaused() {
|
||||
|
@ -43,13 +49,13 @@ func (daemon *Daemon) Commit(name string, c *ContainerCommitConfig) (*image.Imag
|
|||
|
||||
if c.MergeConfigs {
|
||||
if err := runconfig.Merge(c.Config, container.Config); err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
rwTar, err := daemon.exportContainerRw(container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
if rwTar != nil {
|
||||
|
@ -57,31 +63,99 @@ func (daemon *Daemon) Commit(name string, c *ContainerCommitConfig) (*image.Imag
|
|||
}
|
||||
}()
|
||||
|
||||
// Create a new image from the container's base layers + a new layer from container changes
|
||||
img, err := daemon.graph.Create(rwTar, container.ID, container.ImageID, c.Comment, c.Author, container.Config, c.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var history []image.History
|
||||
rootFS := image.NewRootFS()
|
||||
|
||||
if container.ImageID != "" {
|
||||
img, err := daemon.imageStore.Get(container.ImageID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
history = img.History
|
||||
rootFS = img.RootFS
|
||||
}
|
||||
|
||||
l, err := daemon.layerStore.Register(rwTar, rootFS.ChainID())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer layer.ReleaseAndLog(daemon.layerStore, l)
|
||||
|
||||
h := image.History{
|
||||
Author: c.Author,
|
||||
Created: time.Now().UTC(),
|
||||
CreatedBy: strings.Join(container.Config.Cmd.Slice(), " "),
|
||||
Comment: c.Comment,
|
||||
EmptyLayer: true,
|
||||
}
|
||||
|
||||
if diffID := l.DiffID(); layer.DigestSHA256EmptyTar != diffID {
|
||||
h.EmptyLayer = false
|
||||
rootFS.Append(diffID)
|
||||
}
|
||||
|
||||
history = append(history, h)
|
||||
|
||||
config, err := json.Marshal(&image.Image{
|
||||
V1Image: image.V1Image{
|
||||
DockerVersion: dockerversion.Version,
|
||||
Config: c.Config,
|
||||
Architecture: runtime.GOARCH,
|
||||
OS: runtime.GOOS,
|
||||
Container: container.ID,
|
||||
ContainerConfig: *container.Config,
|
||||
Author: c.Author,
|
||||
Created: h.Created,
|
||||
},
|
||||
RootFS: rootFS,
|
||||
History: history,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := daemon.imageStore.Create(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if container.ImageID != "" {
|
||||
if err := daemon.imageStore.SetParent(id, container.ImageID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Register the image if needed
|
||||
if c.Repo != "" {
|
||||
if err := daemon.repositories.Tag(c.Repo, c.Tag, img.ID, true); err != nil {
|
||||
return img, err
|
||||
newTag, err := reference.WithName(c.Repo) // todo: should move this to API layer
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if c.Tag != "" {
|
||||
if newTag, err = reference.WithTag(newTag, c.Tag); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if err := daemon.TagImage(newTag, id.String(), true); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
daemon.LogContainerEvent(container, "commit")
|
||||
return img, nil
|
||||
return id.String(), nil
|
||||
}
|
||||
|
||||
func (daemon *Daemon) exportContainerRw(container *Container) (archive.Archive, error) {
|
||||
archive, err := daemon.diff(container)
|
||||
if err := daemon.Mount(container); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
archive, err := container.rwlayer.TarStream()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ioutils.NewReadCloserWrapper(archive, func() error {
|
||||
err := archive.Close()
|
||||
return err
|
||||
return daemon.layerStore.Unmount(container.ID)
|
||||
}),
|
||||
nil
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ import (
|
|||
"github.com/docker/docker/daemon/logger/jsonfilelog"
|
||||
"github.com/docker/docker/daemon/network"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/nat"
|
||||
"github.com/docker/docker/pkg/promise"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
|
@ -29,6 +31,8 @@ import (
|
|||
"github.com/docker/docker/volume"
|
||||
)
|
||||
|
||||
const configFileName = "config.v2.json"
|
||||
|
||||
var (
|
||||
// ErrRootFSReadOnly is returned when a container
|
||||
// rootfs is marked readonly.
|
||||
|
@ -43,12 +47,13 @@ type CommonContainer struct {
|
|||
*State `json:"State"` // Needed for remote api version <= 1.11
|
||||
root string // Path to the "home" of the container, including metadata.
|
||||
basefs string // Path to the graphdriver mountpoint
|
||||
rwlayer layer.RWLayer
|
||||
ID string
|
||||
Created time.Time
|
||||
Path string
|
||||
Args []string
|
||||
Config *runconfig.Config
|
||||
ImageID string `json:"Image"`
|
||||
ImageID image.ID `json:"Image"`
|
||||
NetworkSettings *network.Settings
|
||||
LogPath string
|
||||
Name string
|
||||
|
@ -256,7 +261,7 @@ func (container *Container) hostConfigPath() (string, error) {
|
|||
}
|
||||
|
||||
func (container *Container) jsonPath() (string, error) {
|
||||
return container.getRootResourcePath("config.json")
|
||||
return container.getRootResourcePath(configFileName)
|
||||
}
|
||||
|
||||
// This directory is only usable when the container is running
|
||||
|
@ -301,7 +306,7 @@ func (container *Container) StartLogger(cfg runconfig.LogConfig) (logger.Logger,
|
|||
ContainerName: container.Name,
|
||||
ContainerEntrypoint: container.Path,
|
||||
ContainerArgs: container.Args,
|
||||
ContainerImageID: container.ImageID,
|
||||
ContainerImageID: container.ImageID.String(),
|
||||
ContainerImageName: container.Config.Image,
|
||||
ContainerCreated: container.Created,
|
||||
ContainerEnv: container.Config.Env,
|
||||
|
|
|
@ -99,7 +99,7 @@ func TestContainerInitDNS(t *testing.T) {
|
|||
"Name":"/ubuntu","Driver":"aufs","MountLabel":"","ProcessLabel":"","AppArmorProfile":"","RestartCount":0,
|
||||
"UpdateDns":false,"Volumes":{},"VolumesRW":{},"AppliedVolumesFrom":null}`
|
||||
|
||||
if err = ioutil.WriteFile(filepath.Join(containerPath, "config.json"), []byte(config), 0644); err != nil {
|
||||
if err = ioutil.WriteFile(filepath.Join(containerPath, configFileName), []byte(config), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/docker/docker/daemon/links"
|
||||
"github.com/docker/docker/daemon/network"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/pkg/directory"
|
||||
"github.com/docker/docker/pkg/fileutils"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/docker/docker/pkg/mount"
|
||||
|
@ -388,8 +387,7 @@ func (daemon *Daemon) getSize(container *Container) (int64, int64) {
|
|||
}
|
||||
defer daemon.Unmount(container)
|
||||
|
||||
initID := fmt.Sprintf("%s-init", container.ID)
|
||||
sizeRw, err = daemon.driver.DiffSize(container.ID, initID)
|
||||
sizeRw, err = container.rwlayer.Size()
|
||||
if err != nil {
|
||||
logrus.Errorf("Driver %s couldn't return diff size of container %s: %s", daemon.driver, container.ID, err)
|
||||
// FIXME: GetSize should return an error. Not changing it now in case
|
||||
|
@ -397,9 +395,12 @@ func (daemon *Daemon) getSize(container *Container) (int64, int64) {
|
|||
sizeRw = -1
|
||||
}
|
||||
|
||||
if _, err = os.Stat(container.basefs); err == nil {
|
||||
if sizeRootfs, err = directory.Size(container.basefs); err != nil {
|
||||
if parent := container.rwlayer.Parent(); parent != nil {
|
||||
sizeRootfs, err = parent.Size()
|
||||
if err != nil {
|
||||
sizeRootfs = -1
|
||||
} else if sizeRw != -1 {
|
||||
sizeRootfs += sizeRw
|
||||
}
|
||||
}
|
||||
return sizeRw, sizeRootfs
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/docker/docker/daemon/execdriver"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/volume"
|
||||
"github.com/docker/libnetwork"
|
||||
)
|
||||
|
@ -98,22 +99,25 @@ func (daemon *Daemon) populateCommand(c *Container, env []string) error {
|
|||
processConfig.Env = env
|
||||
|
||||
var layerPaths []string
|
||||
img, err := daemon.graph.Get(c.ImageID)
|
||||
img, err := daemon.imageStore.Get(c.ImageID)
|
||||
if err != nil {
|
||||
return derr.ErrorCodeGetGraph.WithArgs(c.ImageID, err)
|
||||
}
|
||||
for i := img; i != nil && err == nil; i, err = daemon.graph.GetParent(i) {
|
||||
lp, err := daemon.driver.Get(i.ID, "")
|
||||
if err != nil {
|
||||
return derr.ErrorCodeGetLayer.WithArgs(daemon.driver.String(), i.ID, err)
|
||||
}
|
||||
layerPaths = append(layerPaths, lp)
|
||||
err = daemon.driver.Put(i.ID)
|
||||
if err != nil {
|
||||
return derr.ErrorCodePutLayer.WithArgs(daemon.driver.String(), i.ID, err)
|
||||
|
||||
if img.RootFS != nil && img.RootFS.Type == "layers+base" {
|
||||
max := len(img.RootFS.DiffIDs)
|
||||
for i := 0; i <= max; i++ {
|
||||
img.RootFS.DiffIDs = img.RootFS.DiffIDs[:i]
|
||||
path, err := layer.GetLayerPath(daemon.layerStore, img.RootFS.ChainID())
|
||||
if err != nil {
|
||||
return derr.ErrorCodeGetLayer.WithArgs(err)
|
||||
}
|
||||
// Reverse order, expecting parent most first
|
||||
layerPaths = append([]string{path}, layerPaths...)
|
||||
}
|
||||
}
|
||||
m, err := daemon.driver.GetMetadata(c.ID)
|
||||
|
||||
m, err := layer.RWLayerMetadata(daemon.layerStore, c.ID)
|
||||
if err != nil {
|
||||
return derr.ErrorCodeGetLayerMetadata.WithArgs(err)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"github.com/docker/docker/api/types"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/volume"
|
||||
|
@ -34,7 +35,7 @@ func (daemon *Daemon) ContainerCreate(params *ContainerCreateConfig) (types.Cont
|
|||
|
||||
container, err := daemon.create(params)
|
||||
if err != nil {
|
||||
return types.ContainerCreateResponse{ID: "", Warnings: warnings}, daemon.graphNotExistToErrcode(params.Config.Image, err)
|
||||
return types.ContainerCreateResponse{ID: "", Warnings: warnings}, daemon.imageNotExistToErrcode(err)
|
||||
}
|
||||
|
||||
return types.ContainerCreateResponse{ID: container.ID, Warnings: warnings}, nil
|
||||
|
@ -45,19 +46,16 @@ func (daemon *Daemon) create(params *ContainerCreateConfig) (retC *Container, re
|
|||
var (
|
||||
container *Container
|
||||
img *image.Image
|
||||
imgID string
|
||||
imgID image.ID
|
||||
err error
|
||||
)
|
||||
|
||||
if params.Config.Image != "" {
|
||||
img, err = daemon.repositories.LookupImage(params.Config.Image)
|
||||
img, err = daemon.GetImage(params.Config.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = daemon.graph.CheckDepth(img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imgID = img.ID
|
||||
imgID = img.ID()
|
||||
}
|
||||
|
||||
if err := daemon.mergeAndVerifyConfig(params.Config, img); err != nil {
|
||||
|
@ -87,15 +85,14 @@ func (daemon *Daemon) create(params *ContainerCreateConfig) (retC *Container, re
|
|||
if err := daemon.Register(container); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
container.Lock()
|
||||
if err := parseSecurityOpt(container, params.HostConfig); err != nil {
|
||||
container.Unlock()
|
||||
rootUID, rootGID, err := idtools.GetRootUIDGID(daemon.uidMaps, daemon.gidMaps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
container.Unlock()
|
||||
if err := daemon.createRootfs(container); err != nil {
|
||||
if err := idtools.MkdirAs(container.root, 0700, rootUID, rootGID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := daemon.setHostConfig(container, params.HostConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
501
daemon/daemon.go
501
daemon/daemon.go
|
@ -18,6 +18,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
|
@ -29,9 +31,13 @@ import (
|
|||
_ "github.com/docker/docker/daemon/graphdriver/vfs" // register vfs
|
||||
"github.com/docker/docker/daemon/logger"
|
||||
"github.com/docker/docker/daemon/network"
|
||||
"github.com/docker/docker/distribution"
|
||||
dmetadata "github.com/docker/docker/distribution/metadata"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/graph"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/image/tarexport"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/migrate/v1"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/discovery"
|
||||
"github.com/docker/docker/pkg/fileutils"
|
||||
|
@ -50,12 +56,14 @@ import (
|
|||
"github.com/docker/docker/pkg/truncindex"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/tag"
|
||||
"github.com/docker/docker/utils"
|
||||
volumedrivers "github.com/docker/docker/volume/drivers"
|
||||
"github.com/docker/docker/volume/local"
|
||||
"github.com/docker/docker/volume/store"
|
||||
"github.com/docker/libnetwork"
|
||||
lntypes "github.com/docker/libnetwork/types"
|
||||
"github.com/docker/libtrust"
|
||||
"github.com/opencontainers/runc/libcontainer"
|
||||
)
|
||||
|
||||
|
@ -66,6 +74,15 @@ var (
|
|||
errSystemNotSupported = errors.New("The Docker daemon is not supported on this platform.")
|
||||
)
|
||||
|
||||
// ErrImageDoesNotExist is error returned when no image can be found for a reference.
|
||||
type ErrImageDoesNotExist struct {
|
||||
RefOrID string
|
||||
}
|
||||
|
||||
func (e ErrImageDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("no such id: %s", e.RefOrID)
|
||||
}
|
||||
|
||||
type contStore struct {
|
||||
s map[string]*Container
|
||||
sync.Mutex
|
||||
|
@ -103,29 +120,33 @@ func (c *contStore) List() []*Container {
|
|||
|
||||
// Daemon holds information about the Docker daemon.
|
||||
type Daemon struct {
|
||||
ID string
|
||||
repository string
|
||||
sysInitPath string
|
||||
containers *contStore
|
||||
execCommands *exec.Store
|
||||
graph *graph.Graph
|
||||
repositories *graph.TagStore
|
||||
idIndex *truncindex.TruncIndex
|
||||
configStore *Config
|
||||
containerGraphDB *graphdb.Database
|
||||
driver graphdriver.Driver
|
||||
execDriver execdriver.Driver
|
||||
statsCollector *statsCollector
|
||||
defaultLogConfig runconfig.LogConfig
|
||||
RegistryService *registry.Service
|
||||
EventsService *events.Events
|
||||
netController libnetwork.NetworkController
|
||||
volumes *store.VolumeStore
|
||||
discoveryWatcher discovery.Watcher
|
||||
root string
|
||||
shutdown bool
|
||||
uidMaps []idtools.IDMap
|
||||
gidMaps []idtools.IDMap
|
||||
ID string
|
||||
repository string
|
||||
sysInitPath string
|
||||
containers *contStore
|
||||
execCommands *exec.Store
|
||||
tagStore tag.Store
|
||||
distributionPool *distribution.Pool
|
||||
distributionMetadataStore dmetadata.Store
|
||||
trustKey libtrust.PrivateKey
|
||||
idIndex *truncindex.TruncIndex
|
||||
configStore *Config
|
||||
containerGraphDB *graphdb.Database
|
||||
driver graphdriver.Driver
|
||||
execDriver execdriver.Driver
|
||||
statsCollector *statsCollector
|
||||
defaultLogConfig runconfig.LogConfig
|
||||
RegistryService *registry.Service
|
||||
EventsService *events.Events
|
||||
netController libnetwork.NetworkController
|
||||
volumes *store.VolumeStore
|
||||
discoveryWatcher discovery.Watcher
|
||||
root string
|
||||
shutdown bool
|
||||
uidMaps []idtools.IDMap
|
||||
gidMaps []idtools.IDMap
|
||||
layerStore layer.Store
|
||||
imageStore image.Store
|
||||
}
|
||||
|
||||
// Get looks for a container using the provided information, which could be
|
||||
|
@ -229,9 +250,7 @@ func (daemon *Daemon) Register(container *Container) error {
|
|||
|
||||
container.unmountIpcMounts(mount.Unmount)
|
||||
|
||||
if err := daemon.Unmount(container); err != nil {
|
||||
logrus.Debugf("unmount error %s", err)
|
||||
}
|
||||
daemon.Unmount(container)
|
||||
if err := container.toDiskLocking(); err != nil {
|
||||
logrus.Errorf("Error saving stopped state to disk: %v", err)
|
||||
}
|
||||
|
@ -456,7 +475,7 @@ func (daemon *Daemon) getEntrypointAndArgs(configEntrypoint *stringutils.StrSlic
|
|||
return cmdSlice[0], cmdSlice[1:]
|
||||
}
|
||||
|
||||
func (daemon *Daemon) newContainer(name string, config *runconfig.Config, imgID string) (*Container, error) {
|
||||
func (daemon *Daemon) newContainer(name string, config *runconfig.Config, imgID image.ID) (*Container, error) {
|
||||
var (
|
||||
id string
|
||||
err error
|
||||
|
@ -542,7 +561,7 @@ func (daemon *Daemon) GetLabels(id string) map[string]string {
|
|||
return container.Config.Labels
|
||||
}
|
||||
|
||||
img, err := daemon.repositories.LookupImage(id)
|
||||
img, err := daemon.GetImage(id)
|
||||
if err == nil {
|
||||
return img.ContainerConfig.Labels
|
||||
}
|
||||
|
@ -702,8 +721,25 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
|
|||
return nil, err
|
||||
}
|
||||
|
||||
logrus.Debug("Creating images graph")
|
||||
g, err := graph.NewGraph(filepath.Join(config.Root, "graph"), d.driver, uidMaps, gidMaps)
|
||||
imageRoot := filepath.Join(config.Root, "image", d.driver.String())
|
||||
fms, err := layer.NewFSMetadataStore(filepath.Join(imageRoot, "layerdb"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.layerStore, err = layer.NewStore(fms, d.driver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
distributionPool := distribution.NewPool()
|
||||
|
||||
ifs, err := image.NewFSStoreBackend(filepath.Join(imageRoot, "imagedb"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.imageStore, err = image.NewImageStore(ifs, d.layerStore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -725,23 +761,24 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
|
|||
return nil, err
|
||||
}
|
||||
|
||||
eventsService := events.New()
|
||||
logrus.Debug("Creating repository list")
|
||||
tagCfg := &graph.TagStoreConfig{
|
||||
Graph: g,
|
||||
Key: trustKey,
|
||||
Registry: registryService,
|
||||
Events: eventsService,
|
||||
}
|
||||
repositories, err := graph.NewTagStore(filepath.Join(config.Root, "repositories-"+d.driver.String()), tagCfg)
|
||||
distributionMetadataStore, err := dmetadata.NewFSMetadataStore(filepath.Join(imageRoot, "distribution"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't create Tag store repositories-%s: %s", d.driver.String(), err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if restorer, ok := d.driver.(graphdriver.ImageRestorer); ok {
|
||||
if _, err := restorer.RestoreCustomImages(repositories, g); err != nil {
|
||||
return nil, fmt.Errorf("Couldn't restore custom images: %s", err)
|
||||
}
|
||||
eventsService := events.New()
|
||||
|
||||
tagStore, err := tag.NewTagStore(filepath.Join(imageRoot, "repositories.json"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't create Tag store repositories: %s", err)
|
||||
}
|
||||
|
||||
if err := restoreCustomImage(d.driver, d.imageStore, d.layerStore, tagStore); err != nil {
|
||||
return nil, fmt.Errorf("Couldn't restore custom images: %s", err)
|
||||
}
|
||||
|
||||
if err := v1.Migrate(config.Root, d.driver.String(), d.layerStore, d.imageStore, tagStore, distributionMetadataStore); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Discovery is only enabled when the daemon is launched with an address to advertise. When
|
||||
|
@ -792,8 +829,10 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
|
|||
d.repository = daemonRepo
|
||||
d.containers = &contStore{s: make(map[string]*Container)}
|
||||
d.execCommands = exec.NewStore()
|
||||
d.graph = g
|
||||
d.repositories = repositories
|
||||
d.tagStore = tagStore
|
||||
d.distributionPool = distributionPool
|
||||
d.distributionMetadataStore = distributionMetadataStore
|
||||
d.trustKey = trustKey
|
||||
d.idIndex = truncindex.NewTruncIndex([]string{})
|
||||
d.configStore = config
|
||||
d.sysInitPath = sysInitPath
|
||||
|
@ -910,28 +949,44 @@ func (daemon *Daemon) Shutdown() error {
|
|||
// Mount sets container.basefs
|
||||
// (is it not set coming in? why is it unset?)
|
||||
func (daemon *Daemon) Mount(container *Container) error {
|
||||
dir, err := daemon.driver.Get(container.ID, container.getMountLabel())
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting container %s from driver %s: %s", container.ID, daemon.driver, err)
|
||||
var layerID layer.ChainID
|
||||
if container.ImageID != "" {
|
||||
img, err := daemon.imageStore.Get(container.ImageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
layerID = img.RootFS.ChainID()
|
||||
}
|
||||
rwlayer, err := daemon.layerStore.Mount(container.ID, layerID, container.getMountLabel(), daemon.setupInitLayer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := rwlayer.Path()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("container mounted via layerStore: %v", dir)
|
||||
|
||||
if container.basefs != dir {
|
||||
// The mount path reported by the graph driver should always be trusted on Windows, since the
|
||||
// volume path for a given mounted layer may change over time. This should only be an error
|
||||
// on non-Windows operating systems.
|
||||
if container.basefs != "" && runtime.GOOS != "windows" {
|
||||
daemon.driver.Put(container.ID)
|
||||
daemon.Unmount(container)
|
||||
return fmt.Errorf("Error: driver %s is returning inconsistent paths for container %s ('%s' then '%s')",
|
||||
daemon.driver, container.ID, container.basefs, dir)
|
||||
}
|
||||
}
|
||||
container.basefs = dir
|
||||
container.basefs = dir // TODO: combine these fields
|
||||
container.rwlayer = rwlayer
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unmount unsets the container base filesystem
|
||||
func (daemon *Daemon) Unmount(container *Container) error {
|
||||
return daemon.driver.Put(container.ID)
|
||||
func (daemon *Daemon) Unmount(container *Container) {
|
||||
if err := daemon.layerStore.Unmount(container.ID); err != nil {
|
||||
logrus.Errorf("Error unmounting container %s: %s", container.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run uses the execution driver to run a given container
|
||||
|
@ -962,82 +1017,46 @@ func (daemon *Daemon) unsubscribeToContainerStats(c *Container, ch chan interfac
|
|||
}
|
||||
|
||||
func (daemon *Daemon) changes(container *Container) ([]archive.Change, error) {
|
||||
initID := fmt.Sprintf("%s-init", container.ID)
|
||||
return daemon.driver.Changes(container.ID, initID)
|
||||
}
|
||||
|
||||
func (daemon *Daemon) diff(container *Container) (archive.Archive, error) {
|
||||
initID := fmt.Sprintf("%s-init", container.ID)
|
||||
return daemon.driver.Diff(container.ID, initID)
|
||||
}
|
||||
|
||||
func (daemon *Daemon) createRootfs(container *Container) error {
|
||||
// Step 1: create the container directory.
|
||||
// This doubles as a barrier to avoid race conditions.
|
||||
rootUID, rootGID, err := idtools.GetRootUIDGID(daemon.uidMaps, daemon.gidMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := idtools.MkdirAs(container.root, 0700, rootUID, rootGID); err != nil {
|
||||
return err
|
||||
}
|
||||
initID := fmt.Sprintf("%s-init", container.ID)
|
||||
|
||||
if err := daemon.driver.Create(initID, container.ImageID, container.getMountLabel()); err != nil {
|
||||
return err
|
||||
}
|
||||
initPath, err := daemon.driver.Get(initID, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := setupInitLayer(initPath, rootUID, rootGID); err != nil {
|
||||
if err := daemon.driver.Put(initID); err != nil {
|
||||
logrus.Errorf("Failed to Put init layer: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// We want to unmount init layer before we take snapshot of it
|
||||
// for the actual container.
|
||||
if err := daemon.driver.Put(initID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := daemon.driver.Create(container.ID, initID, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Graph returns *graph.Graph which can be using for layers graph operations.
|
||||
func (daemon *Daemon) Graph() *graph.Graph {
|
||||
return daemon.graph
|
||||
return daemon.layerStore.Changes(container.ID)
|
||||
}
|
||||
|
||||
// TagImage creates a tag in the repository reponame, pointing to the image named
|
||||
// imageName. If force is true, an existing tag with the same name may be
|
||||
// overwritten.
|
||||
func (daemon *Daemon) TagImage(repoName, tag, imageName string, force bool) error {
|
||||
if err := daemon.repositories.Tag(repoName, tag, imageName, force); err != nil {
|
||||
func (daemon *Daemon) TagImage(newTag reference.Named, imageName string, force bool) error {
|
||||
if _, isDigested := newTag.(reference.Digested); isDigested {
|
||||
return errors.New("refusing to create a tag with a digest reference")
|
||||
}
|
||||
if newTag.Name() == string(digest.Canonical) {
|
||||
return errors.New("refusing to create an ambiguous tag using digest algorithm as name")
|
||||
}
|
||||
|
||||
newTag = registry.NormalizeLocalReference(newTag)
|
||||
imageID, err := daemon.GetImageID(imageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
daemon.EventsService.Log("tag", utils.ImageReference(repoName, tag), "")
|
||||
return nil
|
||||
daemon.EventsService.Log("tag", newTag.String(), "")
|
||||
return daemon.tagStore.Add(newTag, imageID, force)
|
||||
}
|
||||
|
||||
// PullImage initiates a pull operation. image is the repository name to pull, and
|
||||
// tag may be either empty, or indicate a specific tag to pull.
|
||||
func (daemon *Daemon) PullImage(image string, tag string, imagePullConfig *graph.ImagePullConfig) error {
|
||||
return daemon.repositories.Pull(image, tag, imagePullConfig)
|
||||
}
|
||||
func (daemon *Daemon) PullImage(ref reference.Named, metaHeaders map[string][]string, authConfig *cliconfig.AuthConfig, outStream io.Writer) error {
|
||||
imagePullConfig := &distribution.ImagePullConfig{
|
||||
MetaHeaders: metaHeaders,
|
||||
AuthConfig: authConfig,
|
||||
OutStream: outStream,
|
||||
RegistryService: daemon.RegistryService,
|
||||
EventsService: daemon.EventsService,
|
||||
MetadataStore: daemon.distributionMetadataStore,
|
||||
LayerStore: daemon.layerStore,
|
||||
ImageStore: daemon.imageStore,
|
||||
TagStore: daemon.tagStore,
|
||||
Pool: daemon.distributionPool,
|
||||
}
|
||||
|
||||
// ImportImage imports an image, getting the archived layer data either from
|
||||
// inConfig (if src is "-"), or from a URI specified in src. Progress output is
|
||||
// written to outStream. Repository and tag names can optionally be given in
|
||||
// the repo and tag arguments, respectively.
|
||||
func (daemon *Daemon) ImportImage(src, repo, tag, msg string, inConfig io.ReadCloser, outStream io.Writer, containerConfig *runconfig.Config) error {
|
||||
return daemon.repositories.Import(src, repo, tag, msg, inConfig, outStream, containerConfig)
|
||||
return distribution.Pull(ref, imagePullConfig)
|
||||
}
|
||||
|
||||
// ExportImage exports a list of images to the given output stream. The
|
||||
|
@ -1046,47 +1065,214 @@ func (daemon *Daemon) ImportImage(src, repo, tag, msg string, inConfig io.ReadCl
|
|||
// the same tag are exported. names is the set of tags to export, and
|
||||
// outStream is the writer which the images are written to.
|
||||
func (daemon *Daemon) ExportImage(names []string, outStream io.Writer) error {
|
||||
return daemon.repositories.ImageExport(names, outStream)
|
||||
imageExporter := tarexport.NewTarExporter(daemon.imageStore, daemon.layerStore, daemon.tagStore)
|
||||
return imageExporter.Save(names, outStream)
|
||||
}
|
||||
|
||||
// PushImage initiates a push operation on the repository named localName.
|
||||
func (daemon *Daemon) PushImage(localName string, imagePushConfig *graph.ImagePushConfig) error {
|
||||
return daemon.repositories.Push(localName, imagePushConfig)
|
||||
func (daemon *Daemon) PushImage(ref reference.Named, metaHeaders map[string][]string, authConfig *cliconfig.AuthConfig, outStream io.Writer) error {
|
||||
imagePushConfig := &distribution.ImagePushConfig{
|
||||
MetaHeaders: metaHeaders,
|
||||
AuthConfig: authConfig,
|
||||
OutStream: outStream,
|
||||
RegistryService: daemon.RegistryService,
|
||||
EventsService: daemon.EventsService,
|
||||
MetadataStore: daemon.distributionMetadataStore,
|
||||
LayerStore: daemon.layerStore,
|
||||
ImageStore: daemon.imageStore,
|
||||
TagStore: daemon.tagStore,
|
||||
TrustKey: daemon.trustKey,
|
||||
}
|
||||
|
||||
return distribution.Push(ref, imagePushConfig)
|
||||
}
|
||||
|
||||
// LookupImage looks up an image by name and returns it as an ImageInspect
|
||||
// structure.
|
||||
func (daemon *Daemon) LookupImage(name string) (*types.ImageInspect, error) {
|
||||
return daemon.repositories.Lookup(name)
|
||||
img, err := daemon.GetImage(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("No such image: %s", name)
|
||||
}
|
||||
|
||||
refs := daemon.tagStore.References(img.ID())
|
||||
repoTags := []string{}
|
||||
repoDigests := []string{}
|
||||
for _, ref := range refs {
|
||||
switch ref.(type) {
|
||||
case reference.Tagged:
|
||||
repoTags = append(repoTags, ref.String())
|
||||
case reference.Digested:
|
||||
repoDigests = append(repoDigests, ref.String())
|
||||
}
|
||||
}
|
||||
|
||||
var size int64
|
||||
var layerMetadata map[string]string
|
||||
layerID := img.RootFS.ChainID()
|
||||
if layerID != "" {
|
||||
l, err := daemon.layerStore.Get(layerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer layer.ReleaseAndLog(daemon.layerStore, l)
|
||||
size, err = l.Size()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
layerMetadata, err = l.Metadata()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
imageInspect := &types.ImageInspect{
|
||||
ID: img.ID().String(),
|
||||
RepoTags: repoTags,
|
||||
RepoDigests: repoDigests,
|
||||
Parent: img.Parent.String(),
|
||||
Comment: img.Comment,
|
||||
Created: img.Created.Format(time.RFC3339Nano),
|
||||
Container: img.Container,
|
||||
ContainerConfig: &img.ContainerConfig,
|
||||
DockerVersion: img.DockerVersion,
|
||||
Author: img.Author,
|
||||
Config: img.Config,
|
||||
Architecture: img.Architecture,
|
||||
Os: img.OS,
|
||||
Size: size,
|
||||
VirtualSize: size, // TODO: field unused, deprecate
|
||||
}
|
||||
|
||||
imageInspect.GraphDriver.Name = daemon.driver.String()
|
||||
|
||||
imageInspect.GraphDriver.Data = layerMetadata
|
||||
|
||||
return imageInspect, nil
|
||||
}
|
||||
|
||||
// LoadImage uploads a set of images into the repository. This is the
|
||||
// complement of ImageExport. The input stream is an uncompressed tar
|
||||
// ball containing images and metadata.
|
||||
func (daemon *Daemon) LoadImage(inTar io.ReadCloser, outStream io.Writer) error {
|
||||
return daemon.repositories.Load(inTar, outStream)
|
||||
}
|
||||
|
||||
// ListImages returns a filtered list of images. filterArgs is a JSON-encoded set
|
||||
// of filter arguments which will be interpreted by pkg/parsers/filters.
|
||||
// filter is a shell glob string applied to repository names. The argument
|
||||
// named all controls whether all images in the graph are filtered, or just
|
||||
// the heads.
|
||||
func (daemon *Daemon) ListImages(filterArgs, filter string, all bool) ([]*types.Image, error) {
|
||||
return daemon.repositories.Images(filterArgs, filter, all)
|
||||
imageExporter := tarexport.NewTarExporter(daemon.imageStore, daemon.layerStore, daemon.tagStore)
|
||||
return imageExporter.Load(inTar, outStream)
|
||||
}
|
||||
|
||||
// ImageHistory returns a slice of ImageHistory structures for the specified image
|
||||
// name by walking the image lineage.
|
||||
func (daemon *Daemon) ImageHistory(name string) ([]*types.ImageHistory, error) {
|
||||
return daemon.repositories.History(name)
|
||||
img, err := daemon.GetImage(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
history := []*types.ImageHistory{}
|
||||
|
||||
layerCounter := 0
|
||||
rootFS := *img.RootFS
|
||||
rootFS.DiffIDs = nil
|
||||
|
||||
for _, h := range img.History {
|
||||
var layerSize int64
|
||||
|
||||
if !h.EmptyLayer {
|
||||
if len(img.RootFS.DiffIDs) <= layerCounter {
|
||||
return nil, errors.New("too many non-empty layers in History section")
|
||||
}
|
||||
|
||||
rootFS.Append(img.RootFS.DiffIDs[layerCounter])
|
||||
l, err := daemon.layerStore.Get(rootFS.ChainID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
layerSize, err = l.DiffSize()
|
||||
layer.ReleaseAndLog(daemon.layerStore, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
layerCounter++
|
||||
}
|
||||
|
||||
history = append([]*types.ImageHistory{{
|
||||
ID: "<missing>",
|
||||
Created: h.Created.Unix(),
|
||||
CreatedBy: h.CreatedBy,
|
||||
Comment: h.Comment,
|
||||
Size: layerSize,
|
||||
}}, history...)
|
||||
}
|
||||
|
||||
// Fill in image IDs and tags
|
||||
histImg := img
|
||||
id := img.ID()
|
||||
for _, h := range history {
|
||||
h.ID = id.String()
|
||||
|
||||
var tags []string
|
||||
for _, r := range daemon.tagStore.References(id) {
|
||||
if _, ok := r.(reference.NamedTagged); ok {
|
||||
tags = append(tags, r.String())
|
||||
}
|
||||
}
|
||||
|
||||
h.Tags = tags
|
||||
|
||||
id = histImg.Parent
|
||||
if id == "" {
|
||||
break
|
||||
}
|
||||
histImg, err = daemon.GetImage(id.String())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return history, nil
|
||||
}
|
||||
|
||||
// GetImage returns pointer to an Image struct corresponding to the given
|
||||
// name. The name can include an optional tag; otherwise the default tag will
|
||||
// be used.
|
||||
func (daemon *Daemon) GetImage(name string) (*image.Image, error) {
|
||||
return daemon.repositories.LookupImage(name)
|
||||
// GetImageID returns an image ID corresponding to the image referred to by
|
||||
// refOrID.
|
||||
func (daemon *Daemon) GetImageID(refOrID string) (image.ID, error) {
|
||||
// Treat as an ID
|
||||
if id, err := digest.ParseDigest(refOrID); err == nil {
|
||||
return image.ID(id), nil
|
||||
}
|
||||
|
||||
// Treat it as a possible tag or digest reference
|
||||
if ref, err := reference.ParseNamed(refOrID); err == nil {
|
||||
ref = registry.NormalizeLocalReference(ref)
|
||||
if id, err := daemon.tagStore.Get(ref); err == nil {
|
||||
return id, nil
|
||||
}
|
||||
if tagged, ok := ref.(reference.Tagged); ok {
|
||||
if id, err := daemon.imageStore.Search(tagged.Tag()); err == nil {
|
||||
for _, namedRef := range daemon.tagStore.References(id) {
|
||||
if namedRef.Name() == ref.Name() {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search based on ID
|
||||
if id, err := daemon.imageStore.Search(refOrID); err == nil {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
return "", ErrImageDoesNotExist{refOrID}
|
||||
}
|
||||
|
||||
// GetImage returns an image corresponding to the image referred to by refOrID.
|
||||
func (daemon *Daemon) GetImage(refOrID string) (*image.Image, error) {
|
||||
imgID, err := daemon.GetImageID(refOrID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return daemon.imageStore.Get(imgID)
|
||||
}
|
||||
|
||||
func (daemon *Daemon) config() *Config {
|
||||
|
@ -1132,33 +1318,23 @@ func (daemon *Daemon) GetRemappedUIDGID() (int, int) {
|
|||
// of the image with imgID, that had the same config when it was
|
||||
// created. nil is returned if a child cannot be found. An error is
|
||||
// returned if the parent image cannot be found.
|
||||
func (daemon *Daemon) ImageGetCached(imgID string, config *runconfig.Config) (*image.Image, error) {
|
||||
// for now just exit if imgID has no children.
|
||||
// maybe parentRefs in graph could be used to store
|
||||
// the Image obj children for faster lookup below but this can
|
||||
// be quite memory hungry.
|
||||
if !daemon.Graph().HasChildren(imgID) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (daemon *Daemon) ImageGetCached(imgID image.ID, config *runconfig.Config) (*image.Image, error) {
|
||||
// Retrieve all images
|
||||
images := daemon.Graph().Map()
|
||||
imgs := daemon.Map()
|
||||
|
||||
// Store the tree in a map of map (map[parentId][childId])
|
||||
imageMap := make(map[string]map[string]struct{})
|
||||
for _, img := range images {
|
||||
if _, exists := imageMap[img.Parent]; !exists {
|
||||
imageMap[img.Parent] = make(map[string]struct{})
|
||||
var siblings []image.ID
|
||||
for id, img := range imgs {
|
||||
if img.Parent == imgID {
|
||||
siblings = append(siblings, id)
|
||||
}
|
||||
imageMap[img.Parent][img.ID] = struct{}{}
|
||||
}
|
||||
|
||||
// Loop on the children of the given image and check the config
|
||||
var match *image.Image
|
||||
for elem := range imageMap[imgID] {
|
||||
img, ok := images[elem]
|
||||
for _, id := range siblings {
|
||||
img, ok := imgs[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to find image %q", elem)
|
||||
return nil, fmt.Errorf("unable to find image %q", id)
|
||||
}
|
||||
if runconfig.Compare(&img.ContainerConfig, config) {
|
||||
if match == nil || match.Created.Before(img.Created) {
|
||||
|
@ -1179,6 +1355,12 @@ func tempDir(rootDir string, rootUID, rootGID int) (string, error) {
|
|||
}
|
||||
|
||||
func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig.HostConfig) error {
|
||||
container.Lock()
|
||||
if err := parseSecurityOpt(container, hostConfig); err != nil {
|
||||
container.Unlock()
|
||||
return err
|
||||
}
|
||||
container.Unlock()
|
||||
|
||||
// Do not lock while creating volumes since this could be calling out to external plugins
|
||||
// Don't want to block other actions, like `docker ps` because we're waiting on an external plugin
|
||||
|
@ -1199,6 +1381,11 @@ func (daemon *Daemon) setHostConfig(container *Container, hostConfig *runconfig.
|
|||
return nil
|
||||
}
|
||||
|
||||
func (daemon *Daemon) setupInitLayer(initPath string) error {
|
||||
rootUID, rootGID := daemon.GetRemappedUIDGID()
|
||||
return setupInitLayer(initPath, rootUID, rootGID)
|
||||
}
|
||||
|
||||
func setDefaultMtu(config *Config) {
|
||||
// do nothing if the config does not have the default 0 value.
|
||||
if config.Mtu != 0 {
|
||||
|
|
|
@ -14,12 +14,15 @@ import (
|
|||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/daemon/graphdriver"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
pblkiodev "github.com/docker/docker/pkg/blkiodev"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/parsers/kernel"
|
||||
"github.com/docker/docker/pkg/sysinfo"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/tag"
|
||||
"github.com/docker/libnetwork"
|
||||
nwconfig "github.com/docker/libnetwork/config"
|
||||
"github.com/docker/libnetwork/drivers/bridge"
|
||||
|
@ -601,9 +604,7 @@ func (daemon *Daemon) conditionalMountOnStart(container *Container) error {
|
|||
// conditionalUnmountOnCleanup is a platform specific helper function called
|
||||
// during the cleanup of a container to unmount.
|
||||
func (daemon *Daemon) conditionalUnmountOnCleanup(container *Container) {
|
||||
if err := daemon.Unmount(container); err != nil {
|
||||
logrus.Errorf("%v: Failed to umount filesystem: %v", container.ID, err)
|
||||
}
|
||||
daemon.Unmount(container)
|
||||
}
|
||||
|
||||
// getDefaultRouteMtu returns the MTU for the default route's interface.
|
||||
|
@ -624,3 +625,8 @@ func getDefaultRouteMtu() (int, error) {
|
|||
}
|
||||
return 0, errNoDefaultRoute
|
||||
}
|
||||
|
||||
func restoreCustomImage(driver graphdriver.Driver, is image.Store, ls layer.Store, ts tag.Store) error {
|
||||
// Unix has no custom images to register
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
package daemon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/daemon/graphdriver"
|
||||
"github.com/docker/docker/dockerversion"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/tag"
|
||||
// register the windows graph driver
|
||||
_ "github.com/docker/docker/daemon/graphdriver/windows"
|
||||
"github.com/docker/docker/daemon/graphdriver/windows"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/libnetwork"
|
||||
|
@ -128,8 +138,71 @@ func (daemon *Daemon) conditionalMountOnStart(container *Container) error {
|
|||
func (daemon *Daemon) conditionalUnmountOnCleanup(container *Container) {
|
||||
// We do not unmount if a Hyper-V container
|
||||
if !container.hostConfig.Isolation.IsHyperV() {
|
||||
if err := daemon.Unmount(container); err != nil {
|
||||
logrus.Errorf("%v: Failed to umount filesystem: %v", container.ID, err)
|
||||
}
|
||||
daemon.Unmount(container)
|
||||
}
|
||||
}
|
||||
|
||||
func restoreCustomImage(driver graphdriver.Driver, is image.Store, ls layer.Store, ts tag.Store) error {
|
||||
if wd, ok := driver.(*windows.Driver); ok {
|
||||
imageInfos, err := wd.GetCustomImageInfos()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert imageData to valid image configuration
|
||||
for i := range imageInfos {
|
||||
name := strings.ToLower(imageInfos[i].Name)
|
||||
|
||||
type registrar interface {
|
||||
RegisterDiffID(graphID string, size int64) (layer.Layer, error)
|
||||
}
|
||||
r, ok := ls.(registrar)
|
||||
if !ok {
|
||||
return errors.New("Layerstore doesn't support RegisterDiffID")
|
||||
}
|
||||
if _, err := r.RegisterDiffID(imageInfos[i].ID, imageInfos[i].Size); err != nil {
|
||||
return err
|
||||
}
|
||||
// layer is intentionally not released
|
||||
|
||||
rootFS := image.NewRootFS()
|
||||
rootFS.BaseLayer = filepath.Base(imageInfos[i].Path)
|
||||
|
||||
// Create history for base layer
|
||||
config, err := json.Marshal(&image.Image{
|
||||
V1Image: image.V1Image{
|
||||
DockerVersion: dockerversion.Version,
|
||||
Architecture: runtime.GOARCH,
|
||||
OS: runtime.GOOS,
|
||||
Created: imageInfos[i].CreatedTime,
|
||||
},
|
||||
RootFS: rootFS,
|
||||
History: []image.History{},
|
||||
})
|
||||
|
||||
named, err := reference.ParseNamed(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref, err := reference.WithTag(named, imageInfos[i].Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := is.Create(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ts.Add(ref, id, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("Registered base layer %s as %s", ref, id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -9,17 +9,16 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/builder"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/daemon"
|
||||
"github.com/docker/docker/graph"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/httputils"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/urlutil"
|
||||
"github.com/docker/docker/registry"
|
||||
|
@ -44,15 +43,24 @@ func (d Docker) LookupImage(name string) (*image.Image, error) {
|
|||
|
||||
// Pull tells Docker to pull image referenced by `name`.
|
||||
func (d Docker) Pull(name string) (*image.Image, error) {
|
||||
remote, tag := parsers.ParseRepositoryTag(name)
|
||||
if tag == "" {
|
||||
tag = "latest"
|
||||
ref, err := reference.ParseNamed(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch ref.(type) {
|
||||
case reference.Tagged:
|
||||
case reference.Digested:
|
||||
default:
|
||||
ref, err = reference.WithTag(ref, "latest")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pullRegistryAuth := &cliconfig.AuthConfig{}
|
||||
if len(d.AuthConfigs) > 0 {
|
||||
// The request came with a full auth config file, we prefer to use that
|
||||
repoInfo, err := d.Daemon.RegistryService.ResolveRepository(remote)
|
||||
repoInfo, err := d.Daemon.RegistryService.ResolveRepository(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -64,12 +72,7 @@ func (d Docker) Pull(name string) (*image.Image, error) {
|
|||
pullRegistryAuth = &resolvedConfig
|
||||
}
|
||||
|
||||
imagePullConfig := &graph.ImagePullConfig{
|
||||
AuthConfig: pullRegistryAuth,
|
||||
OutStream: ioutils.NopWriteCloser(d.OutOld),
|
||||
}
|
||||
|
||||
if err := d.Daemon.PullImage(remote, tag, imagePullConfig); err != nil {
|
||||
if err := d.Daemon.PullImage(ref, nil, pullRegistryAuth, ioutils.NopWriteCloser(d.OutOld)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -106,18 +109,20 @@ func (d Docker) Remove(id string, cfg *daemon.ContainerRmConfig) error {
|
|||
}
|
||||
|
||||
// Commit creates a new Docker image from an existing Docker container.
|
||||
func (d Docker) Commit(name string, cfg *daemon.ContainerCommitConfig) (*image.Image, error) {
|
||||
func (d Docker) Commit(name string, cfg *daemon.ContainerCommitConfig) (string, error) {
|
||||
return d.Daemon.Commit(name, cfg)
|
||||
}
|
||||
|
||||
// Retain retains an image avoiding it to be removed or overwritten until a corresponding Release() call.
|
||||
func (d Docker) Retain(sessionID, imgID string) {
|
||||
d.Daemon.Graph().Retain(sessionID, imgID)
|
||||
// FIXME: This will be solved with tags in client-side builder
|
||||
//d.Daemon.Graph().Retain(sessionID, imgID)
|
||||
}
|
||||
|
||||
// Release releases a list of images that were retained for the time of a build.
|
||||
func (d Docker) Release(sessionID string, activeImages []string) {
|
||||
d.Daemon.Graph().Release(sessionID, activeImages...)
|
||||
// FIXME: This will be solved with tags in client-side builder
|
||||
//d.Daemon.Graph().Release(sessionID, activeImages...)
|
||||
}
|
||||
|
||||
// Copy copies/extracts a source FileInfo to a destination path inside a container
|
||||
|
@ -199,11 +204,11 @@ func (d Docker) Copy(c *daemon.Container, destPath string, src builder.FileInfo,
|
|||
// GetCachedImage returns a reference to a cached image whose parent equals `parent`
|
||||
// and runconfig equals `cfg`. A cache miss is expected to return an empty ID and a nil error.
|
||||
func (d Docker) GetCachedImage(imgID string, cfg *runconfig.Config) (string, error) {
|
||||
cache, err := d.Daemon.ImageGetCached(imgID, cfg)
|
||||
cache, err := d.Daemon.ImageGetCached(image.ID(imgID), cfg)
|
||||
if cache == nil || err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cache.ID, nil
|
||||
return cache.ID().String(), nil
|
||||
}
|
||||
|
||||
// Kill stops the container execution abruptly.
|
||||
|
@ -218,7 +223,8 @@ func (d Docker) Mount(c *daemon.Container) error {
|
|||
|
||||
// Unmount unmounts the root filesystem for the container.
|
||||
func (d Docker) Unmount(c *daemon.Container) error {
|
||||
return d.Daemon.Unmount(c)
|
||||
d.Daemon.Unmount(c)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts a container
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/layer"
|
||||
volumestore "github.com/docker/docker/volume/store"
|
||||
)
|
||||
|
||||
|
@ -119,15 +119,12 @@ func (daemon *Daemon) rm(container *Container, forceRemove bool) (err error) {
|
|||
logrus.Debugf("Unable to remove container from link graph: %s", err)
|
||||
}
|
||||
|
||||
if err = daemon.driver.Remove(container.ID); err != nil {
|
||||
metadata, err := daemon.layerStore.DeleteMount(container.ID)
|
||||
layer.LogReleaseMetadata(metadata)
|
||||
if err != nil {
|
||||
return derr.ErrorCodeRmDriverFS.WithArgs(daemon.driver, container.ID, err)
|
||||
}
|
||||
|
||||
initID := fmt.Sprintf("%s-init", container.ID)
|
||||
if err := daemon.driver.Remove(initID); err != nil {
|
||||
return derr.ErrorCodeRmInit.WithArgs(daemon.driver, initID, err)
|
||||
}
|
||||
|
||||
if err = os.RemoveAll(container.root); err != nil {
|
||||
return derr.ErrorCodeRmFS.WithArgs(container.ID, err)
|
||||
}
|
||||
|
|
|
@ -3,21 +3,25 @@ package daemon
|
|||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/graph/tags"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
tagpkg "github.com/docker/docker/tag"
|
||||
)
|
||||
|
||||
func (d *Daemon) graphNotExistToErrcode(imageName string, err error) error {
|
||||
if d.Graph().IsNotExist(err, imageName) {
|
||||
if strings.Contains(imageName, "@") {
|
||||
return derr.ErrorCodeNoSuchImageHash.WithArgs(imageName)
|
||||
func (d *Daemon) imageNotExistToErrcode(err error) error {
|
||||
if dne, isDNE := err.(ErrImageDoesNotExist); isDNE {
|
||||
if strings.Contains(dne.RefOrID, "@") {
|
||||
return derr.ErrorCodeNoSuchImageHash.WithArgs(dne.RefOrID)
|
||||
}
|
||||
img, tag := parsers.ParseRepositoryTag(imageName)
|
||||
if tag == "" {
|
||||
tag = tags.DefaultTag
|
||||
tag := tagpkg.DefaultTag
|
||||
ref, err := reference.ParseNamed(dne.RefOrID)
|
||||
if err != nil {
|
||||
return derr.ErrorCodeNoSuchImageTag.WithArgs(dne.RefOrID, tag)
|
||||
}
|
||||
return derr.ErrorCodeNoSuchImageTag.WithArgs(img, tag)
|
||||
if tagged, isTagged := ref.(reference.Tagged); isTagged {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
return derr.ErrorCodeNoSuchImageTag.WithArgs(ref.Name(), tag)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/parsers/filters"
|
||||
)
|
||||
|
||||
|
@ -38,8 +38,11 @@ func (ef *Filter) isLabelFieldIncluded(id string) bool {
|
|||
// against the stripped repo name without any tags.
|
||||
func (ef *Filter) isImageIncluded(eventID string, eventFrom string) bool {
|
||||
stripTag := func(image string) string {
|
||||
repo, _ := parsers.ParseRepositoryTag(image)
|
||||
return repo
|
||||
ref, err := reference.ParseNamed(image)
|
||||
if err != nil {
|
||||
return image
|
||||
}
|
||||
return ref.Name()
|
||||
}
|
||||
|
||||
return isFieldIncluded(eventID, ef.filter["image"]) ||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// +build daemon
|
||||
|
||||
package graphdriver
|
||||
|
||||
import (
|
||||
|
@ -13,6 +11,12 @@ import (
|
|||
"github.com/docker/docker/pkg/ioutils"
|
||||
)
|
||||
|
||||
var (
|
||||
// ApplyUncompressedLayer defines the unpack method used by the graph
|
||||
// driver.
|
||||
ApplyUncompressedLayer = chrootarchive.ApplyUncompressedLayer
|
||||
)
|
||||
|
||||
// NaiveDiffDriver takes a ProtoDriver and adds the
|
||||
// capability of the Diffing methods which it may or may not
|
||||
// support on its own. See the comment on the exported
|
||||
|
@ -129,7 +133,7 @@ func (gdw *NaiveDiffDriver) ApplyDiff(id, parent string, diff archive.Reader) (s
|
|||
GIDMaps: gdw.gidMaps}
|
||||
start := time.Now().UTC()
|
||||
logrus.Debugf("Start untar layer")
|
||||
if size, err = chrootarchive.ApplyUncompressedLayer(layerFs, diff, options); err != nil {
|
||||
if size, err = ApplyUncompressedLayer(layerFs, diff, options); err != nil {
|
||||
return
|
||||
}
|
||||
logrus.Debugf("Untar time: %vs", time.Now().UTC().Sub(start).Seconds())
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
package graphdriver
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/image"
|
||||
)
|
||||
|
||||
// NOTE: These interfaces are used for implementing specific features of the Windows
|
||||
// graphdriver implementation. The current versions are a short-term solution and
|
||||
// likely to change or possibly be eliminated, so avoid using them outside of the Windows
|
||||
// graphdriver code.
|
||||
|
||||
// ImageRestorer interface allows the implementer to add a custom image to
|
||||
// the graph and tagstore.
|
||||
type ImageRestorer interface {
|
||||
RestoreCustomImages(tagger Tagger, recorder Recorder) ([]string, error)
|
||||
}
|
||||
|
||||
// Tagger is an interface that exposes the TagStore.Tag function without needing
|
||||
// to import graph.
|
||||
type Tagger interface {
|
||||
Tag(repoName, tag, imageName string, force bool) error
|
||||
}
|
||||
|
||||
// Recorder is an interface that exposes the Graph.Register and Graph.Exists
|
||||
// functions without needing to import graph.
|
||||
type Recorder interface {
|
||||
Exists(id string) bool
|
||||
Register(img image.Descriptor, layerData io.Reader) error
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
// +build experimental
|
||||
// +build daemon
|
||||
|
||||
package graphdriver
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// +build experimental
|
||||
// +build daemon
|
||||
|
||||
package graphdriver
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// +build daemon
|
||||
|
||||
package vfs
|
||||
|
||||
import (
|
||||
|
@ -14,6 +12,11 @@ import (
|
|||
"github.com/opencontainers/runc/libcontainer/label"
|
||||
)
|
||||
|
||||
var (
|
||||
// CopyWithTar defines the copy method to use.
|
||||
CopyWithTar = chrootarchive.CopyWithTar
|
||||
)
|
||||
|
||||
func init() {
|
||||
graphdriver.Register("vfs", Init)
|
||||
}
|
||||
|
@ -89,7 +92,7 @@ func (d *Driver) Create(id, parent, mountLabel string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("%s: %s", parent, err)
|
||||
}
|
||||
if err := chrootarchive.CopyWithTar(parentDir, dir); err != nil {
|
||||
if err := CopyWithTar(parentDir, dir); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
// +build !daemon
|
||||
|
||||
package vfs
|
|
@ -6,10 +6,10 @@ import (
|
|||
"crypto/sha512"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -17,8 +17,6 @@ import (
|
|||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/daemon/graphdriver"
|
||||
"github.com/docker/docker/dockerversion"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/chrootarchive"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
|
@ -40,26 +38,6 @@ const (
|
|||
filterDriver
|
||||
)
|
||||
|
||||
// CustomImageDescriptor is an image descriptor for use by RestoreCustomImages
|
||||
type customImageDescriptor struct {
|
||||
img *image.Image
|
||||
}
|
||||
|
||||
// ID returns the image ID specified in the image structure.
|
||||
func (img customImageDescriptor) ID() string {
|
||||
return img.img.ID
|
||||
}
|
||||
|
||||
// Parent returns the parent ID - in this case, none
|
||||
func (img customImageDescriptor) Parent() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// MarshalConfig renders the image structure into JSON.
|
||||
func (img customImageDescriptor) MarshalConfig() ([]byte, error) {
|
||||
return json.Marshal(img.img)
|
||||
}
|
||||
|
||||
// Driver represents a windows graph driver.
|
||||
type Driver struct {
|
||||
// info stores the shim driver information
|
||||
|
@ -195,7 +173,7 @@ func (d *Driver) Remove(id string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.RemoveAll(filepath.Join(d.info.HomeDir, "sysfile-backups", rID)) // ok to fail
|
||||
return hcsshim.DestroyLayer(d.info, rID)
|
||||
}
|
||||
|
||||
|
@ -402,22 +380,27 @@ func (d *Driver) DiffSize(id, parent string) (size int64, err error) {
|
|||
return archive.ChangesSize(layerFs, changes), nil
|
||||
}
|
||||
|
||||
// RestoreCustomImages adds any auto-detected OS specific images to the tag and graph store.
|
||||
func (d *Driver) RestoreCustomImages(tagger graphdriver.Tagger, recorder graphdriver.Recorder) (imageIDs []string, err error) {
|
||||
// CustomImageInfo is the object returned by the driver describing the base
|
||||
// image.
|
||||
type CustomImageInfo struct {
|
||||
ID string
|
||||
Name string
|
||||
Version string
|
||||
Path string
|
||||
Size int64
|
||||
CreatedTime time.Time
|
||||
}
|
||||
|
||||
// GetCustomImageInfos returns the image infos for window specific
|
||||
// base images which should always be present.
|
||||
func (d *Driver) GetCustomImageInfos() ([]CustomImageInfo, error) {
|
||||
strData, err := hcsshim.GetSharedBaseImages()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to restore base images: %s", err)
|
||||
}
|
||||
|
||||
type customImageInfo struct {
|
||||
Name string
|
||||
Version string
|
||||
Path string
|
||||
Size int64
|
||||
CreatedTime time.Time
|
||||
}
|
||||
type customImageInfoList struct {
|
||||
Images []customImageInfo
|
||||
Images []CustomImageInfo
|
||||
}
|
||||
|
||||
var infoData customImageInfoList
|
||||
|
@ -428,43 +411,28 @@ func (d *Driver) RestoreCustomImages(tagger graphdriver.Tagger, recorder graphdr
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var images []CustomImageInfo
|
||||
|
||||
for _, imageData := range infoData.Images {
|
||||
_, folderName := filepath.Split(imageData.Path)
|
||||
folderName := filepath.Base(imageData.Path)
|
||||
|
||||
// Use crypto hash of the foldername to generate a docker style id.
|
||||
h := sha512.Sum384([]byte(folderName))
|
||||
id := fmt.Sprintf("%x", h[:32])
|
||||
|
||||
if !recorder.Exists(id) {
|
||||
// Register the image.
|
||||
img := &image.Image{
|
||||
ID: id,
|
||||
Created: imageData.CreatedTime,
|
||||
DockerVersion: dockerversion.Version,
|
||||
Architecture: runtime.GOARCH,
|
||||
OS: runtime.GOOS,
|
||||
Size: imageData.Size,
|
||||
}
|
||||
|
||||
if err := recorder.Register(customImageDescriptor{img}, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create tags for the new image.
|
||||
if err := tagger.Tag(strings.ToLower(imageData.Name), imageData.Version, img.ID, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the alternate ID file.
|
||||
if err := d.setID(img.ID, folderName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageIDs = append(imageIDs, img.ID)
|
||||
if err := d.Create(id, "", ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create the alternate ID file.
|
||||
if err := d.setID(id, folderName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageData.ID = id
|
||||
images = append(images, imageData)
|
||||
}
|
||||
|
||||
return imageIDs, nil
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// GetMetadata returns custom driver information.
|
||||
|
@ -533,6 +501,10 @@ func (d *Driver) importLayer(id string, layerData archive.Reader, parentLayerPat
|
|||
if size, err = chrootarchive.ApplyLayer(tempFolder, layerData); err != nil {
|
||||
return
|
||||
}
|
||||
err = copySysFiles(tempFolder, filepath.Join(d.info.HomeDir, "sysfile-backups", id))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
logrus.Debugf("Untar time: %vs", time.Now().UTC().Sub(start).Seconds())
|
||||
|
||||
if err = hcsshim.ImportLayer(d.info, id, tempFolder, parentLayerPaths); err != nil {
|
||||
|
@ -596,3 +568,103 @@ func (d *Driver) setLayerChain(id string, chain []string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DiffPath returns a directory that contains files needed to construct layer diff.
|
||||
func (d *Driver) DiffPath(id string) (path string, release func() error, err error) {
|
||||
id, err = d.resolveID(id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Getting the layer paths must be done outside of the lock.
|
||||
layerChain, err := d.getLayerChain(id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
layerFolder := d.dir(id)
|
||||
tempFolder := layerFolder + "-" + strconv.FormatUint(uint64(random.Rand.Uint32()), 10)
|
||||
if err = os.MkdirAll(tempFolder, 0755); err != nil {
|
||||
logrus.Errorf("Could not create %s %s", tempFolder, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_, folderName := filepath.Split(tempFolder)
|
||||
if err2 := hcsshim.DestroyLayer(d.info, folderName); err2 != nil {
|
||||
logrus.Warnf("Couldn't clean-up tempFolder: %s %s", tempFolder, err2)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = hcsshim.ExportLayer(d.info, id, tempFolder, layerChain); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = copySysFiles(filepath.Join(d.info.HomeDir, "sysfile-backups", id), tempFolder)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return tempFolder, func() error {
|
||||
// TODO: activate layers and release here?
|
||||
_, folderName := filepath.Split(tempFolder)
|
||||
return hcsshim.DestroyLayer(d.info, folderName)
|
||||
}, nil
|
||||
}
|
||||
|
||||
var sysFileWhiteList = []string{
|
||||
"Hives\\*",
|
||||
"Files\\BOOTNXT",
|
||||
"tombstones.txt",
|
||||
}
|
||||
|
||||
// note this only handles files
|
||||
func copySysFiles(src string, dest string) error {
|
||||
if err := os.MkdirAll(dest, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||||
rel, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, sysfile := range sysFileWhiteList {
|
||||
if matches, err := filepath.Match(sysfile, rel); err != nil || !matches {
|
||||
continue
|
||||
}
|
||||
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !fi.Mode().IsRegular() {
|
||||
continue
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(dest, rel)
|
||||
if err = os.MkdirAll(filepath.Dir(targetPath), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
in, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
in.Close()
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(out, in)
|
||||
in.Close()
|
||||
out.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,13 +4,12 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/graph/tags"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/utils"
|
||||
tagpkg "github.com/docker/docker/tag"
|
||||
)
|
||||
|
||||
// ImageDelete deletes the image referenced by the given imageRef from this
|
||||
|
@ -53,39 +52,46 @@ import (
|
|||
func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.ImageDelete, error) {
|
||||
records := []types.ImageDelete{}
|
||||
|
||||
img, err := daemon.repositories.LookupImage(imageRef)
|
||||
imgID, err := daemon.GetImageID(imageRef)
|
||||
if err != nil {
|
||||
return nil, daemon.graphNotExistToErrcode(imageRef, err)
|
||||
return nil, daemon.imageNotExistToErrcode(err)
|
||||
}
|
||||
|
||||
repoRefs := daemon.tagStore.References(imgID)
|
||||
|
||||
var removedRepositoryRef bool
|
||||
if !isImageIDPrefix(img.ID, imageRef) {
|
||||
if !isImageIDPrefix(imgID.String(), imageRef) {
|
||||
// A repository reference was given and should be removed
|
||||
// first. We can only remove this reference if either force is
|
||||
// true, there are multiple repository references to this
|
||||
// image, or there are no containers using the given reference.
|
||||
if !(force || daemon.imageHasMultipleRepositoryReferences(img.ID)) {
|
||||
if container := daemon.getContainerUsingImage(img.ID); container != nil {
|
||||
if !(force || len(repoRefs) > 1) {
|
||||
if container := daemon.getContainerUsingImage(imgID); container != nil {
|
||||
// If we removed the repository reference then
|
||||
// this image would remain "dangling" and since
|
||||
// we really want to avoid that the client must
|
||||
// explicitly force its removal.
|
||||
return nil, derr.ErrorCodeImgDelUsed.WithArgs(imageRef, stringid.TruncateID(container.ID), stringid.TruncateID(img.ID))
|
||||
return nil, derr.ErrorCodeImgDelUsed.WithArgs(imageRef, stringid.TruncateID(container.ID), stringid.TruncateID(imgID.String()))
|
||||
}
|
||||
}
|
||||
|
||||
parsedRef, err := daemon.removeImageRef(imageRef)
|
||||
parsedRef, err := reference.ParseNamed(imageRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
untaggedRecord := types.ImageDelete{Untagged: parsedRef}
|
||||
parsedRef, err = daemon.removeImageRef(parsedRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
daemon.EventsService.Log("untag", img.ID, "")
|
||||
untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()}
|
||||
|
||||
daemon.EventsService.Log("untag", imgID.String(), "")
|
||||
records = append(records, untaggedRecord)
|
||||
|
||||
// If has remaining references then untag finishes the remove
|
||||
if daemon.repositories.HasReferences(img) {
|
||||
if len(repoRefs) > 1 {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
|
@ -95,38 +101,39 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I
|
|||
// repository reference to the image then we will want to
|
||||
// remove that reference.
|
||||
// FIXME: Is this the behavior we want?
|
||||
repoRefs := daemon.repositories.ByID()[img.ID]
|
||||
if len(repoRefs) == 1 {
|
||||
parsedRef, err := daemon.removeImageRef(repoRefs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
untaggedRecord := types.ImageDelete{Untagged: parsedRef}
|
||||
untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()}
|
||||
|
||||
daemon.EventsService.Log("untag", img.ID, "")
|
||||
daemon.EventsService.Log("untag", imgID.String(), "")
|
||||
records = append(records, untaggedRecord)
|
||||
}
|
||||
}
|
||||
|
||||
return records, daemon.imageDeleteHelper(img, &records, force, prune, removedRepositoryRef)
|
||||
return records, daemon.imageDeleteHelper(imgID, &records, force, prune, removedRepositoryRef)
|
||||
}
|
||||
|
||||
// isImageIDPrefix returns whether the given possiblePrefix is a prefix of the
|
||||
// given imageID.
|
||||
func isImageIDPrefix(imageID, possiblePrefix string) bool {
|
||||
return strings.HasPrefix(imageID, possiblePrefix)
|
||||
}
|
||||
if strings.HasPrefix(imageID, possiblePrefix) {
|
||||
return true
|
||||
}
|
||||
|
||||
// imageHasMultipleRepositoryReferences returns whether there are multiple
|
||||
// repository references to the given imageID.
|
||||
func (daemon *Daemon) imageHasMultipleRepositoryReferences(imageID string) bool {
|
||||
return len(daemon.repositories.ByID()[imageID]) > 1
|
||||
if i := strings.IndexRune(imageID, ':'); i >= 0 {
|
||||
return strings.HasPrefix(imageID[i+1:], possiblePrefix)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getContainerUsingImage returns a container that was created using the given
|
||||
// imageID. Returns nil if there is no such container.
|
||||
func (daemon *Daemon) getContainerUsingImage(imageID string) *Container {
|
||||
func (daemon *Daemon) getContainerUsingImage(imageID image.ID) *Container {
|
||||
for _, container := range daemon.List() {
|
||||
if container.ImageID == imageID {
|
||||
return container
|
||||
|
@ -141,18 +148,24 @@ func (daemon *Daemon) getContainerUsingImage(imageID string) *Container {
|
|||
// repositoryRef must not be an image ID but a repository name followed by an
|
||||
// optional tag or digest reference. If tag or digest is omitted, the default
|
||||
// tag is used. Returns the resolved image reference and an error.
|
||||
func (daemon *Daemon) removeImageRef(repositoryRef string) (string, error) {
|
||||
repository, ref := parsers.ParseRepositoryTag(repositoryRef)
|
||||
if ref == "" {
|
||||
ref = tags.DefaultTag
|
||||
func (daemon *Daemon) removeImageRef(ref reference.Named) (reference.Named, error) {
|
||||
switch ref.(type) {
|
||||
case reference.Tagged:
|
||||
case reference.Digested:
|
||||
default:
|
||||
var err error
|
||||
ref, err = reference.WithTag(ref, tagpkg.DefaultTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore the boolean value returned, as far as we're concerned, this
|
||||
// is an idempotent operation and it's okay if the reference didn't
|
||||
// exist in the first place.
|
||||
_, err := daemon.repositories.Delete(repository, ref)
|
||||
_, err := daemon.tagStore.Delete(ref)
|
||||
|
||||
return utils.ImageReference(repository, ref), err
|
||||
return ref, err
|
||||
}
|
||||
|
||||
// removeAllReferencesToImageID attempts to remove every reference to the given
|
||||
|
@ -160,8 +173,8 @@ func (daemon *Daemon) removeImageRef(repositoryRef string) (string, error) {
|
|||
// on the first encountered error. Removed references are logged to this
|
||||
// daemon's event service. An "Untagged" types.ImageDelete is added to the
|
||||
// given list of records.
|
||||
func (daemon *Daemon) removeAllReferencesToImageID(imgID string, records *[]types.ImageDelete) error {
|
||||
imageRefs := daemon.repositories.ByID()[imgID]
|
||||
func (daemon *Daemon) removeAllReferencesToImageID(imgID image.ID, records *[]types.ImageDelete) error {
|
||||
imageRefs := daemon.tagStore.References(imgID)
|
||||
|
||||
for _, imageRef := range imageRefs {
|
||||
parsedRef, err := daemon.removeImageRef(imageRef)
|
||||
|
@ -169,9 +182,9 @@ func (daemon *Daemon) removeAllReferencesToImageID(imgID string, records *[]type
|
|||
return err
|
||||
}
|
||||
|
||||
untaggedRecord := types.ImageDelete{Untagged: parsedRef}
|
||||
untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()}
|
||||
|
||||
daemon.EventsService.Log("untag", imgID, "")
|
||||
daemon.EventsService.Log("untag", imgID.String(), "")
|
||||
*records = append(*records, untaggedRecord)
|
||||
}
|
||||
|
||||
|
@ -182,7 +195,7 @@ func (daemon *Daemon) removeAllReferencesToImageID(imgID string, records *[]type
|
|||
// Implements the error interface.
|
||||
type imageDeleteConflict struct {
|
||||
hard bool
|
||||
imgID string
|
||||
imgID image.ID
|
||||
message string
|
||||
}
|
||||
|
||||
|
@ -194,7 +207,7 @@ func (idc *imageDeleteConflict) Error() string {
|
|||
forceMsg = "must be forced"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("conflict: unable to delete %s (%s) - %s", stringid.TruncateID(idc.imgID), forceMsg, idc.message)
|
||||
return fmt.Sprintf("conflict: unable to delete %s (%s) - %s", stringid.TruncateID(idc.imgID.String()), forceMsg, idc.message)
|
||||
}
|
||||
|
||||
// imageDeleteHelper attempts to delete the given image from this daemon. If
|
||||
|
@ -208,11 +221,11 @@ func (idc *imageDeleteConflict) Error() string {
|
|||
// conflict is encountered, it will be returned immediately without deleting
|
||||
// the image. If quiet is true, any encountered conflicts will be ignored and
|
||||
// the function will return nil immediately without deleting the image.
|
||||
func (daemon *Daemon) imageDeleteHelper(img *image.Image, records *[]types.ImageDelete, force, prune, quiet bool) error {
|
||||
func (daemon *Daemon) imageDeleteHelper(imgID image.ID, records *[]types.ImageDelete, force, prune, quiet bool) error {
|
||||
// First, determine if this image has any conflicts. Ignore soft conflicts
|
||||
// if force is true.
|
||||
if conflict := daemon.checkImageDeleteConflict(img, force); conflict != nil {
|
||||
if quiet && !daemon.imageIsDangling(img) {
|
||||
if conflict := daemon.checkImageDeleteConflict(imgID, force); conflict != nil {
|
||||
if quiet && !daemon.imageIsDangling(imgID) {
|
||||
// Ignore conflicts UNLESS the image is "dangling" in
|
||||
// which case we want the user to know.
|
||||
return nil
|
||||
|
@ -223,33 +236,38 @@ func (daemon *Daemon) imageDeleteHelper(img *image.Image, records *[]types.Image
|
|||
return conflict
|
||||
}
|
||||
|
||||
parent, err := daemon.imageStore.GetParent(imgID)
|
||||
if err != nil {
|
||||
// There may be no parent
|
||||
parent = ""
|
||||
}
|
||||
|
||||
// Delete all repository tag/digest references to this image.
|
||||
if err := daemon.removeAllReferencesToImageID(img.ID, records); err != nil {
|
||||
if err := daemon.removeAllReferencesToImageID(imgID, records); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := daemon.Graph().Delete(img.ID); err != nil {
|
||||
removedLayers, err := daemon.imageStore.Delete(imgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
daemon.EventsService.Log("delete", img.ID, "")
|
||||
*records = append(*records, types.ImageDelete{Deleted: img.ID})
|
||||
daemon.EventsService.Log("delete", imgID.String(), "")
|
||||
*records = append(*records, types.ImageDelete{Deleted: imgID.String()})
|
||||
for _, removedLayer := range removedLayers {
|
||||
*records = append(*records, types.ImageDelete{Deleted: removedLayer.ChainID.String()})
|
||||
}
|
||||
|
||||
if !prune || img.Parent == "" {
|
||||
if !prune || parent == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We need to prune the parent image. This means delete it if there are
|
||||
// no tags/digests referencing it and there are no containers using it (
|
||||
// either running or stopped).
|
||||
parentImg, err := daemon.Graph().Get(img.Parent)
|
||||
if err != nil {
|
||||
return derr.ErrorCodeImgNoParent.WithArgs(err)
|
||||
}
|
||||
|
||||
// Do not force prunings, but do so quietly (stopping on any encountered
|
||||
// conflicts).
|
||||
return daemon.imageDeleteHelper(parentImg, records, false, true, true)
|
||||
return daemon.imageDeleteHelper(parent, records, false, true, true)
|
||||
}
|
||||
|
||||
// checkImageDeleteConflict determines whether there are any conflicts
|
||||
|
@ -258,9 +276,9 @@ func (daemon *Daemon) imageDeleteHelper(img *image.Image, records *[]types.Image
|
|||
// using the image. A soft conflict is any tags/digest referencing the given
|
||||
// image or any stopped container using the image. If ignoreSoftConflicts is
|
||||
// true, this function will not check for soft conflict conditions.
|
||||
func (daemon *Daemon) checkImageDeleteConflict(img *image.Image, ignoreSoftConflicts bool) *imageDeleteConflict {
|
||||
func (daemon *Daemon) checkImageDeleteConflict(imgID image.ID, ignoreSoftConflicts bool) *imageDeleteConflict {
|
||||
// Check for hard conflicts first.
|
||||
if conflict := daemon.checkImageDeleteHardConflict(img); conflict != nil {
|
||||
if conflict := daemon.checkImageDeleteHardConflict(imgID); conflict != nil {
|
||||
return conflict
|
||||
}
|
||||
|
||||
|
@ -270,24 +288,15 @@ func (daemon *Daemon) checkImageDeleteConflict(img *image.Image, ignoreSoftConfl
|
|||
return nil
|
||||
}
|
||||
|
||||
return daemon.checkImageDeleteSoftConflict(img)
|
||||
return daemon.checkImageDeleteSoftConflict(imgID)
|
||||
}
|
||||
|
||||
func (daemon *Daemon) checkImageDeleteHardConflict(img *image.Image) *imageDeleteConflict {
|
||||
// Check if the image ID is being used by a pull or build.
|
||||
if daemon.Graph().IsHeld(img.ID) {
|
||||
return &imageDeleteConflict{
|
||||
hard: true,
|
||||
imgID: img.ID,
|
||||
message: "image is held by an ongoing pull or build",
|
||||
}
|
||||
}
|
||||
|
||||
func (daemon *Daemon) checkImageDeleteHardConflict(imgID image.ID) *imageDeleteConflict {
|
||||
// Check if the image has any descendent images.
|
||||
if daemon.Graph().HasChildren(img.ID) {
|
||||
if len(daemon.imageStore.Children(imgID)) > 0 {
|
||||
return &imageDeleteConflict{
|
||||
hard: true,
|
||||
imgID: img.ID,
|
||||
imgID: imgID,
|
||||
message: "image has dependent child images",
|
||||
}
|
||||
}
|
||||
|
@ -299,9 +308,9 @@ func (daemon *Daemon) checkImageDeleteHardConflict(img *image.Image) *imageDelet
|
|||
continue
|
||||
}
|
||||
|
||||
if container.ImageID == img.ID {
|
||||
if container.ImageID == imgID {
|
||||
return &imageDeleteConflict{
|
||||
imgID: img.ID,
|
||||
imgID: imgID,
|
||||
hard: true,
|
||||
message: fmt.Sprintf("image is being used by running container %s", stringid.TruncateID(container.ID)),
|
||||
}
|
||||
|
@ -311,11 +320,11 @@ func (daemon *Daemon) checkImageDeleteHardConflict(img *image.Image) *imageDelet
|
|||
return nil
|
||||
}
|
||||
|
||||
func (daemon *Daemon) checkImageDeleteSoftConflict(img *image.Image) *imageDeleteConflict {
|
||||
func (daemon *Daemon) checkImageDeleteSoftConflict(imgID image.ID) *imageDeleteConflict {
|
||||
// Check if any repository tags/digest reference this image.
|
||||
if daemon.repositories.HasReferences(img) {
|
||||
if len(daemon.tagStore.References(imgID)) > 0 {
|
||||
return &imageDeleteConflict{
|
||||
imgID: img.ID,
|
||||
imgID: imgID,
|
||||
message: "image is referenced in one or more repositories",
|
||||
}
|
||||
}
|
||||
|
@ -327,9 +336,9 @@ func (daemon *Daemon) checkImageDeleteSoftConflict(img *image.Image) *imageDelet
|
|||
continue
|
||||
}
|
||||
|
||||
if container.ImageID == img.ID {
|
||||
if container.ImageID == imgID {
|
||||
return &imageDeleteConflict{
|
||||
imgID: img.ID,
|
||||
imgID: imgID,
|
||||
message: fmt.Sprintf("image is being used by stopped container %s", stringid.TruncateID(container.ID)),
|
||||
}
|
||||
}
|
||||
|
@ -341,6 +350,6 @@ func (daemon *Daemon) checkImageDeleteSoftConflict(img *image.Image) *imageDelet
|
|||
// imageIsDangling returns whether the given image is "dangling" which means
|
||||
// that there are no repository references to the given image and it has no
|
||||
// child images.
|
||||
func (daemon *Daemon) imageIsDangling(img *image.Image) bool {
|
||||
return !(daemon.repositories.HasReferences(img) || daemon.Graph().HasChildren(img.ID))
|
||||
func (daemon *Daemon) imageIsDangling(imgID image.ID) bool {
|
||||
return !(len(daemon.tagStore.References(imgID)) > 0 || len(daemon.imageStore.Children(imgID)) > 0)
|
||||
}
|
||||
|
|
163
daemon/images.go
Normal file
163
daemon/images.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
package daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/parsers/filters"
|
||||
)
|
||||
|
||||
var acceptedImageFilterTags = map[string]struct{}{
|
||||
"dangling": {},
|
||||
"label": {},
|
||||
}
|
||||
|
||||
// byCreated is a temporary type used to sort a list of images by creation
|
||||
// time.
|
||||
type byCreated []*types.Image
|
||||
|
||||
func (r byCreated) Len() int { return len(r) }
|
||||
func (r byCreated) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
func (r byCreated) Less(i, j int) bool { return r[i].Created < r[j].Created }
|
||||
|
||||
// Map returns a map of all images in the ImageStore
|
||||
func (daemon *Daemon) Map() map[image.ID]*image.Image {
|
||||
return daemon.imageStore.Map()
|
||||
}
|
||||
|
||||
// Images returns a filtered list of images. filterArgs is a JSON-encoded set
|
||||
// of filter arguments which will be interpreted by pkg/parsers/filters.
|
||||
// filter is a shell glob string applied to repository names. The argument
|
||||
// named all controls whether all images in the graph are filtered, or just
|
||||
// the heads.
|
||||
func (daemon *Daemon) Images(filterArgs, filter string, all bool) ([]*types.Image, error) {
|
||||
var (
|
||||
allImages map[image.ID]*image.Image
|
||||
err error
|
||||
danglingOnly = false
|
||||
)
|
||||
|
||||
imageFilters, err := filters.FromParam(filterArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for name := range imageFilters {
|
||||
if _, ok := acceptedImageFilterTags[name]; !ok {
|
||||
return nil, fmt.Errorf("Invalid filter '%s'", name)
|
||||
}
|
||||
}
|
||||
|
||||
if i, ok := imageFilters["dangling"]; ok {
|
||||
for _, value := range i {
|
||||
if v := strings.ToLower(value); v == "true" {
|
||||
danglingOnly = true
|
||||
} else if v != "false" {
|
||||
return nil, fmt.Errorf("Invalid filter 'dangling=%s'", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if danglingOnly {
|
||||
allImages = daemon.imageStore.Heads()
|
||||
} else {
|
||||
allImages = daemon.imageStore.Map()
|
||||
}
|
||||
|
||||
images := []*types.Image{}
|
||||
|
||||
var filterTagged bool
|
||||
if filter != "" {
|
||||
filterRef, err := reference.Parse(filter)
|
||||
if err == nil { // parse error means wildcard repo
|
||||
if _, ok := filterRef.(reference.Tagged); ok {
|
||||
filterTagged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id, img := range allImages {
|
||||
if _, ok := imageFilters["label"]; ok {
|
||||
if img.Config == nil {
|
||||
// Very old image that do not have image.Config (or even labels)
|
||||
continue
|
||||
}
|
||||
// We are now sure image.Config is not nil
|
||||
if !imageFilters.MatchKVList("label", img.Config.Labels) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
layerID := img.RootFS.ChainID()
|
||||
var size int64
|
||||
if layerID != "" {
|
||||
l, err := daemon.layerStore.Get(layerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size, err = l.Size()
|
||||
layer.ReleaseAndLog(daemon.layerStore, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
newImage := newImage(img, size)
|
||||
|
||||
for _, ref := range daemon.tagStore.References(id) {
|
||||
if filter != "" { // filter by tag/repo name
|
||||
if filterTagged { // filter by tag, require full ref match
|
||||
if ref.String() != filter {
|
||||
continue
|
||||
}
|
||||
} else if matched, err := path.Match(filter, ref.Name()); !matched || err != nil { // name only match, FIXME: docs say exact
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, ok := ref.(reference.Digested); ok {
|
||||
newImage.RepoDigests = append(newImage.RepoDigests, ref.String())
|
||||
}
|
||||
if _, ok := ref.(reference.Tagged); ok {
|
||||
newImage.RepoTags = append(newImage.RepoTags, ref.String())
|
||||
}
|
||||
}
|
||||
if newImage.RepoDigests == nil && newImage.RepoTags == nil {
|
||||
if all || len(daemon.imageStore.Children(id)) == 0 {
|
||||
if filter != "" { // skip images with no references if filtering by tag
|
||||
continue
|
||||
}
|
||||
newImage.RepoDigests = []string{"<none>@<none>"}
|
||||
newImage.RepoTags = []string{"<none>:<none>"}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else if danglingOnly {
|
||||
continue
|
||||
}
|
||||
|
||||
images = append(images, newImage)
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(byCreated(images)))
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func newImage(image *image.Image, size int64) *types.Image {
|
||||
newImage := new(types.Image)
|
||||
newImage.ParentID = image.Parent.String()
|
||||
newImage.ID = image.ID().String()
|
||||
newImage.Created = image.Created.Unix()
|
||||
newImage.Size = size
|
||||
newImage.VirtualSize = size
|
||||
if image.Config != nil {
|
||||
newImage.Labels = image.Config.Labels
|
||||
}
|
||||
return newImage
|
||||
}
|
111
daemon/import.go
Normal file
111
daemon/import.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package daemon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/dockerversion"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/httputils"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/runconfig"
|
||||
)
|
||||
|
||||
// ImportImage imports an image, getting the archived layer data either from
|
||||
// inConfig (if src is "-"), or from a URI specified in src. Progress output is
|
||||
// written to outStream. Repository and tag names can optionally be given in
|
||||
// the repo and tag arguments, respectively.
|
||||
func (daemon *Daemon) ImportImage(src string, newRef reference.Named, msg string, inConfig io.ReadCloser, outStream io.Writer, config *runconfig.Config) error {
|
||||
var (
|
||||
sf = streamformatter.NewJSONStreamFormatter()
|
||||
archive io.ReadCloser
|
||||
resp *http.Response
|
||||
)
|
||||
|
||||
if src == "-" {
|
||||
archive = inConfig
|
||||
} else {
|
||||
inConfig.Close()
|
||||
u, err := url.Parse(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "http"
|
||||
u.Host = src
|
||||
u.Path = ""
|
||||
}
|
||||
outStream.Write(sf.FormatStatus("", "Downloading from %s", u))
|
||||
resp, err = httputils.Download(u.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
progressReader := progressreader.New(progressreader.Config{
|
||||
In: resp.Body,
|
||||
Out: outStream,
|
||||
Formatter: sf,
|
||||
Size: resp.ContentLength,
|
||||
NewLines: true,
|
||||
ID: "",
|
||||
Action: "Importing",
|
||||
})
|
||||
archive = progressReader
|
||||
}
|
||||
|
||||
defer archive.Close()
|
||||
if len(msg) == 0 {
|
||||
msg = "Imported from " + src
|
||||
}
|
||||
// TODO: support windows baselayer?
|
||||
l, err := daemon.layerStore.Register(archive, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer layer.ReleaseAndLog(daemon.layerStore, l)
|
||||
|
||||
created := time.Now().UTC()
|
||||
imgConfig, err := json.Marshal(&image.Image{
|
||||
V1Image: image.V1Image{
|
||||
DockerVersion: dockerversion.Version,
|
||||
Config: config,
|
||||
Architecture: runtime.GOARCH,
|
||||
OS: runtime.GOOS,
|
||||
Created: created,
|
||||
Comment: msg,
|
||||
},
|
||||
RootFS: &image.RootFS{
|
||||
Type: "layers",
|
||||
DiffIDs: []layer.DiffID{l.DiffID()},
|
||||
},
|
||||
History: []image.History{{
|
||||
Created: created,
|
||||
Comment: msg,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := daemon.imageStore.Create(imgConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME: connect with commit code and call tagstore directly
|
||||
if newRef != nil {
|
||||
if err := daemon.TagImage(newRef, id.String(), true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
outStream.Write(sf.FormatStatus("", id.String()))
|
||||
daemon.EventsService.Log("import", id.String(), "")
|
||||
return nil
|
||||
}
|
|
@ -62,7 +62,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) {
|
|||
v := &types.Info{
|
||||
ID: daemon.ID,
|
||||
Containers: len(daemon.List()),
|
||||
Images: len(daemon.Graph().Map()),
|
||||
Images: len(daemon.imageStore.Map()),
|
||||
Driver: daemon.GraphDriver().String(),
|
||||
DriverStatus: daemon.GraphDriver().Status(),
|
||||
Plugins: daemon.showPluginsInfo(),
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/docker/docker/api/types/versions/v1p20"
|
||||
"github.com/docker/docker/daemon/exec"
|
||||
"github.com/docker/docker/daemon/network"
|
||||
"github.com/docker/docker/layer"
|
||||
)
|
||||
|
||||
// ContainerInspect returns low-level information about a
|
||||
|
@ -124,7 +125,7 @@ func (daemon *Daemon) getInspectData(container *Container, size bool) (*types.Co
|
|||
Path: container.Path,
|
||||
Args: container.Args,
|
||||
State: containerState,
|
||||
Image: container.ImageID,
|
||||
Image: container.ImageID.String(),
|
||||
LogPath: container.LogPath,
|
||||
Name: container.Name,
|
||||
RestartCount: container.RestartCount,
|
||||
|
@ -149,7 +150,18 @@ func (daemon *Daemon) getInspectData(container *Container, size bool) (*types.Co
|
|||
contJSONBase = setPlatformSpecificContainerFields(container, contJSONBase)
|
||||
|
||||
contJSONBase.GraphDriver.Name = container.Driver
|
||||
graphDriverData, err := daemon.driver.GetMetadata(container.ID)
|
||||
|
||||
image, err := daemon.imageStore.Get(container.ImageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l, err := daemon.layerStore.Get(image.RootFS.ChainID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer layer.ReleaseAndLog(daemon.layerStore, l)
|
||||
|
||||
graphDriverData, err := l.Metadata()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/graph"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/graphdb"
|
||||
"github.com/docker/docker/pkg/nat"
|
||||
|
@ -66,7 +65,7 @@ type listContext struct {
|
|||
// names is a list of container names to filter with
|
||||
names map[string][]string
|
||||
// images is a list of images to filter with
|
||||
images map[string]bool
|
||||
images map[image.ID]bool
|
||||
// filters is a collection of arguments to filter with, specified by the user
|
||||
filters filters.Args
|
||||
// exitAllowed is a list of exit codes allowed to filter with
|
||||
|
@ -176,25 +175,24 @@ func (daemon *Daemon) foldFilter(config *ContainersConfig) (*listContext, error)
|
|||
}
|
||||
}
|
||||
|
||||
imagesFilter := map[string]bool{}
|
||||
imagesFilter := map[image.ID]bool{}
|
||||
var ancestorFilter bool
|
||||
if ancestors, ok := psFilters["ancestor"]; ok {
|
||||
ancestorFilter = true
|
||||
byParents := daemon.Graph().ByParent()
|
||||
// The idea is to walk the graph down the most "efficient" way.
|
||||
for _, ancestor := range ancestors {
|
||||
// First, get the imageId of the ancestor filter (yay)
|
||||
image, err := daemon.repositories.LookupImage(ancestor)
|
||||
id, err := daemon.GetImageID(ancestor)
|
||||
if err != nil {
|
||||
logrus.Warnf("Error while looking up for image %v", ancestor)
|
||||
continue
|
||||
}
|
||||
if imagesFilter[ancestor] {
|
||||
if imagesFilter[id] {
|
||||
// Already seen this ancestor, skip it
|
||||
continue
|
||||
}
|
||||
// Then walk down the graph and put the imageIds in imagesFilter
|
||||
populateImageFilterByParents(imagesFilter, image.ID, byParents)
|
||||
populateImageFilterByParents(imagesFilter, id, daemon.imageStore.Children)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -310,41 +308,29 @@ func includeContainerInList(container *Container, ctx *listContext) iterationAct
|
|||
return includeContainer
|
||||
}
|
||||
|
||||
func getImage(s *graph.TagStore, img, imgID string) (string, error) {
|
||||
// both Image and ImageID is actually ids, nothing to guess
|
||||
if strings.HasPrefix(imgID, img) {
|
||||
return img, nil
|
||||
}
|
||||
id, err := s.GetID(img)
|
||||
if err != nil {
|
||||
if err == graph.ErrNameIsNotExist {
|
||||
return imgID, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if id != imgID {
|
||||
return imgID, nil
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// transformContainer generates the container type expected by the docker ps command.
|
||||
func (daemon *Daemon) transformContainer(container *Container, ctx *listContext) (*types.Container, error) {
|
||||
newC := &types.Container{
|
||||
ID: container.ID,
|
||||
Names: ctx.names[container.ID],
|
||||
ImageID: container.ImageID,
|
||||
ImageID: container.ImageID.String(),
|
||||
}
|
||||
if newC.Names == nil {
|
||||
// Dead containers will often have no name, so make sure the response isn't null
|
||||
newC.Names = []string{}
|
||||
}
|
||||
|
||||
showImg, err := getImage(daemon.repositories, container.Config.Image, container.ImageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
image := container.Config.Image // if possible keep the original ref
|
||||
if image != container.ImageID.String() {
|
||||
id, err := daemon.GetImageID(image)
|
||||
if _, isDNE := err.(ErrImageDoesNotExist); err != nil && !isDNE {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil || id != container.ImageID {
|
||||
image = container.ImageID.String()
|
||||
}
|
||||
}
|
||||
newC.Image = showImg
|
||||
newC.Image = image
|
||||
|
||||
if len(container.Args) > 0 {
|
||||
args := []string{}
|
||||
|
@ -433,12 +419,10 @@ func (daemon *Daemon) Volumes(filter string) ([]*types.Volume, error) {
|
|||
return volumesOut, nil
|
||||
}
|
||||
|
||||
func populateImageFilterByParents(ancestorMap map[string]bool, imageID string, byParents map[string][]*image.Image) {
|
||||
func populateImageFilterByParents(ancestorMap map[image.ID]bool, imageID image.ID, getChildren func(image.ID) []image.ID) {
|
||||
if !ancestorMap[imageID] {
|
||||
if images, ok := byParents[imageID]; ok {
|
||||
for _, image := range images {
|
||||
populateImageFilterByParents(ancestorMap, image.ID, byParents)
|
||||
}
|
||||
for _, id := range getChildren(imageID) {
|
||||
populateImageFilterByParents(ancestorMap, id, getChildren)
|
||||
}
|
||||
ancestorMap[imageID] = true
|
||||
}
|
||||
|
|
100
distribution/metadata/blobsum_service.go
Normal file
100
distribution/metadata/blobsum_service.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/layer"
|
||||
)
|
||||
|
||||
// BlobSumService maps layer IDs to a set of known blobsums for
|
||||
// the layer.
|
||||
type BlobSumService struct {
|
||||
store Store
|
||||
}
|
||||
|
||||
// maxBlobSums is the number of blobsums to keep per layer DiffID.
|
||||
const maxBlobSums = 5
|
||||
|
||||
// NewBlobSumService creates a new blobsum mapping service.
|
||||
func NewBlobSumService(store Store) *BlobSumService {
|
||||
return &BlobSumService{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (blobserv *BlobSumService) diffIDNamespace() string {
|
||||
return "blobsum-storage"
|
||||
}
|
||||
|
||||
func (blobserv *BlobSumService) blobSumNamespace() string {
|
||||
return "blobsum-lookup"
|
||||
}
|
||||
|
||||
func (blobserv *BlobSumService) diffIDKey(diffID layer.DiffID) string {
|
||||
return string(digest.Digest(diffID).Algorithm()) + "/" + digest.Digest(diffID).Hex()
|
||||
}
|
||||
|
||||
func (blobserv *BlobSumService) blobSumKey(blobsum digest.Digest) string {
|
||||
return string(blobsum.Algorithm()) + "/" + blobsum.Hex()
|
||||
}
|
||||
|
||||
// GetBlobSums finds the blobsums associated with a layer DiffID.
|
||||
func (blobserv *BlobSumService) GetBlobSums(diffID layer.DiffID) ([]digest.Digest, error) {
|
||||
jsonBytes, err := blobserv.store.Get(blobserv.diffIDNamespace(), blobserv.diffIDKey(diffID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var blobsums []digest.Digest
|
||||
if err := json.Unmarshal(jsonBytes, &blobsums); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return blobsums, nil
|
||||
}
|
||||
|
||||
// GetDiffID finds a layer DiffID from a blobsum hash.
|
||||
func (blobserv *BlobSumService) GetDiffID(blobsum digest.Digest) (layer.DiffID, error) {
|
||||
diffIDBytes, err := blobserv.store.Get(blobserv.blobSumNamespace(), blobserv.blobSumKey(blobsum))
|
||||
if err != nil {
|
||||
return layer.DiffID(""), err
|
||||
}
|
||||
|
||||
return layer.DiffID(diffIDBytes), nil
|
||||
}
|
||||
|
||||
// Add associates a blobsum with a layer DiffID. If too many blobsums are
|
||||
// present, the oldest one is dropped.
|
||||
func (blobserv *BlobSumService) Add(diffID layer.DiffID, blobsum digest.Digest) error {
|
||||
oldBlobSums, err := blobserv.GetBlobSums(diffID)
|
||||
if err != nil {
|
||||
oldBlobSums = nil
|
||||
}
|
||||
newBlobSums := make([]digest.Digest, 0, len(oldBlobSums)+1)
|
||||
|
||||
// Copy all other blobsums to new slice
|
||||
for _, oldSum := range oldBlobSums {
|
||||
if oldSum != blobsum {
|
||||
newBlobSums = append(newBlobSums, oldSum)
|
||||
}
|
||||
}
|
||||
|
||||
newBlobSums = append(newBlobSums, blobsum)
|
||||
|
||||
if len(newBlobSums) > maxBlobSums {
|
||||
newBlobSums = newBlobSums[len(newBlobSums)-maxBlobSums:]
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(newBlobSums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = blobserv.store.Set(blobserv.diffIDNamespace(), blobserv.diffIDKey(diffID), jsonBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return blobserv.store.Set(blobserv.blobSumNamespace(), blobserv.blobSumKey(blobsum), []byte(diffID))
|
||||
}
|
105
distribution/metadata/blobsum_service_test.go
Normal file
105
distribution/metadata/blobsum_service_test.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/layer"
|
||||
)
|
||||
|
||||
func TestBlobSumService(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "blobsum-storage-service-test")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
metadataStore, err := NewFSMetadataStore(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create metadata store: %v", err)
|
||||
}
|
||||
blobSumService := NewBlobSumService(metadataStore)
|
||||
|
||||
testVectors := []struct {
|
||||
diffID layer.DiffID
|
||||
blobsums []digest.Digest
|
||||
}{
|
||||
{
|
||||
diffID: layer.DiffID("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"),
|
||||
blobsums: []digest.Digest{
|
||||
digest.Digest("sha256:f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937"),
|
||||
},
|
||||
},
|
||||
{
|
||||
diffID: layer.DiffID("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"),
|
||||
blobsums: []digest.Digest{
|
||||
digest.Digest("sha256:f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937"),
|
||||
digest.Digest("sha256:9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e"),
|
||||
},
|
||||
},
|
||||
{
|
||||
diffID: layer.DiffID("sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb"),
|
||||
blobsums: []digest.Digest{
|
||||
digest.Digest("sha256:f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937"),
|
||||
digest.Digest("sha256:9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e"),
|
||||
digest.Digest("sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"),
|
||||
digest.Digest("sha256:8902a7ca89aabbb868835260912159026637634090dd8899eee969523252236e"),
|
||||
digest.Digest("sha256:c84364306344ccc48532c52ff5209236273525231dddaaab53262322352883aa"),
|
||||
digest.Digest("sha256:aa7583bbc87532a8352bbb72520a821b3623523523a8352523a52352aaa888fe"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Set some associations
|
||||
for _, vec := range testVectors {
|
||||
for _, blobsum := range vec.blobsums {
|
||||
err := blobSumService.Add(vec.diffID, blobsum)
|
||||
if err != nil {
|
||||
t.Fatalf("error calling Set: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check the correct values are read back
|
||||
for _, vec := range testVectors {
|
||||
blobsums, err := blobSumService.GetBlobSums(vec.diffID)
|
||||
if err != nil {
|
||||
t.Fatalf("error calling Get: %v", err)
|
||||
}
|
||||
expectedBlobsums := len(vec.blobsums)
|
||||
if expectedBlobsums > 5 {
|
||||
expectedBlobsums = 5
|
||||
}
|
||||
if !reflect.DeepEqual(blobsums, vec.blobsums[len(vec.blobsums)-expectedBlobsums:len(vec.blobsums)]) {
|
||||
t.Fatal("Get returned incorrect layer ID")
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetBlobSums on a nonexistent entry
|
||||
_, err = blobSumService.GetBlobSums(layer.DiffID("sha256:82379823067823853223359023576437723560923756b03560378f4497753917"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error looking up nonexistent entry")
|
||||
}
|
||||
|
||||
// Test GetDiffID on a nonexistent entry
|
||||
_, err = blobSumService.GetDiffID(digest.Digest("sha256:82379823067823853223359023576437723560923756b03560378f4497753917"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error looking up nonexistent entry")
|
||||
}
|
||||
|
||||
// Overwrite one of the entries and read it back
|
||||
err = blobSumService.Add(testVectors[1].diffID, testVectors[0].blobsums[0])
|
||||
if err != nil {
|
||||
t.Fatalf("error calling Add: %v", err)
|
||||
}
|
||||
diffID, err := blobSumService.GetDiffID(testVectors[0].blobsums[0])
|
||||
if err != nil {
|
||||
t.Fatalf("error calling GetDiffID: %v", err)
|
||||
}
|
||||
if diffID != testVectors[1].diffID {
|
||||
t.Fatal("GetDiffID returned incorrect diffID")
|
||||
}
|
||||
}
|
65
distribution/metadata/metadata.go
Normal file
65
distribution/metadata/metadata.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Store implements a K/V store for mapping distribution-related IDs
|
||||
// to on-disk layer IDs and image IDs. The namespace identifies the type of
|
||||
// mapping (i.e. "v1ids" or "artifacts"). MetadataStore is goroutine-safe.
|
||||
type Store interface {
|
||||
// Get retrieves data by namespace and key.
|
||||
Get(namespace string, key string) ([]byte, error)
|
||||
// Set writes data indexed by namespace and key.
|
||||
Set(namespace, key string, value []byte) error
|
||||
}
|
||||
|
||||
// FSMetadataStore uses the filesystem to associate metadata with layer and
|
||||
// image IDs.
|
||||
type FSMetadataStore struct {
|
||||
sync.RWMutex
|
||||
basePath string
|
||||
}
|
||||
|
||||
// NewFSMetadataStore creates a new filesystem-based metadata store.
|
||||
func NewFSMetadataStore(basePath string) (*FSMetadataStore, error) {
|
||||
if err := os.MkdirAll(basePath, 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FSMetadataStore{
|
||||
basePath: basePath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (store *FSMetadataStore) path(namespace, key string) string {
|
||||
return filepath.Join(store.basePath, namespace, key)
|
||||
}
|
||||
|
||||
// Get retrieves data by namespace and key. The data is read from a file named
|
||||
// after the key, stored in the namespace's directory.
|
||||
func (store *FSMetadataStore) Get(namespace string, key string) ([]byte, error) {
|
||||
store.RLock()
|
||||
defer store.RUnlock()
|
||||
|
||||
return ioutil.ReadFile(store.path(namespace, key))
|
||||
}
|
||||
|
||||
// Set writes data indexed by namespace and key. The data is written to a file
|
||||
// named after the key, stored in the namespace's directory.
|
||||
func (store *FSMetadataStore) Set(namespace, key string, value []byte) error {
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
|
||||
path := store.path(namespace, key)
|
||||
tempFilePath := path + ".tmp"
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(tempFilePath, value, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tempFilePath, path)
|
||||
}
|
44
distribution/metadata/v1_id_service.go
Normal file
44
distribution/metadata/v1_id_service.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/image/v1"
|
||||
"github.com/docker/docker/layer"
|
||||
)
|
||||
|
||||
// V1IDService maps v1 IDs to layers on disk.
|
||||
type V1IDService struct {
|
||||
store Store
|
||||
}
|
||||
|
||||
// NewV1IDService creates a new V1 ID mapping service.
|
||||
func NewV1IDService(store Store) *V1IDService {
|
||||
return &V1IDService{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// namespace returns the namespace used by this service.
|
||||
func (idserv *V1IDService) namespace() string {
|
||||
return "v1id"
|
||||
}
|
||||
|
||||
// Get finds a layer by its V1 ID.
|
||||
func (idserv *V1IDService) Get(v1ID, registry string) (layer.ChainID, error) {
|
||||
if err := v1.ValidateID(v1ID); err != nil {
|
||||
return layer.ChainID(""), err
|
||||
}
|
||||
|
||||
idBytes, err := idserv.store.Get(idserv.namespace(), registry+","+v1ID)
|
||||
if err != nil {
|
||||
return layer.ChainID(""), err
|
||||
}
|
||||
return layer.ChainID(idBytes), nil
|
||||
}
|
||||
|
||||
// Set associates an image with a V1 ID.
|
||||
func (idserv *V1IDService) Set(v1ID, registry string, id layer.ChainID) error {
|
||||
if err := v1.ValidateID(v1ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return idserv.store.Set(idserv.namespace(), registry+","+v1ID, []byte(id))
|
||||
}
|
83
distribution/metadata/v1_id_service_test.go
Normal file
83
distribution/metadata/v1_id_service_test.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/layer"
|
||||
)
|
||||
|
||||
func TestV1IDService(t *testing.T) {
|
||||
tmpDir, err := ioutil.TempDir("", "v1-id-service-test")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
metadataStore, err := NewFSMetadataStore(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create metadata store: %v", err)
|
||||
}
|
||||
v1IDService := NewV1IDService(metadataStore)
|
||||
|
||||
testVectors := []struct {
|
||||
registry string
|
||||
v1ID string
|
||||
layerID layer.ChainID
|
||||
}{
|
||||
{
|
||||
registry: "registry1",
|
||||
v1ID: "f0cd5ca10b07f35512fc2f1cbf9a6cefbdb5cba70ac6b0c9e5988f4497f71937",
|
||||
layerID: layer.ChainID("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"),
|
||||
},
|
||||
{
|
||||
registry: "registry2",
|
||||
v1ID: "9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e",
|
||||
layerID: layer.ChainID("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"),
|
||||
},
|
||||
{
|
||||
registry: "registry1",
|
||||
v1ID: "9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e",
|
||||
layerID: layer.ChainID("sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb"),
|
||||
},
|
||||
}
|
||||
|
||||
// Set some associations
|
||||
for _, vec := range testVectors {
|
||||
err := v1IDService.Set(vec.v1ID, vec.registry, vec.layerID)
|
||||
if err != nil {
|
||||
t.Fatalf("error calling Set: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check the correct values are read back
|
||||
for _, vec := range testVectors {
|
||||
layerID, err := v1IDService.Get(vec.v1ID, vec.registry)
|
||||
if err != nil {
|
||||
t.Fatalf("error calling Get: %v", err)
|
||||
}
|
||||
if layerID != vec.layerID {
|
||||
t.Fatal("Get returned incorrect layer ID")
|
||||
}
|
||||
}
|
||||
|
||||
// Test Get on a nonexistent entry
|
||||
_, err = v1IDService.Get("82379823067823853223359023576437723560923756b03560378f4497753917", "registry1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error looking up nonexistent entry")
|
||||
}
|
||||
|
||||
// Overwrite one of the entries and read it back
|
||||
err = v1IDService.Set(testVectors[0].v1ID, testVectors[0].registry, testVectors[1].layerID)
|
||||
if err != nil {
|
||||
t.Fatalf("error calling Set: %v", err)
|
||||
}
|
||||
layerID, err := v1IDService.Get(testVectors[0].v1ID, testVectors[0].registry)
|
||||
if err != nil {
|
||||
t.Fatalf("error calling Get: %v", err)
|
||||
}
|
||||
if layerID != testVectors[1].layerID {
|
||||
t.Fatal("Get returned incorrect layer ID")
|
||||
}
|
||||
}
|
51
distribution/pool.go
Normal file
51
distribution/pool.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/pkg/broadcaster"
|
||||
)
|
||||
|
||||
// A Pool manages concurrent pulls. It deduplicates in-progress downloads.
|
||||
type Pool struct {
|
||||
sync.Mutex
|
||||
pullingPool map[string]*broadcaster.Buffered
|
||||
}
|
||||
|
||||
// NewPool creates a new Pool.
|
||||
func NewPool() *Pool {
|
||||
return &Pool{
|
||||
pullingPool: make(map[string]*broadcaster.Buffered),
|
||||
}
|
||||
}
|
||||
|
||||
// add checks if a pull is already running, and returns (broadcaster, true)
|
||||
// if a running operation is found. Otherwise, it creates a new one and returns
|
||||
// (broadcaster, false).
|
||||
func (pool *Pool) add(key string) (*broadcaster.Buffered, bool) {
|
||||
pool.Lock()
|
||||
defer pool.Unlock()
|
||||
|
||||
if p, exists := pool.pullingPool[key]; exists {
|
||||
return p, true
|
||||
}
|
||||
|
||||
broadcaster := broadcaster.NewBuffered()
|
||||
pool.pullingPool[key] = broadcaster
|
||||
|
||||
return broadcaster, false
|
||||
}
|
||||
|
||||
func (pool *Pool) removeWithError(key string, broadcasterResult error) error {
|
||||
pool.Lock()
|
||||
defer pool.Unlock()
|
||||
if broadcaster, exists := pool.pullingPool[key]; exists {
|
||||
broadcaster.CloseWithError(broadcasterResult)
|
||||
delete(pool.pullingPool, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pool *Pool) remove(key string) error {
|
||||
return pool.removeWithError(key, nil)
|
||||
}
|
28
distribution/pool_test.go
Normal file
28
distribution/pool_test.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPools(t *testing.T) {
|
||||
p := NewPool()
|
||||
|
||||
if _, found := p.add("test1"); found {
|
||||
t.Fatal("Expected pull test1 not to be in progress")
|
||||
}
|
||||
if _, found := p.add("test2"); found {
|
||||
t.Fatal("Expected pull test2 not to be in progress")
|
||||
}
|
||||
if _, found := p.add("test1"); !found {
|
||||
t.Fatalf("Expected pull test1 to be in progress`")
|
||||
}
|
||||
if err := p.remove("test2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := p.remove("test2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := p.remove("test1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package graph
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -6,10 +6,15 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/daemon/events"
|
||||
"github.com/docker/docker/distribution/metadata"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/docker/tag"
|
||||
)
|
||||
|
||||
// ImagePullConfig stores pull configuration.
|
||||
|
@ -23,15 +28,30 @@ type ImagePullConfig struct {
|
|||
// OutStream is the output writer for showing the status of the pull
|
||||
// operation.
|
||||
OutStream io.Writer
|
||||
// RegistryService is the registry service to use for TLS configuration
|
||||
// and endpoint lookup.
|
||||
RegistryService *registry.Service
|
||||
// EventsService is the events service to use for logging.
|
||||
EventsService *events.Events
|
||||
// MetadataStore is the storage backend for distribution-specific
|
||||
// metadata.
|
||||
MetadataStore metadata.Store
|
||||
// LayerStore manages layers.
|
||||
LayerStore layer.Store
|
||||
// ImageStore manages images.
|
||||
ImageStore image.Store
|
||||
// TagStore manages tags.
|
||||
TagStore tag.Store
|
||||
// Pool manages concurrent pulls.
|
||||
Pool *Pool
|
||||
}
|
||||
|
||||
// puller is an interface that abstracts pulling for different API versions.
|
||||
type puller interface {
|
||||
// Puller is an interface that abstracts pulling for different API versions.
|
||||
type Puller interface {
|
||||
// Pull tries to pull the image referenced by `tag`
|
||||
// Pull returns an error if any, as well as a boolean that determines whether to retry Pull on the next configured endpoint.
|
||||
//
|
||||
// TODO(tiborvass): have Pull() take a reference to repository + tag, so that the puller itself is repository-agnostic.
|
||||
Pull(tag string) (fallback bool, err error)
|
||||
Pull(ref reference.Named) (fallback bool, err error)
|
||||
}
|
||||
|
||||
// newPuller returns a Puller interface that will pull from either a v1 or v2
|
||||
|
@ -39,23 +59,23 @@ type puller interface {
|
|||
// whether a v1 or v2 puller will be created. The other parameters are passed
|
||||
// through to the underlying puller implementation for use during the actual
|
||||
// pull operation.
|
||||
func newPuller(s *TagStore, endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, imagePullConfig *ImagePullConfig, sf *streamformatter.StreamFormatter) (puller, error) {
|
||||
func newPuller(endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, imagePullConfig *ImagePullConfig, sf *streamformatter.StreamFormatter) (Puller, error) {
|
||||
switch endpoint.Version {
|
||||
case registry.APIVersion2:
|
||||
return &v2Puller{
|
||||
TagStore: s,
|
||||
endpoint: endpoint,
|
||||
config: imagePullConfig,
|
||||
sf: sf,
|
||||
repoInfo: repoInfo,
|
||||
blobSumService: metadata.NewBlobSumService(imagePullConfig.MetadataStore),
|
||||
endpoint: endpoint,
|
||||
config: imagePullConfig,
|
||||
sf: sf,
|
||||
repoInfo: repoInfo,
|
||||
}, nil
|
||||
case registry.APIVersion1:
|
||||
return &v1Puller{
|
||||
TagStore: s,
|
||||
endpoint: endpoint,
|
||||
config: imagePullConfig,
|
||||
sf: sf,
|
||||
repoInfo: repoInfo,
|
||||
v1IDService: metadata.NewV1IDService(imagePullConfig.MetadataStore),
|
||||
endpoint: endpoint,
|
||||
config: imagePullConfig,
|
||||
sf: sf,
|
||||
repoInfo: repoInfo,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)
|
||||
|
@ -63,29 +83,26 @@ func newPuller(s *TagStore, endpoint registry.APIEndpoint, repoInfo *registry.Re
|
|||
|
||||
// Pull initiates a pull operation. image is the repository name to pull, and
|
||||
// tag may be either empty, or indicate a specific tag to pull.
|
||||
func (s *TagStore) Pull(image string, tag string, imagePullConfig *ImagePullConfig) error {
|
||||
func Pull(ref reference.Named, imagePullConfig *ImagePullConfig) error {
|
||||
var sf = streamformatter.NewJSONStreamFormatter()
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := s.registryService.ResolveRepository(image)
|
||||
repoInfo, err := imagePullConfig.RegistryService.ResolveRepository(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// makes sure name is not empty or `scratch`
|
||||
if err := validateRepoName(repoInfo.LocalName); err != nil {
|
||||
if err := validateRepoName(repoInfo.LocalName.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := s.registryService.LookupPullEndpoints(repoInfo.CanonicalName)
|
||||
endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(repoInfo.CanonicalName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logName := repoInfo.LocalName
|
||||
if tag != "" {
|
||||
logName = utils.ImageReference(logName, tag)
|
||||
}
|
||||
logName := registry.NormalizeLocalReference(ref)
|
||||
|
||||
var (
|
||||
// use a slice to append the error strings and return a joined string to caller
|
||||
|
@ -103,12 +120,12 @@ func (s *TagStore) Pull(image string, tag string, imagePullConfig *ImagePullConf
|
|||
for _, endpoint := range endpoints {
|
||||
logrus.Debugf("Trying to pull %s from %s %s", repoInfo.LocalName, endpoint.URL, endpoint.Version)
|
||||
|
||||
puller, err := newPuller(s, endpoint, repoInfo, imagePullConfig, sf)
|
||||
puller, err := newPuller(endpoint, repoInfo, imagePullConfig, sf)
|
||||
if err != nil {
|
||||
errors = append(errors, err.Error())
|
||||
continue
|
||||
}
|
||||
if fallback, err := puller.Pull(tag); err != nil {
|
||||
if fallback, err := puller.Pull(ref); err != nil {
|
||||
if fallback {
|
||||
if _, ok := err.(registry.ErrNoSupport); !ok {
|
||||
// Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors.
|
||||
|
@ -130,12 +147,12 @@ func (s *TagStore) Pull(image string, tag string, imagePullConfig *ImagePullConf
|
|||
}
|
||||
}
|
||||
|
||||
s.eventsService.Log("pull", logName, "")
|
||||
imagePullConfig.EventsService.Log("pull", logName.String(), "")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(errors) == 0 {
|
||||
return fmt.Errorf("no endpoints found for %s", image)
|
||||
return fmt.Errorf("no endpoints found for %s", ref.String())
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
|
@ -155,3 +172,14 @@ func writeStatus(requestedTag string, out io.Writer, sf *streamformatter.StreamF
|
|||
out.Write(sf.FormatStatus("", "Status: Image is up to date for %s", requestedTag))
|
||||
}
|
||||
}
|
||||
|
||||
// validateRepoName validates the name of a repository.
|
||||
func validateRepoName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("Repository name can't be empty")
|
||||
}
|
||||
if name == "scratch" {
|
||||
return fmt.Errorf("'scratch' is a reserved name")
|
||||
}
|
||||
return nil
|
||||
}
|
454
distribution/pull_v1.go
Normal file
454
distribution/pull_v1.go
Normal file
|
@ -0,0 +1,454 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/distribution/metadata"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/image/v1"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
)
|
||||
|
||||
type v1Puller struct {
|
||||
v1IDService *metadata.V1IDService
|
||||
endpoint registry.APIEndpoint
|
||||
config *ImagePullConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
repoInfo *registry.RepositoryInfo
|
||||
session *registry.Session
|
||||
}
|
||||
|
||||
func (p *v1Puller) Pull(ref reference.Named) (fallback bool, err error) {
|
||||
if _, isDigested := ref.(reference.Digested); isDigested {
|
||||
// Allowing fallback, because HTTPS v1 is before HTTP v2
|
||||
return true, registry.ErrNoSupport{errors.New("Cannot pull by digest with v1 registry")}
|
||||
}
|
||||
|
||||
tlsConfig, err := p.config.RegistryService.TLSConfig(p.repoInfo.Index.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Adds Docker-specific headers as well as user-specified headers (metaHeaders)
|
||||
tr := transport.NewTransport(
|
||||
// TODO(tiborvass): was ReceiveTimeout
|
||||
registry.NewTransport(tlsConfig),
|
||||
registry.DockerHeaders(p.config.MetaHeaders)...,
|
||||
)
|
||||
client := registry.HTTPClient(tr)
|
||||
v1Endpoint, err := p.endpoint.ToV1Endpoint(p.config.MetaHeaders)
|
||||
if err != nil {
|
||||
logrus.Debugf("Could not get v1 endpoint: %v", err)
|
||||
return true, err
|
||||
}
|
||||
p.session, err = registry.NewSession(client, p.config.AuthConfig, v1Endpoint)
|
||||
if err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
logrus.Debugf("Fallback from error: %s", err)
|
||||
return true, err
|
||||
}
|
||||
if err := p.pullRepository(ref); err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
return false, err
|
||||
}
|
||||
out := p.config.OutStream
|
||||
out.Write(p.sf.FormatStatus("", "%s: this image was pulled from a legacy registry. Important: This registry version will not be supported in future versions of docker.", p.repoInfo.CanonicalName.Name()))
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *v1Puller) pullRepository(ref reference.Named) error {
|
||||
out := p.config.OutStream
|
||||
out.Write(p.sf.FormatStatus("", "Pulling repository %s", p.repoInfo.CanonicalName.Name()))
|
||||
|
||||
repoData, err := p.session.GetRepositoryData(p.repoInfo.RemoteName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "HTTP code: 404") {
|
||||
return fmt.Errorf("Error: image %s not found", p.repoInfo.RemoteName.Name())
|
||||
}
|
||||
// Unexpected HTTP error
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("Retrieving the tag list")
|
||||
var tagsList map[string]string
|
||||
tagged, isTagged := ref.(reference.Tagged)
|
||||
if !isTagged {
|
||||
tagsList, err = p.session.GetRemoteTags(repoData.Endpoints, p.repoInfo.RemoteName)
|
||||
} else {
|
||||
var tagID string
|
||||
tagsList = make(map[string]string)
|
||||
tagID, err = p.session.GetRemoteTag(repoData.Endpoints, p.repoInfo.RemoteName, tagged.Tag())
|
||||
if err == registry.ErrRepoNotFound {
|
||||
return fmt.Errorf("Tag %s not found in repository %s", tagged.Tag(), p.repoInfo.CanonicalName.Name())
|
||||
}
|
||||
tagsList[tagged.Tag()] = tagID
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Errorf("unable to get remote tags: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for tag, id := range tagsList {
|
||||
repoData.ImgList[id] = ®istry.ImgData{
|
||||
ID: id,
|
||||
Tag: tag,
|
||||
Checksum: "",
|
||||
}
|
||||
}
|
||||
|
||||
errors := make(chan error)
|
||||
layerDownloaded := make(chan struct{})
|
||||
|
||||
layersDownloaded := false
|
||||
var wg sync.WaitGroup
|
||||
for _, imgData := range repoData.ImgList {
|
||||
if isTagged && imgData.Tag != tagged.Tag() {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(img *registry.ImgData) {
|
||||
p.downloadImage(out, repoData, img, layerDownloaded, errors)
|
||||
wg.Done()
|
||||
}(imgData)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
}()
|
||||
|
||||
var lastError error
|
||||
selectLoop:
|
||||
for {
|
||||
select {
|
||||
case err, ok := <-errors:
|
||||
if !ok {
|
||||
break selectLoop
|
||||
}
|
||||
lastError = err
|
||||
case <-layerDownloaded:
|
||||
layersDownloaded = true
|
||||
}
|
||||
}
|
||||
|
||||
if lastError != nil {
|
||||
return lastError
|
||||
}
|
||||
|
||||
localNameRef := p.repoInfo.LocalName
|
||||
if isTagged {
|
||||
localNameRef, err = reference.WithTag(localNameRef, tagged.Tag())
|
||||
if err != nil {
|
||||
localNameRef = p.repoInfo.LocalName
|
||||
}
|
||||
}
|
||||
writeStatus(localNameRef.String(), out, p.sf, layersDownloaded)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *v1Puller) downloadImage(out io.Writer, repoData *registry.RepositoryData, img *registry.ImgData, layerDownloaded chan struct{}, errors chan error) {
|
||||
if img.Tag == "" {
|
||||
logrus.Debugf("Image (id: %s) present in this repository but untagged, skipping", img.ID)
|
||||
return
|
||||
}
|
||||
|
||||
localNameRef, err := reference.WithTag(p.repoInfo.LocalName, img.Tag)
|
||||
if err != nil {
|
||||
retErr := fmt.Errorf("Image (id: %s) has invalid tag: %s", img.ID, img.Tag)
|
||||
logrus.Debug(retErr.Error())
|
||||
errors <- retErr
|
||||
}
|
||||
|
||||
if err := v1.ValidateID(img.ID); err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s", img.Tag, p.repoInfo.CanonicalName.Name()), nil))
|
||||
success := false
|
||||
var lastErr error
|
||||
var isDownloaded bool
|
||||
for _, ep := range p.repoInfo.Index.Mirrors {
|
||||
ep += "v1/"
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, mirror: %s", img.Tag, p.repoInfo.CanonicalName.Name(), ep), nil))
|
||||
if isDownloaded, err = p.pullImage(out, img.ID, ep, localNameRef); err != nil {
|
||||
// Don't report errors when pulling from mirrors.
|
||||
logrus.Debugf("Error pulling image (%s) from %s, mirror: %s, %s", img.Tag, p.repoInfo.CanonicalName.Name(), ep, err)
|
||||
continue
|
||||
}
|
||||
if isDownloaded {
|
||||
layerDownloaded <- struct{}{}
|
||||
}
|
||||
success = true
|
||||
break
|
||||
}
|
||||
if !success {
|
||||
for _, ep := range repoData.Endpoints {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, endpoint: %s", img.Tag, p.repoInfo.CanonicalName.Name(), ep), nil))
|
||||
if isDownloaded, err = p.pullImage(out, img.ID, ep, localNameRef); err != nil {
|
||||
// It's not ideal that only the last error is returned, it would be better to concatenate the errors.
|
||||
// As the error is also given to the output stream the user will see the error.
|
||||
lastErr = err
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, p.repoInfo.CanonicalName.Name(), ep, err), nil))
|
||||
continue
|
||||
}
|
||||
if isDownloaded {
|
||||
layerDownloaded <- struct{}{}
|
||||
}
|
||||
success = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
err := fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, p.repoInfo.CanonicalName.Name(), lastErr)
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), err.Error(), nil))
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Download complete", nil))
|
||||
}
|
||||
|
||||
func (p *v1Puller) pullImage(out io.Writer, v1ID, endpoint string, localNameRef reference.Named) (layersDownloaded bool, err error) {
|
||||
var history []string
|
||||
history, err = p.session.GetRemoteHistory(v1ID, endpoint)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(history) < 1 {
|
||||
return false, fmt.Errorf("empty history for image %s", v1ID)
|
||||
}
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(v1ID), "Pulling dependent layers", nil))
|
||||
// FIXME: Try to stream the images?
|
||||
// FIXME: Launch the getRemoteImage() in goroutines
|
||||
|
||||
var (
|
||||
referencedLayers []layer.Layer
|
||||
parentID layer.ChainID
|
||||
newHistory []image.History
|
||||
img *image.V1Image
|
||||
imgJSON []byte
|
||||
imgSize int64
|
||||
)
|
||||
|
||||
defer func() {
|
||||
for _, l := range referencedLayers {
|
||||
layer.ReleaseAndLog(p.config.LayerStore, l)
|
||||
}
|
||||
}()
|
||||
|
||||
layersDownloaded = false
|
||||
|
||||
// Iterate over layers from top-most to bottom-most, checking if any
|
||||
// already exist on disk.
|
||||
var i int
|
||||
for i = 0; i != len(history); i++ {
|
||||
v1LayerID := history[i]
|
||||
// Do we have a mapping for this particular v1 ID on this
|
||||
// registry?
|
||||
if layerID, err := p.v1IDService.Get(v1LayerID, p.repoInfo.Index.Name); err == nil {
|
||||
// Does the layer actually exist
|
||||
if l, err := p.config.LayerStore.Get(layerID); err == nil {
|
||||
for j := i; j >= 0; j-- {
|
||||
logrus.Debugf("Layer already exists: %s", history[j])
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(history[j]), "Already exists", nil))
|
||||
}
|
||||
referencedLayers = append(referencedLayers, l)
|
||||
parentID = layerID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
needsDownload := i
|
||||
|
||||
// Iterate over layers, in order from bottom-most to top-most. Download
|
||||
// config for all layers, and download actual layer data if needed.
|
||||
for i = len(history) - 1; i >= 0; i-- {
|
||||
v1LayerID := history[i]
|
||||
imgJSON, imgSize, err = p.downloadLayerConfig(out, v1LayerID, endpoint)
|
||||
if err != nil {
|
||||
return layersDownloaded, err
|
||||
}
|
||||
|
||||
img = &image.V1Image{}
|
||||
if err := json.Unmarshal(imgJSON, img); err != nil {
|
||||
return layersDownloaded, err
|
||||
}
|
||||
|
||||
if i < needsDownload {
|
||||
l, err := p.downloadLayer(out, v1LayerID, endpoint, parentID, imgSize, &layersDownloaded)
|
||||
|
||||
// Note: This needs to be done even in the error case to avoid
|
||||
// stale references to the layer.
|
||||
if l != nil {
|
||||
referencedLayers = append(referencedLayers, l)
|
||||
}
|
||||
if err != nil {
|
||||
return layersDownloaded, err
|
||||
}
|
||||
|
||||
parentID = l.ChainID()
|
||||
}
|
||||
|
||||
// Create a new-style config from the legacy configs
|
||||
h, err := v1.HistoryFromConfig(imgJSON, false)
|
||||
if err != nil {
|
||||
return layersDownloaded, err
|
||||
}
|
||||
newHistory = append(newHistory, h)
|
||||
}
|
||||
|
||||
rootFS := image.NewRootFS()
|
||||
l := referencedLayers[len(referencedLayers)-1]
|
||||
for l != nil {
|
||||
rootFS.DiffIDs = append([]layer.DiffID{l.DiffID()}, rootFS.DiffIDs...)
|
||||
l = l.Parent()
|
||||
}
|
||||
|
||||
config, err := v1.MakeConfigFromV1Config(imgJSON, rootFS, newHistory)
|
||||
if err != nil {
|
||||
return layersDownloaded, err
|
||||
}
|
||||
|
||||
imageID, err := p.config.ImageStore.Create(config)
|
||||
if err != nil {
|
||||
return layersDownloaded, err
|
||||
}
|
||||
|
||||
if err := p.config.TagStore.Add(localNameRef, imageID, true); err != nil {
|
||||
return layersDownloaded, err
|
||||
}
|
||||
|
||||
return layersDownloaded, nil
|
||||
}
|
||||
|
||||
func (p *v1Puller) downloadLayerConfig(out io.Writer, v1LayerID, endpoint string) (imgJSON []byte, imgSize int64, err error) {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(v1LayerID), "Pulling metadata", nil))
|
||||
|
||||
retries := 5
|
||||
for j := 1; j <= retries; j++ {
|
||||
imgJSON, imgSize, err := p.session.GetRemoteImageJSON(v1LayerID, endpoint)
|
||||
if err != nil && j == retries {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(v1LayerID), "Error pulling layer metadata", nil))
|
||||
return nil, 0, err
|
||||
} else if err != nil {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
return imgJSON, imgSize, nil
|
||||
}
|
||||
|
||||
// not reached
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (p *v1Puller) downloadLayer(out io.Writer, v1LayerID, endpoint string, parentID layer.ChainID, layerSize int64, layersDownloaded *bool) (l layer.Layer, err error) {
|
||||
// ensure no two downloads of the same layer happen at the same time
|
||||
poolKey := "layer:" + v1LayerID
|
||||
broadcaster, found := p.config.Pool.add(poolKey)
|
||||
broadcaster.Add(out)
|
||||
if found {
|
||||
logrus.Debugf("Image (id: %s) pull is already running, skipping", v1LayerID)
|
||||
if err = broadcaster.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
layerID, err := p.v1IDService.Get(v1LayerID, p.repoInfo.Index.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Does the layer actually exist
|
||||
l, err := p.config.LayerStore.Get(layerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// This must use a closure so it captures the value of err when
|
||||
// the function returns, not when the 'defer' is evaluated.
|
||||
defer func() {
|
||||
p.config.Pool.removeWithError(poolKey, err)
|
||||
}()
|
||||
|
||||
retries := 5
|
||||
for j := 1; j <= retries; j++ {
|
||||
// Get the layer
|
||||
status := "Pulling fs layer"
|
||||
if j > 1 {
|
||||
status = fmt.Sprintf("Pulling fs layer [retries: %d]", j)
|
||||
}
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(v1LayerID), status, nil))
|
||||
layerReader, err := p.session.GetRemoteImageLayer(v1LayerID, endpoint, layerSize)
|
||||
if uerr, ok := err.(*url.Error); ok {
|
||||
err = uerr.Err
|
||||
}
|
||||
if terr, ok := err.(net.Error); ok && terr.Timeout() && j < retries {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else if err != nil {
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(v1LayerID), "Error pulling dependent layers", nil))
|
||||
return nil, err
|
||||
}
|
||||
*layersDownloaded = true
|
||||
defer layerReader.Close()
|
||||
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: layerReader,
|
||||
Out: broadcaster,
|
||||
Formatter: p.sf,
|
||||
Size: layerSize,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(v1LayerID),
|
||||
Action: "Downloading",
|
||||
})
|
||||
|
||||
inflatedLayerData, err := archive.DecompressStream(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get decompression stream: %v", err)
|
||||
}
|
||||
|
||||
l, err := p.config.LayerStore.Register(inflatedLayerData, parentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register layer: %v", err)
|
||||
}
|
||||
logrus.Debugf("layer %s registered successfully", l.DiffID())
|
||||
|
||||
if terr, ok := err.(net.Error); ok && terr.Timeout() && j < retries {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else if err != nil {
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(v1LayerID), "Error downloading dependent layers", nil))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cache mapping from this v1 ID to content-addressable layer ID
|
||||
if err := p.v1IDService.Set(v1LayerID, p.repoInfo.Index.Name, l.ChainID()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(v1LayerID), "Download complete", nil))
|
||||
broadcaster.Close()
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// not reached
|
||||
return nil, nil
|
||||
}
|
512
distribution/pull_v2.go
Normal file
512
distribution/pull_v2.go
Normal file
|
@ -0,0 +1,512 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/distribution/metadata"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/image/v1"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/broadcaster"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type v2Puller struct {
|
||||
blobSumService *metadata.BlobSumService
|
||||
endpoint registry.APIEndpoint
|
||||
config *ImagePullConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
repoInfo *registry.RepositoryInfo
|
||||
repo distribution.Repository
|
||||
sessionID string
|
||||
}
|
||||
|
||||
func (p *v2Puller) Pull(ref reference.Named) (fallback bool, err error) {
|
||||
// TODO(tiborvass): was ReceiveTimeout
|
||||
p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull")
|
||||
if err != nil {
|
||||
logrus.Debugf("Error getting v2 registry: %v", err)
|
||||
return true, err
|
||||
}
|
||||
|
||||
p.sessionID = stringid.GenerateRandomID()
|
||||
|
||||
if err := p.pullV2Repository(ref); err != nil {
|
||||
if registry.ContinueOnError(err) {
|
||||
logrus.Debugf("Error trying v2 registry: %v", err)
|
||||
return true, err
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *v2Puller) pullV2Repository(ref reference.Named) (err error) {
|
||||
var refs []reference.Named
|
||||
taggedName := p.repoInfo.LocalName
|
||||
if tagged, isTagged := ref.(reference.Tagged); isTagged {
|
||||
taggedName, err = reference.WithTag(p.repoInfo.LocalName, tagged.Tag())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refs = []reference.Named{taggedName}
|
||||
} else if digested, isDigested := ref.(reference.Digested); isDigested {
|
||||
taggedName, err = reference.WithDigest(p.repoInfo.LocalName, digested.Digest())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refs = []reference.Named{taggedName}
|
||||
} else {
|
||||
manSvc, err := p.repo.Manifests(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags, err := manSvc.Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// This probably becomes a lot nicer after the manifest
|
||||
// refactor...
|
||||
for _, tag := range tags {
|
||||
tagRef, err := reference.WithTag(p.repoInfo.LocalName, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refs = append(refs, tagRef)
|
||||
}
|
||||
}
|
||||
|
||||
var layersDownloaded bool
|
||||
for _, pullRef := range refs {
|
||||
// pulledNew is true if either new layers were downloaded OR if existing images were newly tagged
|
||||
// TODO(tiborvass): should we change the name of `layersDownload`? What about message in WriteStatus?
|
||||
pulledNew, err := p.pullV2Tag(p.config.OutStream, pullRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
layersDownloaded = layersDownloaded || pulledNew
|
||||
}
|
||||
|
||||
writeStatus(taggedName.String(), p.config.OutStream, p.sf, layersDownloaded)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadInfo is used to pass information from download to extractor
|
||||
type downloadInfo struct {
|
||||
tmpFile *os.File
|
||||
digest digest.Digest
|
||||
layer distribution.ReadSeekCloser
|
||||
size int64
|
||||
err chan error
|
||||
poolKey string
|
||||
broadcaster *broadcaster.Buffered
|
||||
}
|
||||
|
||||
type errVerification struct{}
|
||||
|
||||
func (errVerification) Error() string { return "verification failed" }
|
||||
|
||||
func (p *v2Puller) download(di *downloadInfo) {
|
||||
logrus.Debugf("pulling blob %q", di.digest)
|
||||
|
||||
blobs := p.repo.Blobs(context.Background())
|
||||
|
||||
desc, err := blobs.Stat(context.Background(), di.digest)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error statting layer: %v", err)
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
di.size = desc.Size
|
||||
|
||||
layerDownload, err := blobs.Open(context.Background(), di.digest)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error fetching layer: %v", err)
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
defer layerDownload.Close()
|
||||
|
||||
verifier, err := digest.NewDigestVerifier(di.digest)
|
||||
if err != nil {
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
|
||||
digestStr := di.digest.String()
|
||||
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: ioutil.NopCloser(io.TeeReader(layerDownload, verifier)),
|
||||
Out: di.broadcaster,
|
||||
Formatter: p.sf,
|
||||
Size: di.size,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(digestStr),
|
||||
Action: "Downloading",
|
||||
})
|
||||
io.Copy(di.tmpFile, reader)
|
||||
|
||||
di.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(digestStr), "Verifying Checksum", nil))
|
||||
|
||||
if !verifier.Verified() {
|
||||
err = fmt.Errorf("filesystem layer verification failed for digest %s", di.digest)
|
||||
logrus.Error(err)
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
|
||||
di.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(digestStr), "Download complete", nil))
|
||||
|
||||
logrus.Debugf("Downloaded %s to tempfile %s", digestStr, di.tmpFile.Name())
|
||||
di.layer = layerDownload
|
||||
|
||||
di.err <- nil
|
||||
}
|
||||
|
||||
func (p *v2Puller) pullV2Tag(out io.Writer, ref reference.Named) (tagUpdated bool, err error) {
|
||||
tagOrDigest := ""
|
||||
if tagged, isTagged := ref.(reference.Tagged); isTagged {
|
||||
tagOrDigest = tagged.Tag()
|
||||
} else if digested, isDigested := ref.(reference.Digested); isDigested {
|
||||
tagOrDigest = digested.Digest().String()
|
||||
} else {
|
||||
return false, fmt.Errorf("internal error: reference has neither a tag nor a digest: %s", ref.String())
|
||||
}
|
||||
|
||||
logrus.Debugf("Pulling ref from V2 registry: %q", tagOrDigest)
|
||||
|
||||
manSvc, err := p.repo.Manifests(context.Background())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
unverifiedManifest, err := manSvc.GetByTag(tagOrDigest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if unverifiedManifest == nil {
|
||||
return false, fmt.Errorf("image manifest does not exist for tag or digest %q", tagOrDigest)
|
||||
}
|
||||
var verifiedManifest *schema1.Manifest
|
||||
verifiedManifest, err = verifyManifest(unverifiedManifest, ref)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
rootFS := image.NewRootFS()
|
||||
|
||||
if err := detectBaseLayer(p.config.ImageStore, verifiedManifest, rootFS); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// remove duplicate layers and check parent chain validity
|
||||
err = fixManifestLayers(verifiedManifest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
out.Write(p.sf.FormatStatus(tagOrDigest, "Pulling from %s", p.repo.Name()))
|
||||
|
||||
var downloads []*downloadInfo
|
||||
|
||||
defer func() {
|
||||
for _, d := range downloads {
|
||||
p.config.Pool.removeWithError(d.poolKey, err)
|
||||
if d.tmpFile != nil {
|
||||
d.tmpFile.Close()
|
||||
if err := os.RemoveAll(d.tmpFile.Name()); err != nil {
|
||||
logrus.Errorf("Failed to remove temp file: %s", d.tmpFile.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Image history converted to the new format
|
||||
var history []image.History
|
||||
|
||||
poolKey := "v2layer:"
|
||||
notFoundLocally := false
|
||||
|
||||
// Note that the order of this loop is in the direction of bottom-most
|
||||
// to top-most, so that the downloads slice gets ordered correctly.
|
||||
for i := len(verifiedManifest.FSLayers) - 1; i >= 0; i-- {
|
||||
blobSum := verifiedManifest.FSLayers[i].BlobSum
|
||||
poolKey += blobSum.String()
|
||||
|
||||
var throwAway struct {
|
||||
ThrowAway bool `json:"throwaway,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(verifiedManifest.History[i].V1Compatibility), &throwAway); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
h, err := v1.HistoryFromConfig([]byte(verifiedManifest.History[i].V1Compatibility), throwAway.ThrowAway)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
history = append(history, h)
|
||||
|
||||
if throwAway.ThrowAway {
|
||||
continue
|
||||
}
|
||||
|
||||
// Do we have a layer on disk corresponding to the set of
|
||||
// blobsums up to this point?
|
||||
if !notFoundLocally {
|
||||
notFoundLocally = true
|
||||
diffID, err := p.blobSumService.GetDiffID(blobSum)
|
||||
if err == nil {
|
||||
rootFS.Append(diffID)
|
||||
if l, err := p.config.LayerStore.Get(rootFS.ChainID()); err == nil {
|
||||
notFoundLocally = false
|
||||
logrus.Debugf("Layer already exists: %s", blobSum.String())
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(blobSum.String()), "Already exists", nil))
|
||||
defer layer.ReleaseAndLog(p.config.LayerStore, l)
|
||||
continue
|
||||
} else {
|
||||
rootFS.DiffIDs = rootFS.DiffIDs[:len(rootFS.DiffIDs)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(blobSum.String()), "Pulling fs layer", nil))
|
||||
|
||||
tmpFile, err := ioutil.TempFile("", "GetImageBlob")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
d := &downloadInfo{
|
||||
poolKey: poolKey,
|
||||
digest: blobSum,
|
||||
tmpFile: tmpFile,
|
||||
// TODO: seems like this chan buffer solved hanging problem in go1.5,
|
||||
// this can indicate some deeper problem that somehow we never take
|
||||
// error from channel in loop below
|
||||
err: make(chan error, 1),
|
||||
}
|
||||
|
||||
downloads = append(downloads, d)
|
||||
|
||||
broadcaster, found := p.config.Pool.add(d.poolKey)
|
||||
broadcaster.Add(out)
|
||||
d.broadcaster = broadcaster
|
||||
if found {
|
||||
d.err <- nil
|
||||
} else {
|
||||
go p.download(d)
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range downloads {
|
||||
if err := <-d.err; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if d.layer == nil {
|
||||
// Wait for a different pull to download and extract
|
||||
// this layer.
|
||||
err = d.broadcaster.Wait()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
diffID, err := p.blobSumService.GetDiffID(d.digest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
rootFS.Append(diffID)
|
||||
|
||||
l, err := p.config.LayerStore.Get(rootFS.ChainID())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
defer layer.ReleaseAndLog(p.config.LayerStore, l)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
d.tmpFile.Seek(0, 0)
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: d.tmpFile,
|
||||
Out: d.broadcaster,
|
||||
Formatter: p.sf,
|
||||
Size: d.size,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(d.digest.String()),
|
||||
Action: "Extracting",
|
||||
})
|
||||
|
||||
inflatedLayerData, err := archive.DecompressStream(reader)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("could not get decompression stream: %v", err)
|
||||
}
|
||||
|
||||
l, err := p.config.LayerStore.Register(inflatedLayerData, rootFS.ChainID())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to register layer: %v", err)
|
||||
}
|
||||
logrus.Debugf("layer %s registered successfully", l.DiffID())
|
||||
rootFS.Append(l.DiffID())
|
||||
|
||||
// Cache mapping from this layer's DiffID to the blobsum
|
||||
if err := p.blobSumService.Add(l.DiffID(), d.digest); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
defer layer.ReleaseAndLog(p.config.LayerStore, l)
|
||||
|
||||
d.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(d.digest.String()), "Pull complete", nil))
|
||||
d.broadcaster.Close()
|
||||
tagUpdated = true
|
||||
}
|
||||
|
||||
config, err := v1.MakeConfigFromV1Config([]byte(verifiedManifest.History[0].V1Compatibility), rootFS, history)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
imageID, err := p.config.ImageStore.Create(config)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
manifestDigest, _, err := digestFromManifest(unverifiedManifest, p.repoInfo.LocalName.Name())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check for new tag if no layers downloaded
|
||||
var oldTagImageID image.ID
|
||||
if !tagUpdated {
|
||||
oldTagImageID, err = p.config.TagStore.Get(ref)
|
||||
if err != nil || oldTagImageID != imageID {
|
||||
tagUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if tagUpdated {
|
||||
if err = p.config.TagStore.Add(ref, imageID, true); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if manifestDigest != "" {
|
||||
out.Write(p.sf.FormatStatus("", "Digest: %s", manifestDigest))
|
||||
}
|
||||
|
||||
return tagUpdated, nil
|
||||
}
|
||||
|
||||
func verifyManifest(signedManifest *schema1.SignedManifest, ref reference.Reference) (m *schema1.Manifest, err error) {
|
||||
// If pull by digest, then verify the manifest digest. NOTE: It is
|
||||
// important to do this first, before any other content validation. If the
|
||||
// digest cannot be verified, don't even bother with those other things.
|
||||
if digested, isDigested := ref.(reference.Digested); isDigested {
|
||||
verifier, err := digest.NewDigestVerifier(digested.Digest())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := signedManifest.Payload()
|
||||
if err != nil {
|
||||
// If this failed, the signatures section was corrupted
|
||||
// or missing. Treat the entire manifest as the payload.
|
||||
payload = signedManifest.Raw
|
||||
}
|
||||
if _, err := verifier.Write(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !verifier.Verified() {
|
||||
err := fmt.Errorf("image verification failed for digest %s", digested.Digest())
|
||||
logrus.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var verifiedManifest schema1.Manifest
|
||||
if err = json.Unmarshal(payload, &verifiedManifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m = &verifiedManifest
|
||||
} else {
|
||||
m = &signedManifest.Manifest
|
||||
}
|
||||
|
||||
if m.SchemaVersion != 1 {
|
||||
return nil, fmt.Errorf("unsupported schema version %d for %q", m.SchemaVersion, ref.String())
|
||||
}
|
||||
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 m, nil
|
||||
}
|
||||
|
||||
// fixManifestLayers removes repeated layers from the manifest and checks the
|
||||
// correctness of the parent chain.
|
||||
func fixManifestLayers(m *schema1.Manifest) error {
|
||||
imgs := make([]*image.V1Image, len(m.FSLayers))
|
||||
for i := range m.FSLayers {
|
||||
img := &image.V1Image{}
|
||||
|
||||
if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), img); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imgs[i] = img
|
||||
if err := v1.ValidateID(img.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if imgs[len(imgs)-1].Parent != "" && runtime.GOOS != "windows" {
|
||||
// Windows base layer can point to a base layer parent that is not in manifest.
|
||||
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
|
||||
m.FSLayers = append(m.FSLayers[:i], m.FSLayers[i+1:]...)
|
||||
m.History = append(m.History[:i], m.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
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package graph
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/reference"
|
||||
)
|
||||
|
||||
// TestFixManifestLayers checks that fixManifestLayers removes a duplicate
|
||||
|
@ -103,7 +104,10 @@ func TestFixManifestLayersBadParent(t *testing.T) {
|
|||
|
||||
// TestValidateManifest verifies the validateManifest function
|
||||
func TestValidateManifest(t *testing.T) {
|
||||
expectedDigest := "sha256:02fee8c3220ba806531f606525eceb83f4feb654f62b207191b1c9209188dedd"
|
||||
expectedDigest, err := reference.Parse("repo@sha256:02fee8c3220ba806531f606525eceb83f4feb654f62b207191b1c9209188dedd")
|
||||
if err != nil {
|
||||
t.Fatal("could not parse reference")
|
||||
}
|
||||
expectedFSLayer0 := digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
|
||||
|
||||
// Good manifest
|
||||
|
@ -167,29 +171,4 @@ func TestValidateManifest(t *testing.T) {
|
|||
if err == nil || !strings.HasPrefix(err.Error(), "image verification failed for digest") {
|
||||
t.Fatal("expected validateManifest to fail with digest error")
|
||||
}
|
||||
|
||||
// Manifest with no signature
|
||||
|
||||
expectedWholeFileDigest := "7ec3615a120efcdfc270e9c7ea4183330775a3e52a09e2efb194b9a7c18e5ff7"
|
||||
|
||||
noSignatureManifestBytes, err := ioutil.ReadFile("fixtures/validate_manifest/no_signature_manifest")
|
||||
if err != nil {
|
||||
t.Fatal("error reading fixture:", err)
|
||||
}
|
||||
|
||||
var noSignatureSignedManifest schema1.SignedManifest
|
||||
noSignatureSignedManifest.Raw = noSignatureManifestBytes
|
||||
err = json.Unmarshal(noSignatureManifestBytes, &noSignatureSignedManifest.Manifest)
|
||||
if err != nil {
|
||||
t.Fatal("error unmarshaling manifest:", err)
|
||||
}
|
||||
|
||||
verifiedManifest, err = verifyManifest(&noSignatureSignedManifest, expectedWholeFileDigest)
|
||||
if err != nil {
|
||||
t.Fatal("validateManifest failed:", err)
|
||||
}
|
||||
|
||||
if verifiedManifest.FSLayers[0].BlobSum != expectedFSLayer0 {
|
||||
t.Fatal("unexpected FSLayer in no-signature manifest")
|
||||
}
|
||||
}
|
12
distribution/pull_v2_unix.go
Normal file
12
distribution/pull_v2_unix.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
// +build !windows
|
||||
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/docker/image"
|
||||
)
|
||||
|
||||
func detectBaseLayer(is image.Store, m *schema1.Manifest, rootFS *image.RootFS) error {
|
||||
return nil
|
||||
}
|
29
distribution/pull_v2_windows.go
Normal file
29
distribution/pull_v2_windows.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
// +build windows
|
||||
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/docker/image"
|
||||
)
|
||||
|
||||
func detectBaseLayer(is image.Store, m *schema1.Manifest, rootFS *image.RootFS) error {
|
||||
v1img := &image.V1Image{}
|
||||
if err := json.Unmarshal([]byte(m.History[len(m.History)-1].V1Compatibility), v1img); err != nil {
|
||||
return err
|
||||
}
|
||||
if v1img.Parent == "" {
|
||||
return fmt.Errorf("Last layer %q does not have a base layer reference", v1img.ID)
|
||||
}
|
||||
// There must be an image that already references the baselayer.
|
||||
for _, img := range is.Map() {
|
||||
if img.RootFS.BaseLayerID() == v1img.Parent {
|
||||
rootFS.BaseLayer = img.RootFS.BaseLayer
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("Invalid base layer %q", v1img.Parent)
|
||||
}
|
179
distribution/push.go
Normal file
179
distribution/push.go
Normal file
|
@ -0,0 +1,179 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/daemon/events"
|
||||
"github.com/docker/docker/distribution/metadata"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/tag"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
// ImagePushConfig stores push configuration.
|
||||
type ImagePushConfig struct {
|
||||
// MetaHeaders store HTTP headers with metadata about the image
|
||||
// (DockerHeaders with prefix X-Meta- in the request).
|
||||
MetaHeaders map[string][]string
|
||||
// AuthConfig holds authentication credentials for authenticating with
|
||||
// the registry.
|
||||
AuthConfig *cliconfig.AuthConfig
|
||||
// OutStream is the output writer for showing the status of the push
|
||||
// operation.
|
||||
OutStream io.Writer
|
||||
// RegistryService is the registry service to use for TLS configuration
|
||||
// and endpoint lookup.
|
||||
RegistryService *registry.Service
|
||||
// EventsService is the events service to use for logging.
|
||||
EventsService *events.Events
|
||||
// MetadataStore is the storage backend for distribution-specific
|
||||
// metadata.
|
||||
MetadataStore metadata.Store
|
||||
// LayerStore manges layers.
|
||||
LayerStore layer.Store
|
||||
// ImageStore manages images.
|
||||
ImageStore image.Store
|
||||
// TagStore manages tags.
|
||||
TagStore tag.Store
|
||||
// TrustKey is the private key for legacy signatures. This is typically
|
||||
// an ephemeral key, since these signatures are no longer verified.
|
||||
TrustKey libtrust.PrivateKey
|
||||
}
|
||||
|
||||
// Pusher is an interface that abstracts pushing for different API versions.
|
||||
type Pusher interface {
|
||||
// Push tries to push the image configured at the creation of Pusher.
|
||||
// Push returns an error if any, as well as a boolean that determines whether to retry Push on the next configured endpoint.
|
||||
//
|
||||
// TODO(tiborvass): have Push() take a reference to repository + tag, so that the pusher itself is repository-agnostic.
|
||||
Push() (fallback bool, err error)
|
||||
}
|
||||
|
||||
const compressionBufSize = 32768
|
||||
|
||||
// NewPusher creates a new Pusher interface that will push to either a v1 or v2
|
||||
// registry. The endpoint argument contains a Version field that determines
|
||||
// whether a v1 or v2 pusher will be created. The other parameters are passed
|
||||
// through to the underlying pusher implementation for use during the actual
|
||||
// push operation.
|
||||
func NewPusher(ref reference.Named, endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, imagePushConfig *ImagePushConfig, sf *streamformatter.StreamFormatter) (Pusher, error) {
|
||||
switch endpoint.Version {
|
||||
case registry.APIVersion2:
|
||||
return &v2Pusher{
|
||||
blobSumService: metadata.NewBlobSumService(imagePushConfig.MetadataStore),
|
||||
ref: ref,
|
||||
endpoint: endpoint,
|
||||
repoInfo: repoInfo,
|
||||
config: imagePushConfig,
|
||||
sf: sf,
|
||||
layersPushed: make(map[digest.Digest]bool),
|
||||
}, nil
|
||||
case registry.APIVersion1:
|
||||
return &v1Pusher{
|
||||
v1IDService: metadata.NewV1IDService(imagePushConfig.MetadataStore),
|
||||
ref: ref,
|
||||
endpoint: endpoint,
|
||||
repoInfo: repoInfo,
|
||||
config: imagePushConfig,
|
||||
sf: sf,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)
|
||||
}
|
||||
|
||||
// Push initiates a push operation on the repository named localName.
|
||||
// ref is the specific variant of the image to be pushed.
|
||||
// If no tag is provided, all tags will be pushed.
|
||||
func Push(ref reference.Named, imagePushConfig *ImagePushConfig) error {
|
||||
// FIXME: Allow to interrupt current push when new push of same image is done.
|
||||
|
||||
var sf = streamformatter.NewJSONStreamFormatter()
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := imagePushConfig.RegistryService.ResolveRepository(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := imagePushConfig.RegistryService.LookupPushEndpoints(repoInfo.CanonicalName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imagePushConfig.OutStream.Write(sf.FormatStatus("", "The push refers to a repository [%s]", repoInfo.CanonicalName))
|
||||
|
||||
associations := imagePushConfig.TagStore.ReferencesByName(repoInfo.LocalName)
|
||||
if len(associations) == 0 {
|
||||
return fmt.Errorf("Repository does not exist: %s", repoInfo.LocalName)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, endpoint := range endpoints {
|
||||
logrus.Debugf("Trying to push %s to %s %s", repoInfo.CanonicalName, endpoint.URL, endpoint.Version)
|
||||
|
||||
pusher, err := NewPusher(ref, endpoint, repoInfo, imagePushConfig, sf)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if fallback, err := pusher.Push(); err != nil {
|
||||
if fallback {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
logrus.Debugf("Not continuing with error: %v", err)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
imagePushConfig.EventsService.Log("push", repoInfo.LocalName.Name(), "")
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("no endpoints found for %s", repoInfo.CanonicalName)
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// compress returns an io.ReadCloser which will supply a compressed version of
|
||||
// the provided Reader. The caller must close the ReadCloser after reading the
|
||||
// compressed data.
|
||||
//
|
||||
// Note that this function returns a reader instead of taking a writer as an
|
||||
// argument so that it can be used with httpBlobWriter's ReadFrom method.
|
||||
// Using httpBlobWriter's Write method would send a PATCH request for every
|
||||
// Write call.
|
||||
func compress(in io.Reader) io.ReadCloser {
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
// Use a bufio.Writer to avoid excessive chunking in HTTP request.
|
||||
bufWriter := bufio.NewWriterSize(pipeWriter, compressionBufSize)
|
||||
compressor := gzip.NewWriter(bufWriter)
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(compressor, in)
|
||||
if err == nil {
|
||||
err = compressor.Close()
|
||||
}
|
||||
if err == nil {
|
||||
err = bufWriter.Flush()
|
||||
}
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
} else {
|
||||
pipeWriter.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
return pipeReader
|
||||
}
|
466
distribution/push_v1.go
Normal file
466
distribution/push_v1.go
Normal file
|
@ -0,0 +1,466 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/distribution/metadata"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/image/v1"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
)
|
||||
|
||||
type v1Pusher struct {
|
||||
v1IDService *metadata.V1IDService
|
||||
endpoint registry.APIEndpoint
|
||||
ref reference.Named
|
||||
repoInfo *registry.RepositoryInfo
|
||||
config *ImagePushConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
session *registry.Session
|
||||
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func (p *v1Pusher) Push() (fallback bool, err error) {
|
||||
tlsConfig, err := p.config.RegistryService.TLSConfig(p.repoInfo.Index.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Adds Docker-specific headers as well as user-specified headers (metaHeaders)
|
||||
tr := transport.NewTransport(
|
||||
// TODO(tiborvass): was NoTimeout
|
||||
registry.NewTransport(tlsConfig),
|
||||
registry.DockerHeaders(p.config.MetaHeaders)...,
|
||||
)
|
||||
client := registry.HTTPClient(tr)
|
||||
v1Endpoint, err := p.endpoint.ToV1Endpoint(p.config.MetaHeaders)
|
||||
if err != nil {
|
||||
logrus.Debugf("Could not get v1 endpoint: %v", err)
|
||||
return true, err
|
||||
}
|
||||
p.session, err = registry.NewSession(client, p.config.AuthConfig, v1Endpoint)
|
||||
if err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
return true, err
|
||||
}
|
||||
if err := p.pushRepository(); err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// v1Image exposes the configuration, filesystem layer ID, and a v1 ID for an
|
||||
// image being pushed to a v1 registry.
|
||||
type v1Image interface {
|
||||
Config() []byte
|
||||
Layer() layer.Layer
|
||||
V1ID() string
|
||||
}
|
||||
|
||||
type v1ImageCommon struct {
|
||||
layer layer.Layer
|
||||
config []byte
|
||||
v1ID string
|
||||
}
|
||||
|
||||
func (common *v1ImageCommon) Config() []byte {
|
||||
return common.config
|
||||
}
|
||||
|
||||
func (common *v1ImageCommon) V1ID() string {
|
||||
return common.v1ID
|
||||
}
|
||||
|
||||
func (common *v1ImageCommon) Layer() layer.Layer {
|
||||
return common.layer
|
||||
}
|
||||
|
||||
// v1TopImage defines a runnable (top layer) image being pushed to a v1
|
||||
// registry.
|
||||
type v1TopImage struct {
|
||||
v1ImageCommon
|
||||
imageID image.ID
|
||||
}
|
||||
|
||||
func newV1TopImage(imageID image.ID, img *image.Image, l layer.Layer, parent *v1DependencyImage) (*v1TopImage, error) {
|
||||
v1ID := digest.Digest(imageID).Hex()
|
||||
parentV1ID := ""
|
||||
if parent != nil {
|
||||
parentV1ID = parent.V1ID()
|
||||
}
|
||||
|
||||
config, err := v1.MakeV1ConfigFromConfig(img, v1ID, parentV1ID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v1TopImage{
|
||||
v1ImageCommon: v1ImageCommon{
|
||||
v1ID: v1ID,
|
||||
config: config,
|
||||
layer: l,
|
||||
},
|
||||
imageID: imageID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// v1DependencyImage defines a dependency layer being pushed to a v1 registry.
|
||||
type v1DependencyImage struct {
|
||||
v1ImageCommon
|
||||
}
|
||||
|
||||
func newV1DependencyImage(l layer.Layer, parent *v1DependencyImage) (*v1DependencyImage, error) {
|
||||
v1ID := digest.Digest(l.ChainID()).Hex()
|
||||
|
||||
config := ""
|
||||
if parent != nil {
|
||||
config = fmt.Sprintf(`{"id":"%s","parent":"%s"}`, v1ID, parent.V1ID())
|
||||
} else {
|
||||
config = fmt.Sprintf(`{"id":"%s"}`, v1ID)
|
||||
}
|
||||
return &v1DependencyImage{
|
||||
v1ImageCommon: v1ImageCommon{
|
||||
v1ID: v1ID,
|
||||
config: []byte(config),
|
||||
layer: l,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Retrieve the all the images to be uploaded in the correct order
|
||||
func (p *v1Pusher) getImageList() (imageList []v1Image, tagsByImage map[image.ID][]string, referencedLayers []layer.Layer, err error) {
|
||||
tagsByImage = make(map[image.ID][]string)
|
||||
|
||||
// Ignore digest references
|
||||
_, isDigested := p.ref.(reference.Digested)
|
||||
if isDigested {
|
||||
return
|
||||
}
|
||||
|
||||
tagged, isTagged := p.ref.(reference.Tagged)
|
||||
if isTagged {
|
||||
// Push a specific tag
|
||||
var imgID image.ID
|
||||
imgID, err = p.config.TagStore.Get(p.ref)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
imageList, err = p.imageListForTag(imgID, nil, &referencedLayers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tagsByImage[imgID] = []string{tagged.Tag()}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
imagesSeen := make(map[image.ID]struct{})
|
||||
dependenciesSeen := make(map[layer.ChainID]*v1DependencyImage)
|
||||
|
||||
associations := p.config.TagStore.ReferencesByName(p.ref)
|
||||
for _, association := range associations {
|
||||
if tagged, isTagged = association.Ref.(reference.Tagged); !isTagged {
|
||||
// Ignore digest references.
|
||||
continue
|
||||
}
|
||||
|
||||
tagsByImage[association.ImageID] = append(tagsByImage[association.ImageID], tagged.Tag())
|
||||
|
||||
if _, present := imagesSeen[association.ImageID]; present {
|
||||
// Skip generating image list for already-seen image
|
||||
continue
|
||||
}
|
||||
imagesSeen[association.ImageID] = struct{}{}
|
||||
|
||||
imageListForThisTag, err := p.imageListForTag(association.ImageID, dependenciesSeen, &referencedLayers)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// append to main image list
|
||||
imageList = append(imageList, imageListForThisTag...)
|
||||
}
|
||||
if len(imageList) == 0 {
|
||||
return nil, nil, nil, fmt.Errorf("No images found for the requested repository / tag")
|
||||
}
|
||||
logrus.Debugf("Image list: %v", imageList)
|
||||
logrus.Debugf("Tags by image: %v", tagsByImage)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *v1Pusher) imageListForTag(imgID image.ID, dependenciesSeen map[layer.ChainID]*v1DependencyImage, referencedLayers *[]layer.Layer) (imageListForThisTag []v1Image, err error) {
|
||||
img, err := p.config.ImageStore.Get(imgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
topLayerID := img.RootFS.ChainID()
|
||||
|
||||
var l layer.Layer
|
||||
if topLayerID == "" {
|
||||
l = layer.EmptyLayer
|
||||
} else {
|
||||
l, err = p.config.LayerStore.Get(topLayerID)
|
||||
*referencedLayers = append(*referencedLayers, l)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get top layer from image: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
dependencyImages, parent, err := generateDependencyImages(l.Parent(), dependenciesSeen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
topImage, err := newV1TopImage(imgID, img, l, parent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageListForThisTag = append(dependencyImages, topImage)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func generateDependencyImages(l layer.Layer, dependenciesSeen map[layer.ChainID]*v1DependencyImage) (imageListForThisTag []v1Image, parent *v1DependencyImage, err error) {
|
||||
if l == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
imageListForThisTag, parent, err = generateDependencyImages(l.Parent(), dependenciesSeen)
|
||||
|
||||
if dependenciesSeen != nil {
|
||||
if dependencyImage, present := dependenciesSeen[l.ChainID()]; present {
|
||||
// This layer is already on the list, we can ignore it
|
||||
// and all its parents.
|
||||
return imageListForThisTag, dependencyImage, nil
|
||||
}
|
||||
}
|
||||
|
||||
dependencyImage, err := newV1DependencyImage(l, parent)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
imageListForThisTag = append(imageListForThisTag, dependencyImage)
|
||||
|
||||
if dependenciesSeen != nil {
|
||||
dependenciesSeen[l.ChainID()] = dependencyImage
|
||||
}
|
||||
|
||||
return imageListForThisTag, dependencyImage, nil
|
||||
}
|
||||
|
||||
// createImageIndex returns an index of an image's layer IDs and tags.
|
||||
func createImageIndex(images []v1Image, tags map[image.ID][]string) []*registry.ImgData {
|
||||
var imageIndex []*registry.ImgData
|
||||
for _, img := range images {
|
||||
v1ID := img.V1ID()
|
||||
|
||||
if topImage, isTopImage := img.(*v1TopImage); isTopImage {
|
||||
if tags, hasTags := tags[topImage.imageID]; hasTags {
|
||||
// If an image has tags you must add an entry in the image index
|
||||
// for each tag
|
||||
for _, tag := range tags {
|
||||
imageIndex = append(imageIndex, ®istry.ImgData{
|
||||
ID: v1ID,
|
||||
Tag: tag,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If the image does not have a tag it still needs to be sent to the
|
||||
// registry with an empty tag so that it is associated with the repository
|
||||
imageIndex = append(imageIndex, ®istry.ImgData{
|
||||
ID: v1ID,
|
||||
Tag: "",
|
||||
})
|
||||
}
|
||||
return imageIndex
|
||||
}
|
||||
|
||||
// lookupImageOnEndpoint checks the specified endpoint to see if an image exists
|
||||
// and if it is absent then it sends the image id to the channel to be pushed.
|
||||
func (p *v1Pusher) lookupImageOnEndpoint(wg *sync.WaitGroup, endpoint string, images chan v1Image, imagesToPush chan string) {
|
||||
defer wg.Done()
|
||||
for image := range images {
|
||||
v1ID := image.V1ID()
|
||||
if err := p.session.LookupRemoteImage(v1ID, endpoint); err != nil {
|
||||
logrus.Errorf("Error in LookupRemoteImage: %s", err)
|
||||
imagesToPush <- v1ID
|
||||
} else {
|
||||
p.out.Write(p.sf.FormatStatus("", "Image %s already pushed, skipping", stringid.TruncateID(v1ID)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *v1Pusher) pushImageToEndpoint(endpoint string, imageList []v1Image, tags map[image.ID][]string, repo *registry.RepositoryData) error {
|
||||
workerCount := len(imageList)
|
||||
// start a maximum of 5 workers to check if images exist on the specified endpoint.
|
||||
if workerCount > 5 {
|
||||
workerCount = 5
|
||||
}
|
||||
var (
|
||||
wg = &sync.WaitGroup{}
|
||||
imageData = make(chan v1Image, workerCount*2)
|
||||
imagesToPush = make(chan string, workerCount*2)
|
||||
pushes = make(chan map[string]struct{}, 1)
|
||||
)
|
||||
for i := 0; i < workerCount; i++ {
|
||||
wg.Add(1)
|
||||
go p.lookupImageOnEndpoint(wg, endpoint, imageData, imagesToPush)
|
||||
}
|
||||
// start a go routine that consumes the images to push
|
||||
go func() {
|
||||
shouldPush := make(map[string]struct{})
|
||||
for id := range imagesToPush {
|
||||
shouldPush[id] = struct{}{}
|
||||
}
|
||||
pushes <- shouldPush
|
||||
}()
|
||||
for _, v1Image := range imageList {
|
||||
imageData <- v1Image
|
||||
}
|
||||
// close the channel to notify the workers that there will be no more images to check.
|
||||
close(imageData)
|
||||
wg.Wait()
|
||||
close(imagesToPush)
|
||||
// wait for all the images that require pushes to be collected into a consumable map.
|
||||
shouldPush := <-pushes
|
||||
// finish by pushing any images and tags to the endpoint. The order that the images are pushed
|
||||
// is very important that is why we are still iterating over the ordered list of imageIDs.
|
||||
for _, img := range imageList {
|
||||
v1ID := img.V1ID()
|
||||
if _, push := shouldPush[v1ID]; push {
|
||||
if _, err := p.pushImage(img, endpoint); err != nil {
|
||||
// FIXME: Continue on error?
|
||||
return err
|
||||
}
|
||||
}
|
||||
if topImage, isTopImage := img.(*v1TopImage); isTopImage {
|
||||
for _, tag := range tags[topImage.imageID] {
|
||||
p.out.Write(p.sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", stringid.TruncateID(v1ID), endpoint+"repositories/"+p.repoInfo.RemoteName.Name()+"/tags/"+tag))
|
||||
if err := p.session.PushRegistryTag(p.repoInfo.RemoteName, v1ID, tag, endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushRepository pushes layers that do not already exist on the registry.
|
||||
func (p *v1Pusher) pushRepository() error {
|
||||
p.out = ioutils.NewWriteFlusher(p.config.OutStream)
|
||||
imgList, tags, referencedLayers, err := p.getImageList()
|
||||
defer func() {
|
||||
for _, l := range referencedLayers {
|
||||
p.config.LayerStore.Release(l)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.out.Write(p.sf.FormatStatus("", "Sending image list"))
|
||||
|
||||
imageIndex := createImageIndex(imgList, tags)
|
||||
for _, data := range imageIndex {
|
||||
logrus.Debugf("Pushing ID: %s with Tag: %s", data.ID, data.Tag)
|
||||
}
|
||||
|
||||
// Register all the images in a repository with the registry
|
||||
// If an image is not in this list it will not be associated with the repository
|
||||
repoData, err := p.session.PushImageJSONIndex(p.repoInfo.RemoteName, imageIndex, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.out.Write(p.sf.FormatStatus("", "Pushing repository %s", p.repoInfo.CanonicalName))
|
||||
// push the repository to each of the endpoints only if it does not exist.
|
||||
for _, endpoint := range repoData.Endpoints {
|
||||
if err := p.pushImageToEndpoint(endpoint, imgList, tags, repoData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = p.session.PushImageJSONIndex(p.repoInfo.RemoteName, imageIndex, true, repoData.Endpoints)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *v1Pusher) pushImage(v1Image v1Image, ep string) (checksum string, err error) {
|
||||
v1ID := v1Image.V1ID()
|
||||
|
||||
jsonRaw := v1Image.Config()
|
||||
p.out.Write(p.sf.FormatProgress(stringid.TruncateID(v1ID), "Pushing", nil))
|
||||
|
||||
// General rule is to use ID for graph accesses and compatibilityID for
|
||||
// calls to session.registry()
|
||||
imgData := ®istry.ImgData{
|
||||
ID: v1ID,
|
||||
}
|
||||
|
||||
// Send the json
|
||||
if err := p.session.PushImageJSONRegistry(imgData, jsonRaw, ep); err != nil {
|
||||
if err == registry.ErrAlreadyExists {
|
||||
p.out.Write(p.sf.FormatProgress(stringid.TruncateID(v1ID), "Image already pushed, skipping", nil))
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
l := v1Image.Layer()
|
||||
|
||||
arch, err := l.TarStream()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// don't care if this fails; best effort
|
||||
size, _ := l.Size()
|
||||
|
||||
// Send the layer
|
||||
logrus.Debugf("rendered layer for %s of [%d] size", v1ID, size)
|
||||
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: ioutil.NopCloser(arch),
|
||||
Out: p.out,
|
||||
Formatter: p.sf,
|
||||
Size: size,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(v1ID),
|
||||
Action: "Pushing",
|
||||
})
|
||||
|
||||
checksum, checksumPayload, err := p.session.PushImageLayerRegistry(v1ID, reader, ep, jsonRaw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
imgData.Checksum = checksum
|
||||
imgData.ChecksumPayload = checksumPayload
|
||||
// Send the checksum
|
||||
if err := p.session.PushImageChecksumRegistry(imgData, ep); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := p.v1IDService.Set(v1ID, p.repoInfo.Index.Name, l.ChainID()); err != nil {
|
||||
logrus.Warnf("Could not set v1 ID mapping: %v", err)
|
||||
}
|
||||
|
||||
p.out.Write(p.sf.FormatProgress(stringid.TruncateID(v1ID), "Image successfully pushed", nil))
|
||||
return imgData.Checksum, nil
|
||||
}
|
410
distribution/push_v2.go
Normal file
410
distribution/push_v2.go
Normal file
|
@ -0,0 +1,410 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/distribution/metadata"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/image/v1"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/tag"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type v2Pusher struct {
|
||||
blobSumService *metadata.BlobSumService
|
||||
ref reference.Named
|
||||
endpoint registry.APIEndpoint
|
||||
repoInfo *registry.RepositoryInfo
|
||||
config *ImagePushConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
repo distribution.Repository
|
||||
|
||||
// layersPushed is the set of layers known to exist on the remote side.
|
||||
// This avoids redundant queries when pushing multiple tags that
|
||||
// involve the same layers.
|
||||
layersPushed map[digest.Digest]bool
|
||||
}
|
||||
|
||||
func (p *v2Pusher) Push() (fallback bool, err error) {
|
||||
p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "push", "pull")
|
||||
if err != nil {
|
||||
logrus.Debugf("Error getting v2 registry: %v", err)
|
||||
return true, err
|
||||
}
|
||||
|
||||
localName := p.repoInfo.LocalName.Name()
|
||||
|
||||
var associations []tag.Association
|
||||
if _, isTagged := p.ref.(reference.Tagged); isTagged {
|
||||
imageID, err := p.config.TagStore.Get(p.ref)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("tag does not exist: %s", p.ref.String())
|
||||
}
|
||||
|
||||
associations = []tag.Association{
|
||||
{
|
||||
Ref: p.ref,
|
||||
ImageID: imageID,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Pull all tags
|
||||
associations = p.config.TagStore.ReferencesByName(p.ref)
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error getting tags for %s: %s", localName, err)
|
||||
}
|
||||
if len(associations) == 0 {
|
||||
return false, fmt.Errorf("no tags to push for %s", localName)
|
||||
}
|
||||
|
||||
for _, association := range associations {
|
||||
if err := p.pushV2Tag(association); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *v2Pusher) pushV2Tag(association tag.Association) error {
|
||||
ref := association.Ref
|
||||
logrus.Debugf("Pushing repository: %s", ref.String())
|
||||
|
||||
img, err := p.config.ImageStore.Get(association.ImageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not find image from tag %s: %v", ref.String(), err)
|
||||
}
|
||||
|
||||
out := p.config.OutStream
|
||||
|
||||
var l layer.Layer
|
||||
|
||||
topLayerID := img.RootFS.ChainID()
|
||||
if topLayerID == "" {
|
||||
l = layer.EmptyLayer
|
||||
} else {
|
||||
l, err = p.config.LayerStore.Get(topLayerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get top layer from image: %v", err)
|
||||
}
|
||||
defer layer.ReleaseAndLog(p.config.LayerStore, l)
|
||||
}
|
||||
|
||||
fsLayers := make(map[layer.DiffID]schema1.FSLayer)
|
||||
|
||||
// Push empty layer if necessary
|
||||
for _, h := range img.History {
|
||||
if h.EmptyLayer {
|
||||
dgst, err := p.pushLayerIfNecessary(out, layer.EmptyLayer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.layersPushed[dgst] = true
|
||||
fsLayers[layer.EmptyLayer.DiffID()] = schema1.FSLayer{BlobSum: dgst}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(img.RootFS.DiffIDs); i++ {
|
||||
dgst, err := p.pushLayerIfNecessary(out, l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.layersPushed[dgst] = true
|
||||
fsLayers[l.DiffID()] = schema1.FSLayer{BlobSum: dgst}
|
||||
|
||||
l = l.Parent()
|
||||
}
|
||||
|
||||
var tag string
|
||||
if tagged, isTagged := ref.(reference.Tagged); isTagged {
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
m, err := CreateV2Manifest(p.repo.Name(), tag, img, fsLayers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("Signed manifest for %s using daemon's key: %s", ref.String(), p.config.TrustKey.KeyID())
|
||||
signed, err := schema1.Sign(m, p.config.TrustKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestDigest, manifestSize, err := digestFromManifest(signed, p.repo.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if manifestDigest != "" {
|
||||
if tagged, isTagged := ref.(reference.Tagged); isTagged {
|
||||
// NOTE: do not change this format without first changing the trust client
|
||||
// code. This information is used to determine what was pushed and should be signed.
|
||||
out.Write(p.sf.FormatStatus("", "%s: digest: %s size: %d", tagged.Tag(), manifestDigest, manifestSize))
|
||||
}
|
||||
}
|
||||
|
||||
manSvc, err := p.repo.Manifests(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return manSvc.Put(signed)
|
||||
}
|
||||
|
||||
func (p *v2Pusher) pushLayerIfNecessary(out io.Writer, l layer.Layer) (digest.Digest, error) {
|
||||
logrus.Debugf("Pushing layer: %s", l.DiffID())
|
||||
|
||||
// Do we have any blobsums associated with this layer's DiffID?
|
||||
possibleBlobsums, err := p.blobSumService.GetBlobSums(l.DiffID())
|
||||
if err == nil {
|
||||
dgst, exists, err := p.blobSumAlreadyExists(possibleBlobsums)
|
||||
if err != nil {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(string(l.DiffID())), "Image push failed", nil))
|
||||
return "", err
|
||||
}
|
||||
if exists {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(string(l.DiffID())), "Layer already exists", nil))
|
||||
return dgst, nil
|
||||
}
|
||||
}
|
||||
|
||||
// if digest was empty or not saved, or if blob does not exist on the remote repository,
|
||||
// then push the blob.
|
||||
pushDigest, err := p.pushV2Layer(p.repo.Blobs(context.Background()), l)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Cache mapping from this layer's DiffID to the blobsum
|
||||
if err := p.blobSumService.Add(l.DiffID(), pushDigest); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pushDigest, nil
|
||||
}
|
||||
|
||||
// blobSumAlreadyExists checks if the registry already know about any of the
|
||||
// blobsums passed in the "blobsums" slice. If it finds one that the registry
|
||||
// knows about, it returns the known digest and "true".
|
||||
func (p *v2Pusher) blobSumAlreadyExists(blobsums []digest.Digest) (digest.Digest, bool, error) {
|
||||
for _, dgst := range blobsums {
|
||||
if p.layersPushed[dgst] {
|
||||
// it is already known that the push is not needed and
|
||||
// therefore doing a stat is unnecessary
|
||||
return dgst, true, nil
|
||||
}
|
||||
_, err := p.repo.Blobs(context.Background()).Stat(context.Background(), dgst)
|
||||
switch err {
|
||||
case nil:
|
||||
return dgst, true, nil
|
||||
case distribution.ErrBlobUnknown:
|
||||
// nop
|
||||
default:
|
||||
return "", false, err
|
||||
}
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
// CreateV2Manifest creates a V2 manifest from an image config and set of
|
||||
// FSLayer digests.
|
||||
// FIXME: This should be moved to the distribution repo, since it will also
|
||||
// be useful for converting new manifests to the old format.
|
||||
func CreateV2Manifest(name, tag string, img *image.Image, fsLayers map[layer.DiffID]schema1.FSLayer) (*schema1.Manifest, error) {
|
||||
if len(img.History) == 0 {
|
||||
return nil, errors.New("empty history when trying to create V2 manifest")
|
||||
}
|
||||
|
||||
// Generate IDs for each layer
|
||||
// For non-top-level layers, create fake V1Compatibility strings that
|
||||
// fit the format and don't collide with anything else, but don't
|
||||
// result in runnable images on their own.
|
||||
type v1Compatibility struct {
|
||||
ID string `json:"id"`
|
||||
Parent string `json:"parent,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Created time.Time `json:"created"`
|
||||
ContainerConfig struct {
|
||||
Cmd []string
|
||||
} `json:"container_config,omitempty"`
|
||||
ThrowAway bool `json:"throwaway,omitempty"`
|
||||
}
|
||||
|
||||
fsLayerList := make([]schema1.FSLayer, len(img.History))
|
||||
history := make([]schema1.History, len(img.History))
|
||||
|
||||
parent := ""
|
||||
layerCounter := 0
|
||||
for i, h := range img.History {
|
||||
if i == len(img.History)-1 {
|
||||
break
|
||||
}
|
||||
|
||||
var diffID layer.DiffID
|
||||
if h.EmptyLayer {
|
||||
diffID = layer.EmptyLayer.DiffID()
|
||||
} else {
|
||||
if len(img.RootFS.DiffIDs) <= layerCounter {
|
||||
return nil, errors.New("too many non-empty layers in History section")
|
||||
}
|
||||
diffID = img.RootFS.DiffIDs[layerCounter]
|
||||
layerCounter++
|
||||
}
|
||||
|
||||
fsLayer, present := fsLayers[diffID]
|
||||
if !present {
|
||||
return nil, fmt.Errorf("missing layer in CreateV2Manifest: %s", diffID.String())
|
||||
}
|
||||
dgst, err := digest.FromBytes([]byte(fsLayer.BlobSum.Hex() + " " + parent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v1ID := dgst.Hex()
|
||||
|
||||
v1Compatibility := v1Compatibility{
|
||||
ID: v1ID,
|
||||
Parent: parent,
|
||||
Comment: h.Comment,
|
||||
Created: h.Created,
|
||||
}
|
||||
v1Compatibility.ContainerConfig.Cmd = []string{img.History[i].CreatedBy}
|
||||
if h.EmptyLayer {
|
||||
v1Compatibility.ThrowAway = true
|
||||
}
|
||||
jsonBytes, err := json.Marshal(&v1Compatibility)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reversedIndex := len(img.History) - i - 1
|
||||
history[reversedIndex].V1Compatibility = string(jsonBytes)
|
||||
fsLayerList[reversedIndex] = fsLayer
|
||||
|
||||
parent = v1ID
|
||||
}
|
||||
|
||||
latestHistory := img.History[len(img.History)-1]
|
||||
|
||||
var diffID layer.DiffID
|
||||
if latestHistory.EmptyLayer {
|
||||
diffID = layer.EmptyLayer.DiffID()
|
||||
} else {
|
||||
if len(img.RootFS.DiffIDs) <= layerCounter {
|
||||
return nil, errors.New("too many non-empty layers in History section")
|
||||
}
|
||||
diffID = img.RootFS.DiffIDs[layerCounter]
|
||||
}
|
||||
fsLayer, present := fsLayers[diffID]
|
||||
if !present {
|
||||
return nil, fmt.Errorf("missing layer in CreateV2Manifest: %s", diffID.String())
|
||||
}
|
||||
|
||||
dgst, err := digest.FromBytes([]byte(fsLayer.BlobSum.Hex() + " " + parent + " " + string(img.RawJSON())))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fsLayerList[0] = fsLayer
|
||||
|
||||
// Top-level v1compatibility string should be a modified version of the
|
||||
// image config.
|
||||
transformedConfig, err := v1.MakeV1ConfigFromConfig(img, dgst.Hex(), parent, latestHistory.EmptyLayer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
history[0].V1Compatibility = string(transformedConfig)
|
||||
|
||||
// windows-only baselayer setup
|
||||
if err := setupBaseLayer(history, *img.RootFS); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &schema1.Manifest{
|
||||
Versioned: manifest.Versioned{
|
||||
SchemaVersion: 1,
|
||||
},
|
||||
Name: name,
|
||||
Tag: tag,
|
||||
Architecture: img.Architecture,
|
||||
FSLayers: fsLayerList,
|
||||
History: history,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rawJSON(value interface{}) *json.RawMessage {
|
||||
jsonval, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return (*json.RawMessage)(&jsonval)
|
||||
}
|
||||
|
||||
func (p *v2Pusher) pushV2Layer(bs distribution.BlobService, l layer.Layer) (digest.Digest, error) {
|
||||
out := p.config.OutStream
|
||||
displayID := stringid.TruncateID(string(l.DiffID()))
|
||||
|
||||
out.Write(p.sf.FormatProgress(displayID, "Preparing", nil))
|
||||
|
||||
arch, err := l.TarStream()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Send the layer
|
||||
layerUpload, err := bs.Create(context.Background())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer layerUpload.Close()
|
||||
|
||||
// don't care if this fails; best effort
|
||||
size, _ := l.DiffSize()
|
||||
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: ioutil.NopCloser(arch), // we'll take care of close here.
|
||||
Out: out,
|
||||
Formatter: p.sf,
|
||||
Size: size,
|
||||
NewLines: false,
|
||||
ID: displayID,
|
||||
Action: "Pushing",
|
||||
})
|
||||
|
||||
compressedReader := compress(reader)
|
||||
|
||||
digester := digest.Canonical.New()
|
||||
tee := io.TeeReader(compressedReader, digester.Hash())
|
||||
|
||||
out.Write(p.sf.FormatProgress(displayID, "Pushing", nil))
|
||||
nn, err := layerUpload.ReadFrom(tee)
|
||||
compressedReader.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dgst := digester.Digest()
|
||||
if _, err := layerUpload.Commit(context.Background(), distribution.Descriptor{Digest: dgst}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
logrus.Debugf("uploaded layer %s (%s), %d bytes", l.DiffID(), dgst, nn)
|
||||
out.Write(p.sf.FormatProgress(displayID, "Pushed", nil))
|
||||
|
||||
return dgst, nil
|
||||
}
|
176
distribution/push_v2_test.go
Normal file
176
distribution/push_v2_test.go
Normal file
|
@ -0,0 +1,176 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
)
|
||||
|
||||
func TestCreateV2Manifest(t *testing.T) {
|
||||
imgJSON := `{
|
||||
"architecture": "amd64",
|
||||
"config": {
|
||||
"AttachStderr": false,
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"echo hi"
|
||||
],
|
||||
"Domainname": "",
|
||||
"Entrypoint": null,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"derived=true",
|
||||
"asdf=true"
|
||||
],
|
||||
"Hostname": "23304fc829f9",
|
||||
"Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246",
|
||||
"Labels": {},
|
||||
"OnBuild": [],
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Tty": false,
|
||||
"User": "",
|
||||
"Volumes": null,
|
||||
"WorkingDir": ""
|
||||
},
|
||||
"container": "e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001",
|
||||
"container_config": {
|
||||
"AttachStderr": false,
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"Cmd": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]"
|
||||
],
|
||||
"Domainname": "",
|
||||
"Entrypoint": null,
|
||||
"Env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"derived=true",
|
||||
"asdf=true"
|
||||
],
|
||||
"Hostname": "23304fc829f9",
|
||||
"Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246",
|
||||
"Labels": {},
|
||||
"OnBuild": [],
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Tty": false,
|
||||
"User": "",
|
||||
"Volumes": null,
|
||||
"WorkingDir": ""
|
||||
},
|
||||
"created": "2015-11-04T23:06:32.365666163Z",
|
||||
"docker_version": "1.9.0-dev",
|
||||
"history": [
|
||||
{
|
||||
"created": "2015-10-31T22:22:54.690851953Z",
|
||||
"created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"
|
||||
},
|
||||
{
|
||||
"created": "2015-10-31T22:22:55.613815829Z",
|
||||
"created_by": "/bin/sh -c #(nop) CMD [\"sh\"]"
|
||||
},
|
||||
{
|
||||
"created": "2015-11-04T23:06:30.934316144Z",
|
||||
"created_by": "/bin/sh -c #(nop) ENV derived=true",
|
||||
"empty_layer": true
|
||||
},
|
||||
{
|
||||
"created": "2015-11-04T23:06:31.192097572Z",
|
||||
"created_by": "/bin/sh -c #(nop) ENV asdf=true",
|
||||
"empty_layer": true
|
||||
},
|
||||
{
|
||||
"created": "2015-11-04T23:06:32.083868454Z",
|
||||
"created_by": "/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"
|
||||
},
|
||||
{
|
||||
"created": "2015-11-04T23:06:32.365666163Z",
|
||||
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]",
|
||||
"empty_layer": true
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"rootfs": {
|
||||
"diff_ids": [
|
||||
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
|
||||
"sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
|
||||
"sha256:13f53e08df5a220ab6d13c58b2bf83a59cbdc2e04d0a3f041ddf4b0ba4112d49"
|
||||
],
|
||||
"type": "layers"
|
||||
}
|
||||
}`
|
||||
|
||||
// To fill in rawJSON
|
||||
img, err := image.NewFromJSON([]byte(imgJSON))
|
||||
if err != nil {
|
||||
t.Fatalf("json decoding failed: %v", err)
|
||||
}
|
||||
|
||||
fsLayers := map[layer.DiffID]schema1.FSLayer{
|
||||
layer.DiffID("sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1"): {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
||||
layer.DiffID("sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"): {BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
||||
layer.DiffID("sha256:13f53e08df5a220ab6d13c58b2bf83a59cbdc2e04d0a3f041ddf4b0ba4112d49"): {BlobSum: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
||||
}
|
||||
|
||||
manifest, err := CreateV2Manifest("testrepo", "testtag", img, fsLayers)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateV2Manifest returned error: %v", err)
|
||||
}
|
||||
|
||||
if manifest.Versioned.SchemaVersion != 1 {
|
||||
t.Fatal("SchemaVersion != 1")
|
||||
}
|
||||
if manifest.Name != "testrepo" {
|
||||
t.Fatal("incorrect name in manifest")
|
||||
}
|
||||
if manifest.Tag != "testtag" {
|
||||
t.Fatal("incorrect tag in manifest")
|
||||
}
|
||||
if manifest.Architecture != "amd64" {
|
||||
t.Fatal("incorrect arch in manifest")
|
||||
}
|
||||
|
||||
expectedFSLayers := []schema1.FSLayer{
|
||||
{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
||||
{BlobSum: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
||||
{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
||||
{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
||||
{BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")},
|
||||
{BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")},
|
||||
}
|
||||
|
||||
if len(manifest.FSLayers) != len(expectedFSLayers) {
|
||||
t.Fatalf("wrong number of FSLayers: %d", len(manifest.FSLayers))
|
||||
}
|
||||
if !reflect.DeepEqual(manifest.FSLayers, expectedFSLayers) {
|
||||
t.Fatal("wrong FSLayers list")
|
||||
}
|
||||
|
||||
expectedV1Compatibility := []string{
|
||||
`{"architecture":"amd64","config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","echo hi"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"container":"e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001","container_config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"created":"2015-11-04T23:06:32.365666163Z","docker_version":"1.9.0-dev","id":"d728140d3fd23dfcac505954af0b2224b3579b177029eded62916579eb19ac64","os":"linux","parent":"0594e66a9830fa5ba73b66349eb221ea4beb6bac8d2148b90a0f371f8d67bcd5","throwaway":true}`,
|
||||
`{"id":"0594e66a9830fa5ba73b66349eb221ea4beb6bac8d2148b90a0f371f8d67bcd5","parent":"39bc0dbed47060dd8952b048e73744ae471fe50354d2c267d308292c53b83ce1","created":"2015-11-04T23:06:32.083868454Z","container_config":{"Cmd":["/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"]}}`,
|
||||
`{"id":"39bc0dbed47060dd8952b048e73744ae471fe50354d2c267d308292c53b83ce1","parent":"875d7f206c023dc979e1677567a01364074f82b61e220c9b83a4610170490381","created":"2015-11-04T23:06:31.192097572Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV asdf=true"]},"throwaway":true}`,
|
||||
`{"id":"875d7f206c023dc979e1677567a01364074f82b61e220c9b83a4610170490381","parent":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","created":"2015-11-04T23:06:30.934316144Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV derived=true"]},"throwaway":true}`,
|
||||
`{"id":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","parent":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:55.613815829Z","container_config":{"Cmd":["/bin/sh -c #(nop) CMD [\"sh\"]"]}}`,
|
||||
`{"id":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:54.690851953Z","container_config":{"Cmd":["/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"]}}`,
|
||||
}
|
||||
|
||||
if len(manifest.History) != len(expectedV1Compatibility) {
|
||||
t.Fatalf("wrong number of history entries: %d", len(manifest.History))
|
||||
}
|
||||
for i := range expectedV1Compatibility {
|
||||
if manifest.History[i].V1Compatibility != expectedV1Compatibility[i] {
|
||||
t.Fatalf("wrong V1Compatibility %d. expected:\n%s\ngot:\n%s", i, expectedV1Compatibility[i], manifest.History[i].V1Compatibility)
|
||||
}
|
||||
}
|
||||
}
|
12
distribution/push_v2_unix.go
Normal file
12
distribution/push_v2_unix.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
// +build !windows
|
||||
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/docker/image"
|
||||
)
|
||||
|
||||
func setupBaseLayer(history []schema1.History, rootFS image.RootFS) error {
|
||||
return nil
|
||||
}
|
28
distribution/push_v2_windows.go
Normal file
28
distribution/push_v2_windows.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// +build windows
|
||||
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/docker/image"
|
||||
)
|
||||
|
||||
func setupBaseLayer(history []schema1.History, rootFS image.RootFS) error {
|
||||
var v1Config map[string]*json.RawMessage
|
||||
if err := json.Unmarshal([]byte(history[len(history)-1].V1Compatibility), &v1Config); err != nil {
|
||||
return err
|
||||
}
|
||||
baseID, err := json.Marshal(rootFS.BaseLayerID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v1Config["parent"] = (*json.RawMessage)(&baseID)
|
||||
configJSON, err := json.Marshal(v1Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
history[len(history)-1].V1Compatibility = string(configJSON)
|
||||
return nil
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
package graph
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution"
|
||||
|
@ -29,10 +28,10 @@ func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) {
|
|||
return dcs.auth.Username, dcs.auth.Password
|
||||
}
|
||||
|
||||
// newV2Repository returns a repository (v2 only). It creates a HTTP transport
|
||||
// NewV2Repository returns a repository (v2 only). It creates a HTTP transport
|
||||
// providing timeout settings and authentication support, and also verifies the
|
||||
// remote API version.
|
||||
func newV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, metaHeaders http.Header, authConfig *cliconfig.AuthConfig, actions ...string) (distribution.Repository, error) {
|
||||
func NewV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, metaHeaders http.Header, authConfig *cliconfig.AuthConfig, actions ...string) (distribution.Repository, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
repoName := repoInfo.CanonicalName
|
||||
|
@ -59,7 +58,7 @@ func newV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEnd
|
|||
authTransport := transport.NewTransport(base, modifiers...)
|
||||
pingClient := &http.Client{
|
||||
Transport: authTransport,
|
||||
Timeout: 15 * time.Second,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
endpointStr := strings.TrimRight(endpoint.URL, "/") + "/v2/"
|
||||
req, err := http.NewRequest("GET", endpointStr, nil)
|
||||
|
@ -93,12 +92,12 @@ func newV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEnd
|
|||
}
|
||||
|
||||
creds := dumbCredentialStore{auth: authConfig}
|
||||
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
|
||||
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName.Name(), actions...)
|
||||
basicHandler := auth.NewBasicHandler(creds)
|
||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
|
||||
tr := transport.NewTransport(base, modifiers...)
|
||||
|
||||
return client.NewRepository(ctx, repoName, endpoint.URL, tr)
|
||||
return client.NewRepository(ctx, repoName.Name(), endpoint.URL, tr)
|
||||
}
|
||||
|
||||
func digestFromManifest(m *schema1.SignedManifest, localName string) (digest.Digest, int, error) {
|
|
@ -832,15 +832,6 @@ var (
|
|||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
})
|
||||
|
||||
// ErrorCodeRmInit is generated when we try to delete a container
|
||||
// but failed deleting its init filesystem.
|
||||
ErrorCodeRmInit = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
||||
Value: "RMINIT",
|
||||
Message: "Driver %s failed to remove init filesystem %s: %s",
|
||||
Description: "While trying to delete a container, the driver failed to remove the init filesystem",
|
||||
HTTPStatusCode: http.StatusInternalServerError,
|
||||
})
|
||||
|
||||
// ErrorCodeRmFS is generated when we try to delete a container
|
||||
// but failed deleting its filesystem.
|
||||
ErrorCodeRmFS = errcode.Register(errGroup, errcode.ErrorDescriptor{
|
||||
|
|
180
graph/export.go
180
graph/export.go
|
@ -1,180 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/registry"
|
||||
)
|
||||
|
||||
// ImageExport exports list of images to a output stream specified in the
|
||||
// config. The exported images are archived into a tar when written to the
|
||||
// output stream. All images with the given tag and all versions containing the
|
||||
// same tag are exported. names is the set of tags to export, and outStream
|
||||
// is the writer which the images are written to.
|
||||
func (s *TagStore) ImageExport(names []string, outStream io.Writer) error {
|
||||
// get image json
|
||||
tempdir, err := ioutil.TempDir("", "docker-export-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempdir)
|
||||
|
||||
rootRepoMap := map[string]repository{}
|
||||
addKey := func(name string, tag string, id string) {
|
||||
logrus.Debugf("add key [%s:%s]", name, tag)
|
||||
if repo, ok := rootRepoMap[name]; !ok {
|
||||
rootRepoMap[name] = repository{tag: id}
|
||||
} else {
|
||||
repo[tag] = id
|
||||
}
|
||||
}
|
||||
for _, name := range names {
|
||||
name = registry.NormalizeLocalName(name)
|
||||
logrus.Debugf("Serializing %s", name)
|
||||
rootRepo := s.Repositories[name]
|
||||
if rootRepo != nil {
|
||||
// this is a base repo name, like 'busybox'
|
||||
for tag, id := range rootRepo {
|
||||
addKey(name, tag, id)
|
||||
if err := s.exportImage(id, tempdir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
img, err := s.LookupImage(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if img != nil {
|
||||
// This is a named image like 'busybox:latest'
|
||||
repoName, repoTag := parsers.ParseRepositoryTag(name)
|
||||
|
||||
// Skip digests on save
|
||||
if _, err := digest.ParseDigest(repoTag); err == nil {
|
||||
repoTag = ""
|
||||
}
|
||||
|
||||
// check this length, because a lookup of a truncated has will not have a tag
|
||||
// and will not need to be added to this map
|
||||
if len(repoTag) > 0 {
|
||||
addKey(repoName, repoTag, img.ID)
|
||||
}
|
||||
if err := s.exportImage(img.ID, tempdir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
} else {
|
||||
// this must be an ID that didn't get looked up just right?
|
||||
if err := s.exportImage(name, tempdir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
logrus.Debugf("End Serializing %s", name)
|
||||
}
|
||||
// write repositories, if there is something to write
|
||||
if len(rootRepoMap) > 0 {
|
||||
f, err := os.OpenFile(filepath.Join(tempdir, "repositories"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := json.NewEncoder(f).Encode(rootRepoMap); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chtimes(filepath.Join(tempdir, "repositories"), time.Unix(0, 0), time.Unix(0, 0)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
logrus.Debugf("There were no repositories to write")
|
||||
}
|
||||
|
||||
fs, err := archive.Tar(tempdir, archive.Uncompressed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
if _, err := io.Copy(outStream, fs); err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("End export image")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TagStore) exportImage(name, tempdir string) error {
|
||||
for n := name; n != ""; {
|
||||
img, err := s.LookupImage(n)
|
||||
if err != nil || img == nil {
|
||||
return fmt.Errorf("No such image %s", n)
|
||||
}
|
||||
|
||||
// temporary directory
|
||||
tmpImageDir := filepath.Join(tempdir, n)
|
||||
if err := os.Mkdir(tmpImageDir, os.FileMode(0755)); err != nil {
|
||||
if os.IsExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var version = "1.0"
|
||||
var versionBuf = []byte(version)
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpImageDir, "VERSION"), versionBuf, os.FileMode(0644)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageInspectRaw, err := json.Marshal(img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// serialize json
|
||||
json, err := os.Create(filepath.Join(tmpImageDir, "json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
written, err := json.Write(imageInspectRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if written != len(imageInspectRaw) {
|
||||
logrus.Warnf("%d byes should have been written instead %d have been written", written, len(imageInspectRaw))
|
||||
}
|
||||
|
||||
// serialize filesystem
|
||||
fsTar, err := os.Create(filepath.Join(tmpImageDir, "layer.tar"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.imageTarLayer(n, fsTar); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, fname := range []string{"", "VERSION", "json", "layer.tar"} {
|
||||
if err := os.Chtimes(filepath.Join(tmpImageDir, fname), img.Created, img.Created); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// try again with parent
|
||||
n = img.Parent
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"name": "library/hello-world",
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
{
|
||||
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:03f4658f8b782e12230c1783426bd3bacce651ce582a4ffb6fbbfa2079428ecb"
|
||||
}
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"af340544ed62de0680f441c71fa1a80cb084678fed42bae393e543faea3a572c\",\"parent\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.608577814Z\",\"container\":\"c2b715156f640c7ac7d98472ea24335aba5432a1323a3bb722697e6d37ef794f\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [\\\"/hello\\\"]\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/hello\"],\"Image\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"535020c3e8add9d6bb06e5ac15a261e73d9b213d62fb2c14d752b8e189b2b912\",\"created\":\"2015-08-06T23:53:22.241352727Z\",\"container\":\"9aeb0006ffa72a8287564caaea87625896853701459261d3b569e320c0c9d5dc\",\"container_config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) COPY file:4abd3bff60458ca3b079d7b131ce26b2719055a030dfa96ff827da2b7c7038a7 in /\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.7.1\",\"config\":{\"Hostname\":\"9aeb0006ffa7\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":960}\n"
|
||||
}
|
||||
]
|
||||
}
|
813
graph/graph.go
813
graph/graph.go
|
@ -1,813 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/daemon/graphdriver"
|
||||
"github.com/docker/docker/dockerversion"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/docker/docker/pkg/locker"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/truncindex"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/vbatts/tar-split/tar/asm"
|
||||
"github.com/vbatts/tar-split/tar/storage"
|
||||
)
|
||||
|
||||
// v1Descriptor is a non-content-addressable image descriptor
|
||||
type v1Descriptor struct {
|
||||
img *image.Image
|
||||
}
|
||||
|
||||
// ID returns the image ID specified in the image structure.
|
||||
func (img v1Descriptor) ID() string {
|
||||
return img.img.ID
|
||||
}
|
||||
|
||||
// Parent returns the parent ID specified in the image structure.
|
||||
func (img v1Descriptor) Parent() string {
|
||||
return img.img.Parent
|
||||
}
|
||||
|
||||
// MarshalConfig renders the image structure into JSON.
|
||||
func (img v1Descriptor) MarshalConfig() ([]byte, error) {
|
||||
return json.Marshal(img.img)
|
||||
}
|
||||
|
||||
// The type is used to protect pulling or building related image
|
||||
// layers from deleteing when filtered by dangling=true
|
||||
// The key of layers is the images ID which is pulling or building
|
||||
// The value of layers is a slice which hold layer IDs referenced to
|
||||
// pulling or building images
|
||||
type retainedLayers struct {
|
||||
layerHolders map[string]map[string]struct{} // map[layerID]map[sessionID]
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (r *retainedLayers) Add(sessionID string, layerIDs []string) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
for _, layerID := range layerIDs {
|
||||
if r.layerHolders[layerID] == nil {
|
||||
r.layerHolders[layerID] = map[string]struct{}{}
|
||||
}
|
||||
r.layerHolders[layerID][sessionID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *retainedLayers) Delete(sessionID string, layerIDs []string) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
for _, layerID := range layerIDs {
|
||||
holders, ok := r.layerHolders[layerID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
delete(holders, sessionID)
|
||||
if len(holders) == 0 {
|
||||
delete(r.layerHolders, layerID) // Delete any empty reference set.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *retainedLayers) Exists(layerID string) bool {
|
||||
r.Lock()
|
||||
_, exists := r.layerHolders[layerID]
|
||||
r.Unlock()
|
||||
return exists
|
||||
}
|
||||
|
||||
// A Graph is a store for versioned filesystem images and the relationship between them.
|
||||
type Graph struct {
|
||||
root string
|
||||
idIndex *truncindex.TruncIndex
|
||||
driver graphdriver.Driver
|
||||
imagesMutex sync.Mutex
|
||||
imageMutex locker.Locker // protect images in driver.
|
||||
retained *retainedLayers
|
||||
tarSplitDisabled bool
|
||||
uidMaps []idtools.IDMap
|
||||
gidMaps []idtools.IDMap
|
||||
|
||||
// access to parentRefs must be protected with imageMutex locking the image id
|
||||
// on the key of the map (e.g. imageMutex.Lock(img.ID), parentRefs[img.ID]...)
|
||||
parentRefs map[string]int
|
||||
}
|
||||
|
||||
// file names for ./graph/<ID>/
|
||||
const (
|
||||
jsonFileName = "json"
|
||||
layersizeFileName = "layersize"
|
||||
digestFileName = "checksum"
|
||||
tarDataFileName = "tar-data.json.gz"
|
||||
v1CompatibilityFileName = "v1Compatibility"
|
||||
parentFileName = "parent"
|
||||
)
|
||||
|
||||
var (
|
||||
// errDigestNotSet is used when request the digest for a layer
|
||||
// but the layer has no digest value or content to compute the
|
||||
// the digest.
|
||||
errDigestNotSet = errors.New("digest is not set for layer")
|
||||
)
|
||||
|
||||
// NewGraph instantiates a new graph at the given root path in the filesystem.
|
||||
// `root` will be created if it doesn't exist.
|
||||
func NewGraph(root string, driver graphdriver.Driver, uidMaps, gidMaps []idtools.IDMap) (*Graph, error) {
|
||||
abspath, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootUID, rootGID, err := idtools.GetRootUIDGID(uidMaps, gidMaps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create the root directory if it doesn't exists
|
||||
if err := idtools.MkdirAllAs(root, 0700, rootUID, rootGID); err != nil && !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
graph := &Graph{
|
||||
root: abspath,
|
||||
idIndex: truncindex.NewTruncIndex([]string{}),
|
||||
driver: driver,
|
||||
retained: &retainedLayers{layerHolders: make(map[string]map[string]struct{})},
|
||||
uidMaps: uidMaps,
|
||||
gidMaps: gidMaps,
|
||||
parentRefs: make(map[string]int),
|
||||
}
|
||||
|
||||
// Windows does not currently support tarsplit functionality.
|
||||
if runtime.GOOS == "windows" {
|
||||
graph.tarSplitDisabled = true
|
||||
}
|
||||
|
||||
if err := graph.restore(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return graph, nil
|
||||
}
|
||||
|
||||
// IsHeld returns whether the given layerID is being used by an ongoing pull or build.
|
||||
func (graph *Graph) IsHeld(layerID string) bool {
|
||||
return graph.retained.Exists(layerID)
|
||||
}
|
||||
|
||||
func (graph *Graph) restore() error {
|
||||
dir, err := ioutil.ReadDir(graph.root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var ids = []string{}
|
||||
for _, v := range dir {
|
||||
id := v.Name()
|
||||
if graph.driver.Exists(id) {
|
||||
img, err := graph.loadImage(id)
|
||||
if err != nil {
|
||||
logrus.Warnf("ignoring image %s, it could not be restored: %v", id, err)
|
||||
continue
|
||||
}
|
||||
graph.imageMutex.Lock(img.Parent)
|
||||
graph.parentRefs[img.Parent]++
|
||||
graph.imageMutex.Unlock(img.Parent)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
|
||||
graph.idIndex = truncindex.NewTruncIndex(ids)
|
||||
logrus.Debugf("Restored %d elements", len(ids))
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsNotExist detects whether an image exists by parsing the incoming error
|
||||
// message.
|
||||
func (graph *Graph) IsNotExist(err error, id string) bool {
|
||||
// FIXME: Implement error subclass instead of looking at the error text
|
||||
// Note: This is the way golang implements os.IsNotExists on Plan9
|
||||
return err != nil && (strings.Contains(strings.ToLower(err.Error()), "does not exist") || strings.Contains(strings.ToLower(err.Error()), "no such")) && strings.Contains(err.Error(), id)
|
||||
}
|
||||
|
||||
// Exists returns true if an image is registered at the given id.
|
||||
// If the image doesn't exist or if an error is encountered, false is returned.
|
||||
func (graph *Graph) Exists(id string) bool {
|
||||
if _, err := graph.Get(id); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Get returns the image with the given id, or an error if the image doesn't exist.
|
||||
func (graph *Graph) Get(name string) (*image.Image, error) {
|
||||
id, err := graph.idIndex.Get(name)
|
||||
if err != nil {
|
||||
if err == truncindex.ErrNotExist {
|
||||
return nil, fmt.Errorf("image %s does not exist", name)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
img, err := graph.loadImage(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if img.ID != id {
|
||||
return nil, fmt.Errorf("Image stored at '%s' has wrong id '%s'", id, img.ID)
|
||||
}
|
||||
|
||||
if img.Size < 0 {
|
||||
size, err := graph.driver.DiffSize(img.ID, img.Parent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to calculate size of image id %q: %s", img.ID, err)
|
||||
}
|
||||
|
||||
img.Size = size
|
||||
if err := graph.saveSize(graph.imageRoot(id), img.Size); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// Create creates a new image and registers it in the graph.
|
||||
func (graph *Graph) Create(layerData io.Reader, containerID, containerImage, comment, author string, containerConfig, config *runconfig.Config) (*image.Image, error) {
|
||||
img := &image.Image{
|
||||
ID: stringid.GenerateRandomID(),
|
||||
Comment: comment,
|
||||
Created: time.Now().UTC(),
|
||||
DockerVersion: dockerversion.Version,
|
||||
Author: author,
|
||||
Config: config,
|
||||
Architecture: runtime.GOARCH,
|
||||
OS: runtime.GOOS,
|
||||
}
|
||||
|
||||
if containerID != "" {
|
||||
img.Parent = containerImage
|
||||
img.Container = containerID
|
||||
img.ContainerConfig = *containerConfig
|
||||
}
|
||||
|
||||
if err := graph.Register(v1Descriptor{img}, layerData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// Register imports a pre-existing image into the graph.
|
||||
// Returns nil if the image is already registered.
|
||||
func (graph *Graph) Register(im image.Descriptor, layerData io.Reader) (err error) {
|
||||
imgID := im.ID()
|
||||
|
||||
if err := image.ValidateID(imgID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// this is needed cause pull_v2 attemptIDReuse could deadlock
|
||||
graph.imagesMutex.Lock()
|
||||
defer graph.imagesMutex.Unlock()
|
||||
|
||||
// We need this entire operation to be atomic within the engine. Note that
|
||||
// this doesn't mean Register is fully safe yet.
|
||||
graph.imageMutex.Lock(imgID)
|
||||
defer graph.imageMutex.Unlock(imgID)
|
||||
|
||||
return graph.register(im, layerData)
|
||||
}
|
||||
|
||||
func (graph *Graph) register(im image.Descriptor, layerData io.Reader) (err error) {
|
||||
imgID := im.ID()
|
||||
|
||||
// Skip register if image is already registered
|
||||
if graph.Exists(imgID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The returned `error` must be named in this function's signature so that
|
||||
// `err` is not shadowed in this deferred cleanup.
|
||||
defer func() {
|
||||
// If any error occurs, remove the new dir from the driver.
|
||||
// Don't check for errors since the dir might not have been created.
|
||||
if err != nil {
|
||||
graph.driver.Remove(imgID)
|
||||
}
|
||||
}()
|
||||
|
||||
// Ensure that the image root does not exist on the filesystem
|
||||
// when it is not registered in the graph.
|
||||
// This is common when you switch from one graph driver to another
|
||||
if err := os.RemoveAll(graph.imageRoot(imgID)); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the driver has this ID but the graph doesn't, remove it from the driver to start fresh.
|
||||
// (the graph is the source of truth).
|
||||
// Ignore errors, since we don't know if the driver correctly returns ErrNotExist.
|
||||
// (FIXME: make that mandatory for drivers).
|
||||
graph.driver.Remove(imgID)
|
||||
|
||||
tmp, err := graph.mktemp()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
parent := im.Parent()
|
||||
|
||||
// Create root filesystem in the driver
|
||||
if err := createRootFilesystemInDriver(graph, imgID, parent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply the diff/layer
|
||||
config, err := im.MarshalConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := graph.storeImage(imgID, parent, config, layerData, tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
// Commit
|
||||
if err := os.Rename(tmp, graph.imageRoot(imgID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
graph.idIndex.Add(imgID)
|
||||
|
||||
graph.imageMutex.Lock(parent)
|
||||
graph.parentRefs[parent]++
|
||||
graph.imageMutex.Unlock(parent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createRootFilesystemInDriver(graph *Graph, id, parent string) error {
|
||||
if err := graph.driver.Create(id, parent, ""); err != nil {
|
||||
return fmt.Errorf("Driver %s failed to create image rootfs %s: %s", graph.driver, id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TempLayerArchive creates a temporary archive of the given image's filesystem layer.
|
||||
// The archive is stored on disk and will be automatically deleted as soon as has been read.
|
||||
// If output is not nil, a human-readable progress bar will be written to it.
|
||||
func (graph *Graph) tempLayerArchive(id string, sf *streamformatter.StreamFormatter, output io.Writer) (*archive.TempArchive, error) {
|
||||
image, err := graph.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmp, err := graph.mktemp()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
a, err := graph.tarLayer(image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
progressReader := progressreader.New(progressreader.Config{
|
||||
In: a,
|
||||
Out: output,
|
||||
Formatter: sf,
|
||||
Size: 0,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(id),
|
||||
Action: "Buffering to disk",
|
||||
})
|
||||
defer progressReader.Close()
|
||||
return archive.NewTempArchive(progressReader, tmp)
|
||||
}
|
||||
|
||||
// mktemp creates a temporary sub-directory inside the graph's filesystem.
|
||||
func (graph *Graph) mktemp() (string, error) {
|
||||
dir := filepath.Join(graph.root, "_tmp", stringid.GenerateNonCryptoID())
|
||||
rootUID, rootGID, err := idtools.GetRootUIDGID(graph.uidMaps, graph.gidMaps)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := idtools.MkdirAllAs(dir, 0700, rootUID, rootGID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// Delete atomically removes an image from the graph.
|
||||
func (graph *Graph) Delete(name string) error {
|
||||
id, err := graph.idIndex.Get(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
img, err := graph.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
graph.idIndex.Delete(id)
|
||||
tmp, err := graph.mktemp()
|
||||
if err != nil {
|
||||
tmp = graph.imageRoot(id)
|
||||
} else {
|
||||
if err := os.Rename(graph.imageRoot(id), tmp); err != nil {
|
||||
// On err make tmp point to old dir and cleanup unused tmp dir
|
||||
os.RemoveAll(tmp)
|
||||
tmp = graph.imageRoot(id)
|
||||
}
|
||||
}
|
||||
// Remove rootfs data from the driver
|
||||
graph.driver.Remove(id)
|
||||
|
||||
graph.imageMutex.Lock(img.Parent)
|
||||
graph.parentRefs[img.Parent]--
|
||||
if graph.parentRefs[img.Parent] == 0 {
|
||||
delete(graph.parentRefs, img.Parent)
|
||||
}
|
||||
graph.imageMutex.Unlock(img.Parent)
|
||||
|
||||
// Remove the trashed image directory
|
||||
return os.RemoveAll(tmp)
|
||||
}
|
||||
|
||||
// Map returns a list of all images in the graph, addressable by ID.
|
||||
func (graph *Graph) Map() map[string]*image.Image {
|
||||
images := make(map[string]*image.Image)
|
||||
graph.walkAll(func(image *image.Image) {
|
||||
images[image.ID] = image
|
||||
})
|
||||
return images
|
||||
}
|
||||
|
||||
// walkAll iterates over each image in the graph, and passes it to a handler.
|
||||
// The walking order is undetermined.
|
||||
func (graph *Graph) walkAll(handler func(*image.Image)) {
|
||||
graph.idIndex.Iterate(func(id string) {
|
||||
img, err := graph.Get(id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if handler != nil {
|
||||
handler(img)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ByParent returns a lookup table of images by their parent.
|
||||
// If an image of key ID has 3 children images, then the value for key ID
|
||||
// will be a list of 3 images.
|
||||
// If an image has no children, it will not have an entry in the table.
|
||||
func (graph *Graph) ByParent() map[string][]*image.Image {
|
||||
byParent := make(map[string][]*image.Image)
|
||||
graph.walkAll(func(img *image.Image) {
|
||||
parent, err := graph.Get(img.Parent)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
byParent[parent.ID] = append(byParent[parent.ID], img)
|
||||
})
|
||||
return byParent
|
||||
}
|
||||
|
||||
// HasChildren returns whether the given image has any child images.
|
||||
func (graph *Graph) HasChildren(imgID string) bool {
|
||||
graph.imageMutex.Lock(imgID)
|
||||
count := graph.parentRefs[imgID]
|
||||
graph.imageMutex.Unlock(imgID)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// Retain keeps the images and layers that are in the pulling chain so that
|
||||
// they are not deleted. If not retained, they may be deleted by rmi.
|
||||
func (graph *Graph) Retain(sessionID string, layerIDs ...string) {
|
||||
graph.retained.Add(sessionID, layerIDs)
|
||||
}
|
||||
|
||||
// Release removes the referenced image ID from the provided set of layers.
|
||||
func (graph *Graph) Release(sessionID string, layerIDs ...string) {
|
||||
graph.retained.Delete(sessionID, layerIDs)
|
||||
}
|
||||
|
||||
// heads returns all heads in the graph, keyed by id.
|
||||
// A head is an image which is not the parent of another image in the graph.
|
||||
func (graph *Graph) heads() map[string]*image.Image {
|
||||
heads := make(map[string]*image.Image)
|
||||
graph.walkAll(func(image *image.Image) {
|
||||
// if it has no children, then it's not a parent, so it's an head
|
||||
if !graph.HasChildren(image.ID) {
|
||||
heads[image.ID] = image
|
||||
}
|
||||
})
|
||||
return heads
|
||||
}
|
||||
|
||||
// tarLayer returns a tar archive of the image's filesystem layer.
|
||||
func (graph *Graph) tarLayer(img *image.Image) (arch io.ReadCloser, err error) {
|
||||
rdr, err := graph.assembleTarLayer(img)
|
||||
if err != nil {
|
||||
logrus.Debugf("[graph] tarLayer with traditional differ: %s", img.ID)
|
||||
return graph.driver.Diff(img.ID, img.Parent)
|
||||
}
|
||||
return rdr, nil
|
||||
}
|
||||
|
||||
func (graph *Graph) imageRoot(id string) string {
|
||||
return filepath.Join(graph.root, id)
|
||||
}
|
||||
|
||||
// loadImage fetches the image with the given id from the graph.
|
||||
func (graph *Graph) loadImage(id string) (*image.Image, error) {
|
||||
root := graph.imageRoot(id)
|
||||
|
||||
// Open the JSON file to decode by streaming
|
||||
jsonSource, err := os.Open(jsonPath(root))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer jsonSource.Close()
|
||||
|
||||
img := &image.Image{}
|
||||
dec := json.NewDecoder(jsonSource)
|
||||
|
||||
// Decode the JSON data
|
||||
if err := dec.Decode(img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if img.ID == "" {
|
||||
img.ID = id
|
||||
}
|
||||
|
||||
if img.Parent == "" && img.ParentID != "" && img.ParentID.Validate() == nil {
|
||||
img.Parent = img.ParentID.Hex()
|
||||
}
|
||||
|
||||
// compatibilityID for parent
|
||||
parent, err := ioutil.ReadFile(filepath.Join(root, parentFileName))
|
||||
if err == nil && len(parent) > 0 {
|
||||
img.Parent = string(parent)
|
||||
}
|
||||
|
||||
if err := image.ValidateID(img.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if buf, err := ioutil.ReadFile(filepath.Join(root, layersizeFileName)); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
// If the layersize file does not exist then set the size to a negative number
|
||||
// because a layer size of 0 (zero) is valid
|
||||
img.Size = -1
|
||||
} else {
|
||||
// Using Atoi here instead would temporarily convert the size to a machine
|
||||
// dependent integer type, which causes images larger than 2^31 bytes to
|
||||
// display negative sizes on 32-bit machines:
|
||||
size, err := strconv.ParseInt(string(buf), 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img.Size = int64(size)
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// saveSize stores the `size` in the provided graph `img` directory `root`.
|
||||
func (graph *Graph) saveSize(root string, size int64) error {
|
||||
if err := ioutil.WriteFile(filepath.Join(root, layersizeFileName), []byte(strconv.FormatInt(size, 10)), 0600); err != nil {
|
||||
return fmt.Errorf("Error storing image size in %s/%s: %s", root, layersizeFileName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setLayerDigestWithLock sets the digest for the image layer to the provided value.
|
||||
func (graph *Graph) setLayerDigestWithLock(id string, dgst digest.Digest) error {
|
||||
graph.imageMutex.Lock(id)
|
||||
defer graph.imageMutex.Unlock(id)
|
||||
|
||||
return graph.setLayerDigest(id, dgst)
|
||||
}
|
||||
func (graph *Graph) setLayerDigest(id string, dgst digest.Digest) error {
|
||||
root := graph.imageRoot(id)
|
||||
if err := ioutil.WriteFile(filepath.Join(root, digestFileName), []byte(dgst.String()), 0600); err != nil {
|
||||
return fmt.Errorf("Error storing digest in %s/%s: %s", root, digestFileName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLayerDigestWithLock gets the digest for the provide image layer id.
|
||||
func (graph *Graph) getLayerDigestWithLock(id string) (digest.Digest, error) {
|
||||
graph.imageMutex.Lock(id)
|
||||
defer graph.imageMutex.Unlock(id)
|
||||
|
||||
return graph.getLayerDigest(id)
|
||||
}
|
||||
|
||||
func (graph *Graph) getLayerDigest(id string) (digest.Digest, error) {
|
||||
root := graph.imageRoot(id)
|
||||
cs, err := ioutil.ReadFile(filepath.Join(root, digestFileName))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", errDigestNotSet
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return digest.ParseDigest(string(cs))
|
||||
}
|
||||
|
||||
// setV1CompatibilityConfig stores the v1Compatibility JSON data associated
|
||||
// with the image in the manifest to the disk
|
||||
func (graph *Graph) setV1CompatibilityConfig(id string, data []byte) error {
|
||||
root := graph.imageRoot(id)
|
||||
return ioutil.WriteFile(filepath.Join(root, v1CompatibilityFileName), data, 0600)
|
||||
}
|
||||
|
||||
// getV1CompatibilityConfig reads the v1Compatibility JSON data for the image
|
||||
// from the disk
|
||||
func (graph *Graph) getV1CompatibilityConfig(id string) ([]byte, error) {
|
||||
root := graph.imageRoot(id)
|
||||
return ioutil.ReadFile(filepath.Join(root, v1CompatibilityFileName))
|
||||
}
|
||||
|
||||
// generateV1CompatibilityChain makes sure v1Compatibility JSON data exists
|
||||
// for the image. If it doesn't it generates and stores it for the image and
|
||||
// all of it's parents based on the image config JSON.
|
||||
func (graph *Graph) generateV1CompatibilityChain(id string) ([]byte, error) {
|
||||
graph.imageMutex.Lock(id)
|
||||
defer graph.imageMutex.Unlock(id)
|
||||
|
||||
if v1config, err := graph.getV1CompatibilityConfig(id); err == nil {
|
||||
return v1config, nil
|
||||
}
|
||||
|
||||
// generate new, store it to disk
|
||||
img, err := graph.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
digestPrefix := string(digest.Canonical) + ":"
|
||||
img.ID = strings.TrimPrefix(img.ID, digestPrefix)
|
||||
|
||||
if img.Parent != "" {
|
||||
parentConfig, err := graph.generateV1CompatibilityChain(img.Parent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var parent struct{ ID string }
|
||||
err = json.Unmarshal(parentConfig, &parent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img.Parent = parent.ID
|
||||
}
|
||||
|
||||
json, err := json.Marshal(img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := graph.setV1CompatibilityConfig(id, json); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json, nil
|
||||
}
|
||||
|
||||
func jsonPath(root string) string {
|
||||
return filepath.Join(root, jsonFileName)
|
||||
}
|
||||
|
||||
// storeImage stores file system layer data for the given image to the
|
||||
// graph's storage driver. Image metadata is stored in a file
|
||||
// at the specified root directory.
|
||||
func (graph *Graph) storeImage(id, parent string, config []byte, layerData io.Reader, root string) (err error) {
|
||||
var size int64
|
||||
// Store the layer. If layerData is not nil, unpack it into the new layer
|
||||
if layerData != nil {
|
||||
if size, err = graph.disassembleAndApplyTarLayer(id, parent, layerData, root); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := graph.saveSize(root, size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(jsonPath(root), config, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If image is pointing to a parent via CompatibilityID write the reference to disk
|
||||
img, err := image.NewImgJSON(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (img.ParentID.Validate() == nil && parent != img.ParentID.Hex()) || (allowBaseParentImage && img.ParentID == "" && parent != "") {
|
||||
// save compatibilityID parent if it doesn't match parentID
|
||||
// on windows always save a parent file pointing to the base layer
|
||||
if err := ioutil.WriteFile(filepath.Join(root, parentFileName), []byte(parent), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (graph *Graph) disassembleAndApplyTarLayer(id, parent string, layerData io.Reader, root string) (size int64, err error) {
|
||||
var ar io.Reader
|
||||
|
||||
if graph.tarSplitDisabled {
|
||||
ar = layerData
|
||||
} else {
|
||||
// this is saving the tar-split metadata
|
||||
mf, err := os.OpenFile(filepath.Join(root, tarDataFileName), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(0600))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
mfz := gzip.NewWriter(mf)
|
||||
metaPacker := storage.NewJSONPacker(mfz)
|
||||
defer mf.Close()
|
||||
defer mfz.Close()
|
||||
|
||||
inflatedLayerData, err := archive.DecompressStream(layerData)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// we're passing nil here for the file putter, because the ApplyDiff will
|
||||
// handle the extraction of the archive
|
||||
rdr, err := asm.NewInputTarStream(inflatedLayerData, metaPacker, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
ar = archive.Reader(rdr)
|
||||
}
|
||||
|
||||
if size, err = graph.driver.ApplyDiff(id, parent, ar); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (graph *Graph) assembleTarLayer(img *image.Image) (io.ReadCloser, error) {
|
||||
root := graph.imageRoot(img.ID)
|
||||
mFileName := filepath.Join(root, tarDataFileName)
|
||||
mf, err := os.Open(mFileName)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logrus.Errorf("failed to open %q: %s", mFileName, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
pR, pW := io.Pipe()
|
||||
// this will need to be in a goroutine, as we are returning the stream of a
|
||||
// tar archive, but can not close the metadata reader early (when this
|
||||
// function returns)...
|
||||
go func() {
|
||||
defer mf.Close()
|
||||
// let's reassemble!
|
||||
logrus.Debugf("[graph] TarLayer with reassembly: %s", img.ID)
|
||||
mfz, err := gzip.NewReader(mf)
|
||||
if err != nil {
|
||||
pW.CloseWithError(fmt.Errorf("[graph] error with %s: %s", mFileName, err))
|
||||
return
|
||||
}
|
||||
defer mfz.Close()
|
||||
|
||||
// get our relative path to the container
|
||||
fsLayer, err := graph.driver.Get(img.ID, "")
|
||||
if err != nil {
|
||||
pW.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
defer graph.driver.Put(img.ID)
|
||||
|
||||
metaUnpacker := storage.NewJSONUnpacker(mfz)
|
||||
fileGetter := storage.NewPathFileGetter(fsLayer)
|
||||
logrus.Debugf("[graph] %s is at %q", img.ID, fsLayer)
|
||||
ots := asm.NewOutputTarStream(fileGetter, metaUnpacker)
|
||||
defer ots.Close()
|
||||
if _, err := io.Copy(pW, ots); err != nil {
|
||||
pW.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
pW.Close()
|
||||
}()
|
||||
return pR, nil
|
||||
}
|
|
@ -1,308 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/daemon/graphdriver"
|
||||
"github.com/docker/docker/dockerversion"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
)
|
||||
|
||||
func TestMount(t *testing.T) {
|
||||
graph, driver := tempGraph(t)
|
||||
defer os.RemoveAll(graph.root)
|
||||
defer driver.Cleanup()
|
||||
|
||||
archive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
image, err := graph.Create(archive, "", "", "Testing", "", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmp, err := ioutil.TempDir("", "docker-test-graph-mount-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
rootfs := path.Join(tmp, "rootfs")
|
||||
if err := os.MkdirAll(rootfs, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rw := path.Join(tmp, "rw")
|
||||
if err := os.MkdirAll(rw, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := driver.Get(image.ID, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
graph, _ := tempGraph(t)
|
||||
defer nukeGraph(graph)
|
||||
// Root should exist
|
||||
if _, err := os.Stat(graph.root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Map() should be empty
|
||||
l := graph.Map()
|
||||
if len(l) != 0 {
|
||||
t.Fatalf("len(Map()) should return %d, not %d", 0, len(l))
|
||||
}
|
||||
}
|
||||
|
||||
// Test that Register can be interrupted cleanly without side effects
|
||||
func TestInterruptedRegister(t *testing.T) {
|
||||
graph, _ := tempGraph(t)
|
||||
defer nukeGraph(graph)
|
||||
badArchive, w := io.Pipe() // Use a pipe reader as a fake archive which never yields data
|
||||
image := &image.Image{
|
||||
ID: stringid.GenerateNonCryptoID(),
|
||||
Comment: "testing",
|
||||
Created: time.Now(),
|
||||
}
|
||||
w.CloseWithError(errors.New("But I'm not a tarball!")) // (Nobody's perfect, darling)
|
||||
graph.Register(v1Descriptor{image}, badArchive)
|
||||
if _, err := graph.Get(image.ID); err == nil {
|
||||
t.Fatal("Image should not exist after Register is interrupted")
|
||||
}
|
||||
// Registering the same image again should succeed if the first register was interrupted
|
||||
goodArchive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := graph.Register(v1Descriptor{image}, goodArchive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Do more extensive tests (ex: create multiple, delete, recreate;
|
||||
// create multiple, check the amount of images and paths, etc..)
|
||||
func TestGraphCreate(t *testing.T) {
|
||||
graph, _ := tempGraph(t)
|
||||
defer nukeGraph(graph)
|
||||
archive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
img, err := graph.Create(archive, "", "", "Testing", "", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := image.ValidateID(img.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if img.Comment != "Testing" {
|
||||
t.Fatalf("Wrong comment: should be '%s', not '%s'", "Testing", img.Comment)
|
||||
}
|
||||
if img.DockerVersion != dockerversion.Version {
|
||||
t.Fatalf("Wrong docker_version: should be '%s', not '%s'", dockerversion.Version, img.DockerVersion)
|
||||
}
|
||||
images := graph.Map()
|
||||
if l := len(images); l != 1 {
|
||||
t.Fatalf("Wrong number of images. Should be %d, not %d", 1, l)
|
||||
}
|
||||
if images[img.ID] == nil {
|
||||
t.Fatalf("Could not find image with id %s", img.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegister(t *testing.T) {
|
||||
graph, _ := tempGraph(t)
|
||||
defer nukeGraph(graph)
|
||||
archive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
image := &image.Image{
|
||||
ID: stringid.GenerateNonCryptoID(),
|
||||
Comment: "testing",
|
||||
Created: time.Now(),
|
||||
}
|
||||
err = graph.Register(v1Descriptor{image}, archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
images := graph.Map()
|
||||
if l := len(images); l != 1 {
|
||||
t.Fatalf("Wrong number of images. Should be %d, not %d", 1, l)
|
||||
}
|
||||
if resultImg, err := graph.Get(image.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if resultImg.ID != image.ID {
|
||||
t.Fatalf("Wrong image ID. Should be '%s', not '%s'", image.ID, resultImg.ID)
|
||||
}
|
||||
if resultImg.Comment != image.Comment {
|
||||
t.Fatalf("Wrong image comment. Should be '%s', not '%s'", image.Comment, resultImg.Comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that an image can be deleted by its shorthand prefix
|
||||
func TestDeletePrefix(t *testing.T) {
|
||||
graph, _ := tempGraph(t)
|
||||
defer nukeGraph(graph)
|
||||
img := createTestImage(graph, t)
|
||||
if err := graph.Delete(stringid.TruncateID(img.ID)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 0)
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
graph, _ := tempGraph(t)
|
||||
defer nukeGraph(graph)
|
||||
archive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 0)
|
||||
img, err := graph.Create(archive, "", "", "Bla bla", "", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 1)
|
||||
if err := graph.Delete(img.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 0)
|
||||
|
||||
archive, err = fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Test 2 create (same name) / 1 delete
|
||||
img1, err := graph.Create(archive, "", "", "Testing", "", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archive, err = fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = graph.Create(archive, "", "", "Testing", "", nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 2)
|
||||
if err := graph.Delete(img1.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 1)
|
||||
|
||||
// Test delete wrong name
|
||||
if err := graph.Delete("Not_foo"); err == nil {
|
||||
t.Fatalf("Deleting wrong ID should return an error")
|
||||
}
|
||||
assertNImages(graph, t, 1)
|
||||
|
||||
archive, err = fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Test delete twice (pull -> rm -> pull -> rm)
|
||||
if err := graph.Register(v1Descriptor{img1}, archive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := graph.Delete(img1.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertNImages(graph, t, 1)
|
||||
}
|
||||
|
||||
func TestByParent(t *testing.T) {
|
||||
archive1, _ := fakeTar()
|
||||
archive2, _ := fakeTar()
|
||||
archive3, _ := fakeTar()
|
||||
|
||||
graph, _ := tempGraph(t)
|
||||
defer nukeGraph(graph)
|
||||
parentImage := &image.Image{
|
||||
ID: stringid.GenerateNonCryptoID(),
|
||||
Comment: "parent",
|
||||
Created: time.Now(),
|
||||
Parent: "",
|
||||
}
|
||||
childImage1 := &image.Image{
|
||||
ID: stringid.GenerateNonCryptoID(),
|
||||
Comment: "child1",
|
||||
Created: time.Now(),
|
||||
Parent: parentImage.ID,
|
||||
}
|
||||
childImage2 := &image.Image{
|
||||
ID: stringid.GenerateNonCryptoID(),
|
||||
Comment: "child2",
|
||||
Created: time.Now(),
|
||||
Parent: parentImage.ID,
|
||||
}
|
||||
|
||||
err := graph.Register(v1Descriptor{parentImage}, archive1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = graph.Register(v1Descriptor{childImage1}, archive2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = graph.Register(v1Descriptor{childImage2}, archive3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
byParent := graph.ByParent()
|
||||
numChildren := len(byParent[parentImage.ID])
|
||||
if numChildren != 2 {
|
||||
t.Fatalf("Expected 2 children, found %d", numChildren)
|
||||
}
|
||||
}
|
||||
|
||||
func createTestImage(graph *Graph, t *testing.T) *image.Image {
|
||||
archive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
img, err := graph.Create(archive, "", "", "Test image", "", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func assertNImages(graph *Graph, t *testing.T, n int) {
|
||||
images := graph.Map()
|
||||
if actualN := len(images); actualN != n {
|
||||
t.Fatalf("Expected %d images, found %d", n, actualN)
|
||||
}
|
||||
}
|
||||
|
||||
func tempGraph(t *testing.T) (*Graph, graphdriver.Driver) {
|
||||
tmp, err := ioutil.TempDir("", "docker-graph-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
driver, err := graphdriver.New(tmp, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
graph, err := NewGraph(tmp, driver, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return graph, driver
|
||||
}
|
||||
|
||||
func nukeGraph(graph *Graph) {
|
||||
graph.driver.Cleanup()
|
||||
os.RemoveAll(graph.root)
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// +build !windows
|
||||
|
||||
package graph
|
||||
|
||||
// allowBaseParentImage allows images to define a custom parent that is not
|
||||
// transported with push/pull but already included with the installation.
|
||||
// Only used in Windows.
|
||||
const allowBaseParentImage = false
|
|
@ -1,8 +0,0 @@
|
|||
// +build windows
|
||||
|
||||
package graph
|
||||
|
||||
// allowBaseParentImage allows images to define a custom parent that is not
|
||||
// transported with push/pull but already included with the installation.
|
||||
// Only used in Windows.
|
||||
const allowBaseParentImage = true
|
119
graph/history.go
119
graph/history.go
|
@ -1,119 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
// walkHistory calls the handler function for each image in the
|
||||
// provided images lineage starting from immediate parent.
|
||||
func (graph *Graph) walkHistory(img *image.Image, handler func(image.Image) error) (err error) {
|
||||
currentImg := img
|
||||
for currentImg != nil {
|
||||
if handler != nil {
|
||||
if err := handler(*currentImg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
currentImg, err = graph.GetParent(currentImg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error while getting parent image: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// depth returns the number of parents for the current image
|
||||
func (graph *Graph) depth(img *image.Image) (int, error) {
|
||||
var (
|
||||
count = 0
|
||||
parent = img
|
||||
err error
|
||||
)
|
||||
|
||||
for parent != nil {
|
||||
count++
|
||||
if parent, err = graph.GetParent(parent); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// Set the max depth to the aufs default that most kernels are compiled with.
|
||||
// For more information see: http://sourceforge.net/p/aufs/aufs3-standalone/ci/aufs3.12/tree/config.mk
|
||||
const maxImageDepth = 127
|
||||
|
||||
// CheckDepth returns an error if the depth of an image, as returned
|
||||
// by ImageDepth, is too large to support creating a container from it
|
||||
// on this daemon.
|
||||
func (graph *Graph) CheckDepth(img *image.Image) error {
|
||||
// We add 2 layers to the depth because the container's rw and
|
||||
// init layer add to the restriction
|
||||
depth, err := graph.depth(img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if depth+2 >= maxImageDepth {
|
||||
return fmt.Errorf("Cannot create container with more than %d parents", maxImageDepth)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// History returns a slice of ImageHistory structures for the specified image
|
||||
// name by walking the image lineage.
|
||||
func (s *TagStore) History(name string) ([]*types.ImageHistory, error) {
|
||||
foundImage, err := s.LookupImage(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lookupMap := make(map[string][]string)
|
||||
for name, repository := range s.Repositories {
|
||||
for tag, id := range repository {
|
||||
// If the ID already has a reverse lookup, do not update it unless for "latest"
|
||||
if _, exists := lookupMap[id]; !exists {
|
||||
lookupMap[id] = []string{}
|
||||
}
|
||||
lookupMap[id] = append(lookupMap[id], utils.ImageReference(name, tag))
|
||||
}
|
||||
}
|
||||
|
||||
history := []*types.ImageHistory{}
|
||||
|
||||
err = s.graph.walkHistory(foundImage, func(img image.Image) error {
|
||||
history = append(history, &types.ImageHistory{
|
||||
ID: img.ID,
|
||||
Created: img.Created.Unix(),
|
||||
CreatedBy: strings.Join(img.ContainerConfig.Cmd.Slice(), " "),
|
||||
Tags: lookupMap[img.ID],
|
||||
Size: img.Size,
|
||||
Comment: img.Comment,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
return history, err
|
||||
}
|
||||
|
||||
// GetParent returns the parent image for the specified image.
|
||||
func (graph *Graph) GetParent(img *image.Image) (*image.Image, error) {
|
||||
if img.Parent == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return graph.Get(img.Parent)
|
||||
}
|
||||
|
||||
// getParentsSize returns the combined size of all parent images. If there is
|
||||
// no parent image or it's unavailable, it returns 0.
|
||||
func (graph *Graph) getParentsSize(img *image.Image) int64 {
|
||||
parentImage, err := graph.GetParent(img)
|
||||
if err != nil || parentImage == nil {
|
||||
return 0
|
||||
}
|
||||
return parentImage.Size + graph.getParentsSize(parentImage)
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/docker/pkg/httputils"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/runconfig"
|
||||
)
|
||||
|
||||
// Import imports an image, getting the archived layer data either from
|
||||
// inConfig (if src is "-"), or from a URI specified in src. Progress output is
|
||||
// written to outStream. Repository and tag names can optionally be given in
|
||||
// the repo and tag arguments, respectively.
|
||||
func (s *TagStore) Import(src string, repo string, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, containerConfig *runconfig.Config) error {
|
||||
var (
|
||||
sf = streamformatter.NewJSONStreamFormatter()
|
||||
archive io.ReadCloser
|
||||
resp *http.Response
|
||||
)
|
||||
|
||||
if src == "-" {
|
||||
archive = inConfig
|
||||
} else {
|
||||
inConfig.Close()
|
||||
u, err := url.Parse(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Scheme == "" {
|
||||
u.Scheme = "http"
|
||||
u.Host = src
|
||||
u.Path = ""
|
||||
}
|
||||
outStream.Write(sf.FormatStatus("", "Downloading from %s", u))
|
||||
resp, err = httputils.Download(u.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
progressReader := progressreader.New(progressreader.Config{
|
||||
In: resp.Body,
|
||||
Out: outStream,
|
||||
Formatter: sf,
|
||||
Size: resp.ContentLength,
|
||||
NewLines: true,
|
||||
ID: "",
|
||||
Action: "Importing",
|
||||
})
|
||||
archive = progressReader
|
||||
}
|
||||
|
||||
defer archive.Close()
|
||||
if len(msg) == 0 {
|
||||
msg = "Imported from " + src
|
||||
}
|
||||
|
||||
img, err := s.graph.Create(archive, "", "", msg, "", nil, containerConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Optionally register the image at REPO/TAG
|
||||
if repo != "" {
|
||||
if err := s.Tag(repo, tag, img.ID, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
outStream.Write(sf.FormatStatus("", img.ID))
|
||||
s.eventsService.Log("import", img.ID, "")
|
||||
return nil
|
||||
}
|
185
graph/list.go
185
graph/list.go
|
@ -1,185 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/parsers/filters"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
var acceptedImageFilterTags = map[string]struct{}{
|
||||
"dangling": {},
|
||||
"label": {},
|
||||
}
|
||||
|
||||
// byCreated is a temporary type used to sort a list of images by creation
|
||||
// time.
|
||||
type byCreated []*types.Image
|
||||
|
||||
func (r byCreated) Len() int { return len(r) }
|
||||
func (r byCreated) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
func (r byCreated) Less(i, j int) bool { return r[i].Created < r[j].Created }
|
||||
|
||||
// Images returns a filtered list of images. filterArgs is a JSON-encoded set
|
||||
// of filter arguments which will be interpreted by pkg/parsers/filters.
|
||||
// filter is a shell glob string applied to repository names. The argument
|
||||
// named all controls whether all images in the graph are filtered, or just
|
||||
// the heads.
|
||||
func (s *TagStore) Images(filterArgs, filter string, all bool) ([]*types.Image, error) {
|
||||
var (
|
||||
allImages map[string]*image.Image
|
||||
err error
|
||||
filtTagged = true
|
||||
filtLabel = false
|
||||
)
|
||||
|
||||
imageFilters, err := filters.FromParam(filterArgs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for name := range imageFilters {
|
||||
if _, ok := acceptedImageFilterTags[name]; !ok {
|
||||
return nil, fmt.Errorf("Invalid filter '%s'", name)
|
||||
}
|
||||
}
|
||||
|
||||
if i, ok := imageFilters["dangling"]; ok {
|
||||
for _, value := range i {
|
||||
if v := strings.ToLower(value); v == "true" {
|
||||
filtTagged = false
|
||||
} else if v != "false" {
|
||||
return nil, fmt.Errorf("Invalid filter 'dangling=%s'", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, filtLabel = imageFilters["label"]
|
||||
|
||||
if all && filtTagged {
|
||||
allImages = s.graph.Map()
|
||||
} else {
|
||||
allImages = s.graph.heads()
|
||||
}
|
||||
|
||||
lookup := make(map[string]*types.Image)
|
||||
s.Lock()
|
||||
for repoName, repository := range s.Repositories {
|
||||
filterTagName := ""
|
||||
if filter != "" {
|
||||
filterName := filter
|
||||
// Test if the tag was in there, if yes, get the name
|
||||
if strings.Contains(filterName, ":") {
|
||||
filterWithTag := strings.Split(filter, ":")
|
||||
filterName = filterWithTag[0]
|
||||
filterTagName = filterWithTag[1]
|
||||
}
|
||||
if match, _ := path.Match(filterName, repoName); !match {
|
||||
continue
|
||||
}
|
||||
if filterTagName != "" {
|
||||
if _, ok := repository[filterTagName]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
for ref, id := range repository {
|
||||
imgRef := utils.ImageReference(repoName, ref)
|
||||
if !strings.Contains(imgRef, filterTagName) {
|
||||
continue
|
||||
}
|
||||
image, err := s.graph.Get(id)
|
||||
if err != nil {
|
||||
logrus.Warnf("couldn't load %s from %s: %s", id, imgRef, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if lImage, exists := lookup[id]; exists {
|
||||
if filtTagged {
|
||||
if utils.DigestReference(ref) {
|
||||
lImage.RepoDigests = append(lImage.RepoDigests, imgRef)
|
||||
} else { // Tag Ref.
|
||||
lImage.RepoTags = append(lImage.RepoTags, imgRef)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// get the boolean list for if only the untagged images are requested
|
||||
delete(allImages, id)
|
||||
|
||||
if len(imageFilters["label"]) > 0 {
|
||||
if image.Config == nil {
|
||||
// Very old image that do not have image.Config (or even labels)
|
||||
continue
|
||||
}
|
||||
// We are now sure image.Config is not nil
|
||||
if !imageFilters.MatchKVList("label", image.Config.Labels) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if filtTagged {
|
||||
newImage := newImage(image, s.graph.getParentsSize(image))
|
||||
|
||||
if utils.DigestReference(ref) {
|
||||
newImage.RepoTags = []string{}
|
||||
newImage.RepoDigests = []string{imgRef}
|
||||
} else {
|
||||
newImage.RepoTags = []string{imgRef}
|
||||
newImage.RepoDigests = []string{}
|
||||
}
|
||||
|
||||
lookup[id] = newImage
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
s.Unlock()
|
||||
|
||||
images := []*types.Image{}
|
||||
for _, value := range lookup {
|
||||
images = append(images, value)
|
||||
}
|
||||
|
||||
// Display images which aren't part of a repository/tag
|
||||
if filter == "" || filtLabel {
|
||||
for _, image := range allImages {
|
||||
if len(imageFilters["label"]) > 0 {
|
||||
if image.Config == nil {
|
||||
// Very old image that do not have image.Config (or even labels)
|
||||
continue
|
||||
}
|
||||
// We are now sure image.Config is not nil
|
||||
if !imageFilters.MatchKVList("label", image.Config.Labels) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
newImage := newImage(image, s.graph.getParentsSize(image))
|
||||
newImage.RepoTags = []string{"<none>:<none>"}
|
||||
newImage.RepoDigests = []string{"<none>@<none>"}
|
||||
|
||||
images = append(images, newImage)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(byCreated(images)))
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func newImage(image *image.Image, parentSize int64) *types.Image {
|
||||
newImage := new(types.Image)
|
||||
newImage.ParentID = image.Parent
|
||||
newImage.ID = image.ID
|
||||
newImage.Created = image.Created.Unix()
|
||||
newImage.Size = image.Size
|
||||
newImage.VirtualSize = parentSize + image.Size
|
||||
if image.Config != nil {
|
||||
newImage.Labels = image.Config.Labels
|
||||
}
|
||||
return newImage
|
||||
}
|
134
graph/load.go
134
graph/load.go
|
@ -1,134 +0,0 @@
|
|||
// +build linux windows
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/chrootarchive"
|
||||
)
|
||||
|
||||
// Load uploads a set of images into the repository. This is the complementary of ImageExport.
|
||||
// The input stream is an uncompressed tar ball containing images and metadata.
|
||||
func (s *TagStore) Load(inTar io.ReadCloser, outStream io.Writer) error {
|
||||
tmpImageDir, err := ioutil.TempDir("", "docker-import-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tmpImageDir)
|
||||
|
||||
var (
|
||||
repoDir = filepath.Join(tmpImageDir, "repo")
|
||||
)
|
||||
|
||||
if err := os.Mkdir(repoDir, os.ModeDir); err != nil {
|
||||
return err
|
||||
}
|
||||
images := s.graph.Map()
|
||||
excludes := make([]string, len(images))
|
||||
i := 0
|
||||
for k := range images {
|
||||
excludes[i] = k
|
||||
i++
|
||||
}
|
||||
if err := chrootarchive.Untar(inTar, repoDir, &archive.TarOptions{ExcludePatterns: excludes}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dirs, err := ioutil.ReadDir(repoDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, d := range dirs {
|
||||
if d.IsDir() {
|
||||
if err := s.recursiveLoad(d.Name(), tmpImageDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reposJSONFile, err := os.Open(filepath.Join(tmpImageDir, "repo", "repositories"))
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer reposJSONFile.Close()
|
||||
|
||||
repositories := map[string]repository{}
|
||||
if err := json.NewDecoder(reposJSONFile).Decode(&repositories); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for imageName, tagMap := range repositories {
|
||||
for tag, address := range tagMap {
|
||||
if err := s.setLoad(imageName, tag, address, true, outStream); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TagStore) recursiveLoad(address, tmpImageDir string) error {
|
||||
if _, err := s.LookupImage(address); err != nil {
|
||||
logrus.Debugf("Loading %s", address)
|
||||
|
||||
imageJSON, err := ioutil.ReadFile(filepath.Join(tmpImageDir, "repo", address, "json"))
|
||||
if err != nil {
|
||||
logrus.Debugf("Error reading json: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
layer, err := os.Open(filepath.Join(tmpImageDir, "repo", address, "layer.tar"))
|
||||
if err != nil {
|
||||
logrus.Debugf("Error reading embedded tar: %v", err)
|
||||
return err
|
||||
}
|
||||
img, err := image.NewImgJSON(imageJSON)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error unmarshalling json: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := image.ValidateID(img.ID); err != nil {
|
||||
logrus.Debugf("Error validating ID: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure no two downloads of the same layer happen at the same time
|
||||
poolKey := "layer:" + img.ID
|
||||
broadcaster, found := s.poolAdd("pull", poolKey)
|
||||
if found {
|
||||
logrus.Debugf("Image (id: %s) load is already running, waiting", img.ID)
|
||||
return broadcaster.Wait()
|
||||
}
|
||||
|
||||
defer s.poolRemove("pull", poolKey)
|
||||
|
||||
if img.Parent != "" {
|
||||
if !s.graph.Exists(img.Parent) {
|
||||
if err := s.recursiveLoad(img.Parent, tmpImageDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := s.graph.Register(v1Descriptor{img}, layer); err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("Completed processing %s", address)
|
||||
return nil
|
||||
}
|
||||
logrus.Debugf("already loaded %s", address)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// +build !linux,!windows
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Load method is implemented here for non-linux and non-windows platforms and
|
||||
// may return an error indicating that image load is not supported on other platforms.
|
||||
func (s *TagStore) Load(inTar io.ReadCloser, outStream io.Writer) error {
|
||||
return fmt.Errorf("Load is not supported on this platform")
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/broadcaster"
|
||||
"github.com/docker/docker/pkg/reexec"
|
||||
)
|
||||
|
||||
func init() {
|
||||
reexec.Init()
|
||||
}
|
||||
|
||||
func TestPools(t *testing.T) {
|
||||
s := &TagStore{
|
||||
pullingPool: make(map[string]*broadcaster.Buffered),
|
||||
pushingPool: make(map[string]*broadcaster.Buffered),
|
||||
}
|
||||
|
||||
if _, found := s.poolAdd("pull", "test1"); found {
|
||||
t.Fatal("Expected pull test1 not to be in progress")
|
||||
}
|
||||
if _, found := s.poolAdd("pull", "test2"); found {
|
||||
t.Fatal("Expected pull test2 not to be in progress")
|
||||
}
|
||||
if _, found := s.poolAdd("push", "test1"); !found {
|
||||
t.Fatalf("Expected pull test1 to be in progress`")
|
||||
}
|
||||
if _, found := s.poolAdd("pull", "test1"); !found {
|
||||
t.Fatalf("Expected pull test1 to be in progress`")
|
||||
}
|
||||
if err := s.poolRemove("pull", "test2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.poolRemove("pull", "test2"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.poolRemove("pull", "test1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.poolRemove("push", "test1"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
350
graph/pull_v1.go
350
graph/pull_v1.go
|
@ -1,350 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
type v1Puller struct {
|
||||
*TagStore
|
||||
endpoint registry.APIEndpoint
|
||||
config *ImagePullConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
repoInfo *registry.RepositoryInfo
|
||||
session *registry.Session
|
||||
}
|
||||
|
||||
func (p *v1Puller) Pull(tag string) (fallback bool, err error) {
|
||||
if utils.DigestReference(tag) {
|
||||
// Allowing fallback, because HTTPS v1 is before HTTP v2
|
||||
return true, registry.ErrNoSupport{Err: errors.New("Cannot pull by digest with v1 registry")}
|
||||
}
|
||||
|
||||
tlsConfig, err := p.registryService.TLSConfig(p.repoInfo.Index.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Adds Docker-specific headers as well as user-specified headers (metaHeaders)
|
||||
tr := transport.NewTransport(
|
||||
// TODO(tiborvass): was ReceiveTimeout
|
||||
registry.NewTransport(tlsConfig),
|
||||
registry.DockerHeaders(p.config.MetaHeaders)...,
|
||||
)
|
||||
client := registry.HTTPClient(tr)
|
||||
v1Endpoint, err := p.endpoint.ToV1Endpoint(p.config.MetaHeaders)
|
||||
if err != nil {
|
||||
logrus.Debugf("Could not get v1 endpoint: %v", err)
|
||||
return true, err
|
||||
}
|
||||
p.session, err = registry.NewSession(client, p.config.AuthConfig, v1Endpoint)
|
||||
if err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
logrus.Debugf("Fallback from error: %s", err)
|
||||
return true, err
|
||||
}
|
||||
if err := p.pullRepository(tag); err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
return false, err
|
||||
}
|
||||
out := p.config.OutStream
|
||||
out.Write(p.sf.FormatStatus("", "%s: this image was pulled from a legacy registry. Important: This registry version will not be supported in future versions of docker.", p.repoInfo.CanonicalName))
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *v1Puller) pullRepository(askedTag string) error {
|
||||
out := p.config.OutStream
|
||||
out.Write(p.sf.FormatStatus("", "Pulling repository %s", p.repoInfo.CanonicalName))
|
||||
|
||||
repoData, err := p.session.GetRepositoryData(p.repoInfo.RemoteName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "HTTP code: 404") {
|
||||
return fmt.Errorf("Error: image %s not found", utils.ImageReference(p.repoInfo.RemoteName, askedTag))
|
||||
}
|
||||
// Unexpected HTTP error
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("Retrieving the tag list")
|
||||
tagsList := make(map[string]string)
|
||||
if askedTag == "" {
|
||||
tagsList, err = p.session.GetRemoteTags(repoData.Endpoints, p.repoInfo.RemoteName)
|
||||
} else {
|
||||
var tagID string
|
||||
tagID, err = p.session.GetRemoteTag(repoData.Endpoints, p.repoInfo.RemoteName, askedTag)
|
||||
tagsList[askedTag] = tagID
|
||||
}
|
||||
if err != nil {
|
||||
if err == registry.ErrRepoNotFound && askedTag != "" {
|
||||
return fmt.Errorf("Tag %s not found in repository %s", askedTag, p.repoInfo.CanonicalName)
|
||||
}
|
||||
logrus.Errorf("unable to get remote tags: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for tag, id := range tagsList {
|
||||
repoData.ImgList[id] = ®istry.ImgData{
|
||||
ID: id,
|
||||
Tag: tag,
|
||||
Checksum: "",
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("Registering tags")
|
||||
// If no tag has been specified, pull them all
|
||||
if askedTag == "" {
|
||||
for tag, id := range tagsList {
|
||||
repoData.ImgList[id].Tag = tag
|
||||
}
|
||||
} else {
|
||||
// Otherwise, check that the tag exists and use only that one
|
||||
id, exists := tagsList[askedTag]
|
||||
if !exists {
|
||||
return fmt.Errorf("Tag %s not found in repository %s", askedTag, p.repoInfo.CanonicalName)
|
||||
}
|
||||
repoData.ImgList[id].Tag = askedTag
|
||||
}
|
||||
|
||||
errors := make(chan error)
|
||||
|
||||
layersDownloaded := false
|
||||
imgIDs := []string{}
|
||||
sessionID := p.session.ID()
|
||||
defer func() {
|
||||
p.graph.Release(sessionID, imgIDs...)
|
||||
}()
|
||||
for _, imgData := range repoData.ImgList {
|
||||
downloadImage := func(img *registry.ImgData) {
|
||||
if askedTag != "" && img.Tag != askedTag {
|
||||
errors <- nil
|
||||
return
|
||||
}
|
||||
|
||||
if img.Tag == "" {
|
||||
logrus.Debugf("Image (id: %s) present in this repository but untagged, skipping", img.ID)
|
||||
errors <- nil
|
||||
return
|
||||
}
|
||||
|
||||
if err := image.ValidateID(img.ID); err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
|
||||
// ensure no two downloads of the same image happen at the same time
|
||||
poolKey := "img:" + img.ID
|
||||
broadcaster, found := p.poolAdd("pull", poolKey)
|
||||
broadcaster.Add(out)
|
||||
if found {
|
||||
errors <- broadcaster.Wait()
|
||||
return
|
||||
}
|
||||
defer p.poolRemove("pull", poolKey)
|
||||
|
||||
// we need to retain it until tagging
|
||||
p.graph.Retain(sessionID, img.ID)
|
||||
imgIDs = append(imgIDs, img.ID)
|
||||
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s", img.Tag, p.repoInfo.CanonicalName), nil))
|
||||
success := false
|
||||
var lastErr, err error
|
||||
var isDownloaded bool
|
||||
for _, ep := range p.repoInfo.Index.Mirrors {
|
||||
ep += "v1/"
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, mirror: %s", img.Tag, p.repoInfo.CanonicalName, ep), nil))
|
||||
if isDownloaded, err = p.pullImage(broadcaster, img.ID, ep); err != nil {
|
||||
// Don't report errors when pulling from mirrors.
|
||||
logrus.Debugf("Error pulling image (%s) from %s, mirror: %s, %s", img.Tag, p.repoInfo.CanonicalName, ep, err)
|
||||
continue
|
||||
}
|
||||
layersDownloaded = layersDownloaded || isDownloaded
|
||||
success = true
|
||||
break
|
||||
}
|
||||
if !success {
|
||||
for _, ep := range repoData.Endpoints {
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, endpoint: %s", img.Tag, p.repoInfo.CanonicalName, ep), nil))
|
||||
if isDownloaded, err = p.pullImage(broadcaster, img.ID, ep); err != nil {
|
||||
// It's not ideal that only the last error is returned, it would be better to concatenate the errors.
|
||||
// As the error is also given to the output stream the user will see the error.
|
||||
lastErr = err
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, p.repoInfo.CanonicalName, ep, err), nil))
|
||||
continue
|
||||
}
|
||||
layersDownloaded = layersDownloaded || isDownloaded
|
||||
success = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
err := fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, p.repoInfo.CanonicalName, lastErr)
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), err.Error(), nil))
|
||||
errors <- err
|
||||
broadcaster.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Download complete", nil))
|
||||
|
||||
errors <- nil
|
||||
}
|
||||
|
||||
go downloadImage(imgData)
|
||||
}
|
||||
|
||||
var lastError error
|
||||
for i := 0; i < len(repoData.ImgList); i++ {
|
||||
if err := <-errors; err != nil {
|
||||
lastError = err
|
||||
}
|
||||
}
|
||||
if lastError != nil {
|
||||
return lastError
|
||||
}
|
||||
|
||||
for tag, id := range tagsList {
|
||||
if askedTag != "" && tag != askedTag {
|
||||
continue
|
||||
}
|
||||
if err := p.Tag(p.repoInfo.LocalName, tag, id, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
requestedTag := p.repoInfo.LocalName
|
||||
if len(askedTag) > 0 {
|
||||
requestedTag = utils.ImageReference(p.repoInfo.LocalName, askedTag)
|
||||
}
|
||||
writeStatus(requestedTag, out, p.sf, layersDownloaded)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *v1Puller) pullImage(out io.Writer, imgID, endpoint string) (layersDownloaded bool, err error) {
|
||||
var history []string
|
||||
history, err = p.session.GetRemoteHistory(imgID, endpoint)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(imgID), "Pulling dependent layers", nil))
|
||||
// FIXME: Try to stream the images?
|
||||
// FIXME: Launch the getRemoteImage() in goroutines
|
||||
|
||||
sessionID := p.session.ID()
|
||||
// As imgID has been retained in pullRepository, no need to retain again
|
||||
p.graph.Retain(sessionID, history[1:]...)
|
||||
defer p.graph.Release(sessionID, history[1:]...)
|
||||
|
||||
layersDownloaded = false
|
||||
for i := len(history) - 1; i >= 0; i-- {
|
||||
id := history[i]
|
||||
|
||||
// ensure no two downloads of the same layer happen at the same time
|
||||
poolKey := "layer:" + id
|
||||
broadcaster, found := p.poolAdd("pull", poolKey)
|
||||
broadcaster.Add(out)
|
||||
if found {
|
||||
logrus.Debugf("Image (id: %s) pull is already running, skipping", id)
|
||||
err = broadcaster.Wait()
|
||||
if err != nil {
|
||||
return layersDownloaded, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// This must use a closure so it captures the value of err when
|
||||
// the function returns, not when the 'defer' is evaluated.
|
||||
defer func() {
|
||||
p.poolRemoveWithError("pull", poolKey, err)
|
||||
}()
|
||||
|
||||
if !p.graph.Exists(id) {
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Pulling metadata", nil))
|
||||
var (
|
||||
imgJSON []byte
|
||||
imgSize int64
|
||||
err error
|
||||
img *image.Image
|
||||
)
|
||||
retries := 5
|
||||
for j := 1; j <= retries; j++ {
|
||||
imgJSON, imgSize, err = p.session.GetRemoteImageJSON(id, endpoint)
|
||||
if err != nil && j == retries {
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Error pulling dependent layers", nil))
|
||||
return layersDownloaded, err
|
||||
} else if err != nil {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
img, err = image.NewImgJSON(imgJSON)
|
||||
layersDownloaded = true
|
||||
if err != nil && j == retries {
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Error pulling dependent layers", nil))
|
||||
return layersDownloaded, fmt.Errorf("Failed to parse json: %s", err)
|
||||
} else if err != nil {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for j := 1; j <= retries; j++ {
|
||||
// Get the layer
|
||||
status := "Pulling fs layer"
|
||||
if j > 1 {
|
||||
status = fmt.Sprintf("Pulling fs layer [retries: %d]", j)
|
||||
}
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(id), status, nil))
|
||||
layer, err := p.session.GetRemoteImageLayer(img.ID, endpoint, imgSize)
|
||||
if uerr, ok := err.(*url.Error); ok {
|
||||
err = uerr.Err
|
||||
}
|
||||
if terr, ok := err.(net.Error); ok && terr.Timeout() && j < retries {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else if err != nil {
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Error pulling dependent layers", nil))
|
||||
return layersDownloaded, err
|
||||
}
|
||||
layersDownloaded = true
|
||||
defer layer.Close()
|
||||
|
||||
err = p.graph.Register(v1Descriptor{img},
|
||||
progressreader.New(progressreader.Config{
|
||||
In: layer,
|
||||
Out: broadcaster,
|
||||
Formatter: p.sf,
|
||||
Size: imgSize,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(id),
|
||||
Action: "Downloading",
|
||||
}))
|
||||
if terr, ok := err.(net.Error); ok && terr.Timeout() && j < retries {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else if err != nil {
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Error downloading dependent layers", nil))
|
||||
return layersDownloaded, err
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Download complete", nil))
|
||||
broadcaster.Close()
|
||||
}
|
||||
return layersDownloaded, nil
|
||||
}
|
728
graph/pull_v2.go
728
graph/pull_v2.go
|
@ -1,728 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/broadcaster"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/utils"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type v2Puller struct {
|
||||
*TagStore
|
||||
endpoint registry.APIEndpoint
|
||||
config *ImagePullConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
repoInfo *registry.RepositoryInfo
|
||||
repo distribution.Repository
|
||||
sessionID string
|
||||
}
|
||||
|
||||
func (p *v2Puller) Pull(tag string) (fallback bool, err error) {
|
||||
// TODO(tiborvass): was ReceiveTimeout
|
||||
p.repo, err = newV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull")
|
||||
if err != nil {
|
||||
logrus.Warnf("Error getting v2 registry: %v", err)
|
||||
return true, err
|
||||
}
|
||||
|
||||
p.sessionID = stringid.GenerateRandomID()
|
||||
|
||||
if err := p.pullV2Repository(tag); err != nil {
|
||||
if registry.ContinueOnError(err) {
|
||||
logrus.Debugf("Error trying v2 registry: %v", err)
|
||||
return true, err
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *v2Puller) pullV2Repository(tag string) (err error) {
|
||||
var tags []string
|
||||
taggedName := p.repoInfo.LocalName
|
||||
if len(tag) > 0 {
|
||||
tags = []string{tag}
|
||||
taggedName = utils.ImageReference(p.repoInfo.LocalName, tag)
|
||||
} else {
|
||||
var err error
|
||||
|
||||
manSvc, err := p.repo.Manifests(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags, err = manSvc.Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
poolKey := "v2:" + taggedName
|
||||
broadcaster, found := p.poolAdd("pull", poolKey)
|
||||
broadcaster.Add(p.config.OutStream)
|
||||
if found {
|
||||
// Another pull of the same repository is already taking place; just wait for it to finish
|
||||
return broadcaster.Wait()
|
||||
}
|
||||
|
||||
// This must use a closure so it captures the value of err when the
|
||||
// function returns, not when the 'defer' is evaluated.
|
||||
defer func() {
|
||||
p.poolRemoveWithError("pull", poolKey, err)
|
||||
}()
|
||||
|
||||
var layersDownloaded bool
|
||||
for _, tag := range tags {
|
||||
// pulledNew is true if either new layers were downloaded OR if existing images were newly tagged
|
||||
// TODO(tiborvass): should we change the name of `layersDownload`? What about message in WriteStatus?
|
||||
pulledNew, err := p.pullV2Tag(broadcaster, tag, taggedName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
layersDownloaded = layersDownloaded || pulledNew
|
||||
}
|
||||
|
||||
writeStatus(taggedName, broadcaster, p.sf, layersDownloaded)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadInfo is used to pass information from download to extractor
|
||||
type downloadInfo struct {
|
||||
img contentAddressableDescriptor
|
||||
imgIndex int
|
||||
tmpFile *os.File
|
||||
digest digest.Digest
|
||||
layer distribution.ReadSeekCloser
|
||||
size int64
|
||||
err chan error
|
||||
poolKey string
|
||||
broadcaster *broadcaster.Buffered
|
||||
}
|
||||
|
||||
// contentAddressableDescriptor is used to pass image data from a manifest to the
|
||||
// graph.
|
||||
type contentAddressableDescriptor struct {
|
||||
id string
|
||||
parent string
|
||||
strongID digest.Digest
|
||||
compatibilityID string
|
||||
config []byte
|
||||
v1Compatibility []byte
|
||||
}
|
||||
|
||||
func newContentAddressableImage(v1Compatibility []byte, blobSum digest.Digest, parent digest.Digest) (contentAddressableDescriptor, error) {
|
||||
img := contentAddressableDescriptor{
|
||||
v1Compatibility: v1Compatibility,
|
||||
}
|
||||
|
||||
var err error
|
||||
img.config, err = image.MakeImageConfig(v1Compatibility, blobSum, parent)
|
||||
if err != nil {
|
||||
return img, err
|
||||
}
|
||||
img.strongID, err = image.StrongID(img.config)
|
||||
if err != nil {
|
||||
return img, err
|
||||
}
|
||||
|
||||
unmarshalledConfig, err := image.NewImgJSON(v1Compatibility)
|
||||
if err != nil {
|
||||
return img, err
|
||||
}
|
||||
|
||||
img.compatibilityID = unmarshalledConfig.ID
|
||||
img.id = img.strongID.Hex()
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// ID returns the actual ID to be used for the downloaded image. This may be
|
||||
// a computed ID.
|
||||
func (img contentAddressableDescriptor) ID() string {
|
||||
return img.id
|
||||
}
|
||||
|
||||
// Parent returns the parent ID to be used for the image. This may be a
|
||||
// computed ID.
|
||||
func (img contentAddressableDescriptor) Parent() string {
|
||||
return img.parent
|
||||
}
|
||||
|
||||
// MarshalConfig renders the image structure into JSON.
|
||||
func (img contentAddressableDescriptor) MarshalConfig() ([]byte, error) {
|
||||
return img.config, nil
|
||||
}
|
||||
|
||||
type errVerification struct{}
|
||||
|
||||
func (errVerification) Error() string { return "verification failed" }
|
||||
|
||||
func (p *v2Puller) download(di *downloadInfo) {
|
||||
logrus.Debugf("pulling blob %q to %s", di.digest, di.img.id)
|
||||
|
||||
blobs := p.repo.Blobs(context.Background())
|
||||
|
||||
desc, err := blobs.Stat(context.Background(), di.digest)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error statting layer: %v", err)
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
di.size = desc.Size
|
||||
|
||||
layerDownload, err := blobs.Open(context.Background(), di.digest)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error fetching layer: %v", err)
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
defer layerDownload.Close()
|
||||
|
||||
verifier, err := digest.NewDigestVerifier(di.digest)
|
||||
if err != nil {
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: ioutil.NopCloser(io.TeeReader(layerDownload, verifier)),
|
||||
Out: di.broadcaster,
|
||||
Formatter: p.sf,
|
||||
Size: di.size,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(di.img.id),
|
||||
Action: "Downloading",
|
||||
})
|
||||
io.Copy(di.tmpFile, reader)
|
||||
|
||||
di.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.id), "Verifying Checksum", nil))
|
||||
|
||||
if !verifier.Verified() {
|
||||
err = fmt.Errorf("filesystem layer verification failed for digest %s", di.digest)
|
||||
logrus.Error(err)
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
|
||||
di.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.id), "Download complete", nil))
|
||||
|
||||
logrus.Debugf("Downloaded %s to tempfile %s", di.img.id, di.tmpFile.Name())
|
||||
di.layer = layerDownload
|
||||
|
||||
di.err <- nil
|
||||
}
|
||||
|
||||
func (p *v2Puller) pullV2Tag(out io.Writer, tag, taggedName string) (tagUpdated bool, err error) {
|
||||
logrus.Debugf("Pulling tag from V2 registry: %q", tag)
|
||||
|
||||
manSvc, err := p.repo.Manifests(context.Background())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
unverifiedManifest, err := manSvc.GetByTag(tag)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if unverifiedManifest == nil {
|
||||
return false, fmt.Errorf("image manifest does not exist for tag %q", tag)
|
||||
}
|
||||
var verifiedManifest *schema1.Manifest
|
||||
verifiedManifest, err = verifyManifest(unverifiedManifest, tag)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// remove duplicate layers and check parent chain validity
|
||||
err = fixManifestLayers(verifiedManifest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
imgs, err := p.getImageInfos(verifiedManifest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
out.Write(p.sf.FormatStatus(tag, "Pulling from %s", p.repo.Name()))
|
||||
|
||||
var downloads []*downloadInfo
|
||||
|
||||
var layerIDs []string
|
||||
defer func() {
|
||||
p.graph.Release(p.sessionID, layerIDs...)
|
||||
|
||||
for _, d := range downloads {
|
||||
p.poolRemoveWithError("pull", d.poolKey, err)
|
||||
if d.tmpFile != nil {
|
||||
d.tmpFile.Close()
|
||||
if err := os.RemoveAll(d.tmpFile.Name()); err != nil {
|
||||
logrus.Errorf("Failed to remove temp file: %s", d.tmpFile.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for i := len(verifiedManifest.FSLayers) - 1; i >= 0; i-- {
|
||||
img := imgs[i]
|
||||
|
||||
p.graph.Retain(p.sessionID, img.id)
|
||||
layerIDs = append(layerIDs, img.id)
|
||||
|
||||
p.graph.imageMutex.Lock(img.id)
|
||||
|
||||
// Check if exists
|
||||
if p.graph.Exists(img.id) {
|
||||
if err := p.validateImageInGraph(img.id, imgs, i); err != nil {
|
||||
p.graph.imageMutex.Unlock(img.id)
|
||||
return false, fmt.Errorf("image validation failed: %v", err)
|
||||
}
|
||||
logrus.Debugf("Image already exists: %s", img.id)
|
||||
p.graph.imageMutex.Unlock(img.id)
|
||||
continue
|
||||
}
|
||||
p.graph.imageMutex.Unlock(img.id)
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.id), "Pulling fs layer", nil))
|
||||
|
||||
d := &downloadInfo{
|
||||
img: img,
|
||||
imgIndex: i,
|
||||
poolKey: "v2layer:" + img.id,
|
||||
digest: verifiedManifest.FSLayers[i].BlobSum,
|
||||
// TODO: seems like this chan buffer solved hanging problem in go1.5,
|
||||
// this can indicate some deeper problem that somehow we never take
|
||||
// error from channel in loop below
|
||||
err: make(chan error, 1),
|
||||
}
|
||||
|
||||
tmpFile, err := ioutil.TempFile("", "GetImageBlob")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
d.tmpFile = tmpFile
|
||||
|
||||
downloads = append(downloads, d)
|
||||
|
||||
broadcaster, found := p.poolAdd("pull", d.poolKey)
|
||||
broadcaster.Add(out)
|
||||
d.broadcaster = broadcaster
|
||||
if found {
|
||||
d.err <- nil
|
||||
} else {
|
||||
go p.download(d)
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range downloads {
|
||||
if err := <-d.err; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if d.layer == nil {
|
||||
// Wait for a different pull to download and extract
|
||||
// this layer.
|
||||
err = d.broadcaster.Wait()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
d.tmpFile.Seek(0, 0)
|
||||
err := func() error {
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: d.tmpFile,
|
||||
Out: d.broadcaster,
|
||||
Formatter: p.sf,
|
||||
Size: d.size,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(d.img.id),
|
||||
Action: "Extracting",
|
||||
})
|
||||
|
||||
p.graph.imagesMutex.Lock()
|
||||
defer p.graph.imagesMutex.Unlock()
|
||||
|
||||
p.graph.imageMutex.Lock(d.img.id)
|
||||
defer p.graph.imageMutex.Unlock(d.img.id)
|
||||
|
||||
// Must recheck the data on disk if any exists.
|
||||
// This protects against races where something
|
||||
// else is written to the graph under this ID
|
||||
// after attemptIDReuse.
|
||||
if p.graph.Exists(d.img.id) {
|
||||
if err := p.validateImageInGraph(d.img.id, imgs, d.imgIndex); err != nil {
|
||||
return fmt.Errorf("image validation failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.graph.register(d.img, reader); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.graph.setLayerDigest(d.img.id, d.digest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.graph.setV1CompatibilityConfig(d.img.id, d.img.v1Compatibility); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
d.broadcaster.Write(p.sf.FormatProgress(stringid.TruncateID(d.img.id), "Pull complete", nil))
|
||||
d.broadcaster.Close()
|
||||
tagUpdated = true
|
||||
}
|
||||
|
||||
manifestDigest, _, err := digestFromManifest(unverifiedManifest, p.repoInfo.LocalName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check for new tag if no layers downloaded
|
||||
if !tagUpdated {
|
||||
repo, err := p.get(p.repoInfo.LocalName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if repo != nil {
|
||||
if _, exists := repo[tag]; !exists {
|
||||
tagUpdated = true
|
||||
}
|
||||
} else {
|
||||
tagUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
firstID := layerIDs[len(layerIDs)-1]
|
||||
if utils.DigestReference(tag) {
|
||||
// TODO(stevvooe): Ideally, we should always set the digest so we can
|
||||
// use the digest whether we pull by it or not. Unfortunately, the tag
|
||||
// store treats the digest as a separate tag, meaning there may be an
|
||||
// untagged digest image that would seem to be dangling by a user.
|
||||
if err = p.setDigest(p.repoInfo.LocalName, tag, firstID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
// only set the repository/tag -> image ID mapping when pulling by tag (i.e. not by digest)
|
||||
if err = p.Tag(p.repoInfo.LocalName, tag, firstID, true); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if manifestDigest != "" {
|
||||
out.Write(p.sf.FormatStatus("", "Digest: %s", manifestDigest))
|
||||
}
|
||||
|
||||
return tagUpdated, nil
|
||||
}
|
||||
|
||||
func verifyManifest(signedManifest *schema1.SignedManifest, tag string) (m *schema1.Manifest, err error) {
|
||||
// If pull by digest, then verify the manifest digest. NOTE: It is
|
||||
// important to do this first, before any other content validation. If the
|
||||
// digest cannot be verified, don't even bother with those other things.
|
||||
if manifestDigest, err := digest.ParseDigest(tag); err == nil {
|
||||
verifier, err := digest.NewDigestVerifier(manifestDigest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := signedManifest.Payload()
|
||||
if err != nil {
|
||||
// If this failed, the signatures section was corrupted
|
||||
// or missing. Treat the entire manifest as the payload.
|
||||
payload = signedManifest.Raw
|
||||
}
|
||||
if _, err := verifier.Write(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !verifier.Verified() {
|
||||
err := fmt.Errorf("image verification failed for digest %s", manifestDigest)
|
||||
logrus.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var verifiedManifest schema1.Manifest
|
||||
if err = json.Unmarshal(payload, &verifiedManifest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m = &verifiedManifest
|
||||
} else {
|
||||
m = &signedManifest.Manifest
|
||||
}
|
||||
|
||||
if m.SchemaVersion != 1 {
|
||||
return nil, fmt.Errorf("unsupported schema version %d for tag %q", m.SchemaVersion, tag)
|
||||
}
|
||||
if len(m.FSLayers) != len(m.History) {
|
||||
return nil, fmt.Errorf("length of history not equal to number of layers for tag %q", tag)
|
||||
}
|
||||
if len(m.FSLayers) == 0 {
|
||||
return nil, fmt.Errorf("no FSLayers in manifest for tag %q", tag)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// fixManifestLayers removes repeated layers from the manifest and checks the
|
||||
// correctness of the parent chain.
|
||||
func fixManifestLayers(m *schema1.Manifest) error {
|
||||
images := make([]*image.Image, len(m.FSLayers))
|
||||
for i := range m.FSLayers {
|
||||
img, err := image.NewImgJSON([]byte(m.History[i].V1Compatibility))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
images[i] = img
|
||||
if err := image.ValidateID(img.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if images[len(images)-1].Parent != "" && !allowBaseParentImage {
|
||||
// Windows base layer can point to a base layer parent that is not in manifest.
|
||||
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 images {
|
||||
// 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(images) - 2; i >= 0; i-- {
|
||||
if images[i].ID == images[i+1].ID { // repeated ID. remove and continue
|
||||
m.FSLayers = append(m.FSLayers[:i], m.FSLayers[i+1:]...)
|
||||
m.History = append(m.History[:i], m.History[i+1:]...)
|
||||
} else if images[i].Parent != images[i+1].ID {
|
||||
return fmt.Errorf("Invalid parent ID. Expected %v, got %v.", images[i+1].ID, images[i].Parent)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getImageInfos returns an imageinfo struct for every image in the manifest.
|
||||
// These objects contain both calculated strongIDs and compatibilityIDs found
|
||||
// in v1Compatibility object.
|
||||
func (p *v2Puller) getImageInfos(m *schema1.Manifest) ([]contentAddressableDescriptor, error) {
|
||||
imgs := make([]contentAddressableDescriptor, len(m.FSLayers))
|
||||
|
||||
var parent digest.Digest
|
||||
for i := len(imgs) - 1; i >= 0; i-- {
|
||||
var err error
|
||||
imgs[i], err = newContentAddressableImage([]byte(m.History[i].V1Compatibility), m.FSLayers[i].BlobSum, parent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parent = imgs[i].strongID
|
||||
}
|
||||
|
||||
p.attemptIDReuse(imgs)
|
||||
|
||||
// reset the base layer parent for windows
|
||||
if allowBaseParentImage {
|
||||
var base struct{ Parent string }
|
||||
if err := json.Unmarshal(imgs[len(imgs)-1].v1Compatibility, &base); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if base.Parent != "" {
|
||||
imgs[len(imgs)-1].parent = base.Parent
|
||||
}
|
||||
}
|
||||
|
||||
return imgs, nil
|
||||
}
|
||||
|
||||
// attemptIDReuse does a best attempt to match verified compatibilityIDs
|
||||
// already in the graph with the computed strongIDs so we can keep using them.
|
||||
// This process will never fail but may just return the strongIDs if none of
|
||||
// the compatibilityIDs exists or can be verified. If the strongIDs themselves
|
||||
// fail verification, we deterministically generate alternate IDs to use until
|
||||
// we find one that's available or already exists with the correct data.
|
||||
func (p *v2Puller) attemptIDReuse(imgs []contentAddressableDescriptor) {
|
||||
// This function needs to be protected with a global lock, because it
|
||||
// locks multiple IDs at once, and there's no good way to make sure
|
||||
// the locking happens a deterministic order.
|
||||
p.graph.imagesMutex.Lock()
|
||||
defer p.graph.imagesMutex.Unlock()
|
||||
|
||||
idMap := make(map[string]struct{})
|
||||
for _, img := range imgs {
|
||||
idMap[img.id] = struct{}{}
|
||||
idMap[img.compatibilityID] = struct{}{}
|
||||
|
||||
if p.graph.Exists(img.compatibilityID) {
|
||||
if _, err := p.graph.generateV1CompatibilityChain(img.compatibilityID); err != nil {
|
||||
logrus.Debugf("Migration v1Compatibility generation error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for id := range idMap {
|
||||
p.graph.imageMutex.Lock(id)
|
||||
defer p.graph.imageMutex.Unlock(id)
|
||||
}
|
||||
|
||||
// continueReuse controls whether the function will try to find
|
||||
// existing layers on disk under the old v1 IDs, to avoid repulling
|
||||
// them. The hashes are checked to ensure these layers are okay to
|
||||
// use. continueReuse starts out as true, but is set to false if
|
||||
// the code encounters something that doesn't match the expected hash.
|
||||
continueReuse := true
|
||||
|
||||
for i := len(imgs) - 1; i >= 0; i-- {
|
||||
// TODO - (swernli:11-16-2015) Skipping content addressable IDs on
|
||||
// Windows as a hack for TP4 compat. The correct fix is to ensure that
|
||||
// Windows layers do not have anything in them that takes a dependency
|
||||
// on the ID of the layer in the management client. This will be fixed
|
||||
// in Windows post-TP4.
|
||||
if runtime.GOOS == "windows" {
|
||||
imgs[i].id = imgs[i].compatibilityID
|
||||
}
|
||||
|
||||
if p.graph.Exists(imgs[i].id) {
|
||||
// Found an image in the graph under the strongID. Validate the
|
||||
// image before using it.
|
||||
if err := p.validateImageInGraph(imgs[i].id, imgs, i); err != nil {
|
||||
continueReuse = false
|
||||
logrus.Debugf("not using existing strongID: %v", err)
|
||||
|
||||
// The strong ID existed in the graph but didn't
|
||||
// validate successfully. We can't use the strong ID
|
||||
// because it didn't validate successfully. Treat the
|
||||
// graph like a hash table with probing... compute
|
||||
// SHA256(id) until we find an ID that either doesn't
|
||||
// already exist in the graph, or has existing content
|
||||
// that validates successfully.
|
||||
for {
|
||||
if err := p.tryNextID(imgs, i, idMap); err != nil {
|
||||
logrus.Debug(err.Error())
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if continueReuse {
|
||||
compatibilityID := imgs[i].compatibilityID
|
||||
if err := p.validateImageInGraph(compatibilityID, imgs, i); err != nil {
|
||||
logrus.Debugf("stopping ID reuse: %v", err)
|
||||
continueReuse = false
|
||||
} else {
|
||||
// The compatibility ID exists in the graph and was
|
||||
// validated. Use it.
|
||||
imgs[i].id = compatibilityID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fix up the parents of the images
|
||||
for i := 0; i < len(imgs); i++ {
|
||||
if i == len(imgs)-1 { // Base layer
|
||||
imgs[i].parent = ""
|
||||
} else {
|
||||
imgs[i].parent = imgs[i+1].id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validateImageInGraph checks that an image in the graph has the expected
|
||||
// strongID. id is the entry in the graph to check, imgs is the slice of
|
||||
// images being processed (for access to the parent), and i is the index
|
||||
// into this slice which the graph entry should be checked against.
|
||||
func (p *v2Puller) validateImageInGraph(id string, imgs []contentAddressableDescriptor, i int) error {
|
||||
img, err := p.graph.Get(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("missing: %v", err)
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
// TODO - (swernli:11-16-2015) Skipping content addressable IDs on
|
||||
// Windows as a hack for TP4 compat. The correct fix is to ensure that
|
||||
// Windows layers do not have anything in them that takes a dependency
|
||||
// on the ID of the layer in the management client. This will be fixed
|
||||
// in Windows post-TP4.
|
||||
return nil
|
||||
}
|
||||
layerID, err := p.graph.getLayerDigest(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("digest: %v", err)
|
||||
}
|
||||
var parentID digest.Digest
|
||||
if i != len(imgs)-1 {
|
||||
if img.Parent != imgs[i+1].id { // comparing that graph points to validated ID
|
||||
return fmt.Errorf("parent: %v %v", img.Parent, imgs[i+1].id)
|
||||
}
|
||||
parentID = imgs[i+1].strongID
|
||||
} else if img.Parent != "" {
|
||||
return fmt.Errorf("unexpected parent: %v", img.Parent)
|
||||
}
|
||||
|
||||
v1Config, err := p.graph.getV1CompatibilityConfig(img.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("v1Compatibility: %v %v", img.ID, err)
|
||||
}
|
||||
|
||||
json, err := image.MakeImageConfig(v1Config, layerID, parentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("make config: %v", err)
|
||||
}
|
||||
|
||||
if dgst, err := image.StrongID(json); err == nil && dgst == imgs[i].strongID {
|
||||
logrus.Debugf("Validated %v as %v", dgst, id)
|
||||
} else {
|
||||
return fmt.Errorf("digest mismatch: %v %v, error: %v", dgst, imgs[i].strongID, err)
|
||||
}
|
||||
|
||||
// All clear
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *v2Puller) tryNextID(imgs []contentAddressableDescriptor, i int, idMap map[string]struct{}) error {
|
||||
nextID, _ := digest.FromBytes([]byte(imgs[i].id))
|
||||
imgs[i].id = nextID.Hex()
|
||||
|
||||
if _, exists := idMap[imgs[i].id]; !exists {
|
||||
p.graph.imageMutex.Lock(imgs[i].id)
|
||||
defer p.graph.imageMutex.Unlock(imgs[i].id)
|
||||
}
|
||||
|
||||
if p.graph.Exists(imgs[i].id) {
|
||||
if err := p.validateImageInGraph(imgs[i].id, imgs, i); err != nil {
|
||||
return fmt.Errorf("not using existing strongID permutation %s: %v", imgs[i].id, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
126
graph/push.go
126
graph/push.go
|
@ -1,126 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/registry"
|
||||
)
|
||||
|
||||
// ImagePushConfig stores push configuration.
|
||||
type ImagePushConfig struct {
|
||||
// MetaHeaders store HTTP headers with metadata about the image
|
||||
// (DockerHeaders with prefix X-Meta- in the request).
|
||||
MetaHeaders map[string][]string
|
||||
// AuthConfig holds authentication credentials for authenticating with
|
||||
// the registry.
|
||||
AuthConfig *cliconfig.AuthConfig
|
||||
// Tag is the specific variant of the image to be pushed.
|
||||
// If no tag is provided, all tags will be pushed.
|
||||
Tag string
|
||||
// OutStream is the output writer for showing the status of the push
|
||||
// operation.
|
||||
OutStream io.Writer
|
||||
}
|
||||
|
||||
// pusher is an interface that abstracts pushing for different API versions.
|
||||
type pusher interface {
|
||||
// Push tries to push the image configured at the creation of Pusher.
|
||||
// Push returns an error if any, as well as a boolean that determines whether to retry Push on the next configured endpoint.
|
||||
//
|
||||
// TODO(tiborvass): have Push() take a reference to repository + tag, so that the pusher itself is repository-agnostic.
|
||||
Push() (fallback bool, err error)
|
||||
}
|
||||
|
||||
// newPusher creates a new Pusher interface that will push to either a v1 or v2
|
||||
// registry. The endpoint argument contains a Version field that determines
|
||||
// whether a v1 or v2 pusher will be created. The other parameters are passed
|
||||
// through to the underlying pusher implementation for use during the actual
|
||||
// push operation.
|
||||
func (s *TagStore) newPusher(endpoint registry.APIEndpoint, localRepo repository, repoInfo *registry.RepositoryInfo, imagePushConfig *ImagePushConfig, sf *streamformatter.StreamFormatter) (pusher, error) {
|
||||
switch endpoint.Version {
|
||||
case registry.APIVersion2:
|
||||
return &v2Pusher{
|
||||
TagStore: s,
|
||||
endpoint: endpoint,
|
||||
localRepo: localRepo,
|
||||
repoInfo: repoInfo,
|
||||
config: imagePushConfig,
|
||||
sf: sf,
|
||||
layersPushed: make(map[digest.Digest]bool),
|
||||
}, nil
|
||||
case registry.APIVersion1:
|
||||
return &v1Pusher{
|
||||
TagStore: s,
|
||||
endpoint: endpoint,
|
||||
localRepo: localRepo,
|
||||
repoInfo: repoInfo,
|
||||
config: imagePushConfig,
|
||||
sf: sf,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)
|
||||
}
|
||||
|
||||
// Push initiates a push operation on the repository named localName.
|
||||
func (s *TagStore) Push(localName string, imagePushConfig *ImagePushConfig) error {
|
||||
// FIXME: Allow to interrupt current push when new push of same image is done.
|
||||
|
||||
var sf = streamformatter.NewJSONStreamFormatter()
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := s.registryService.ResolveRepository(localName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := s.registryService.LookupPushEndpoints(repoInfo.CanonicalName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reposLen := 1
|
||||
if imagePushConfig.Tag == "" {
|
||||
reposLen = len(s.Repositories[repoInfo.LocalName])
|
||||
}
|
||||
|
||||
imagePushConfig.OutStream.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", repoInfo.CanonicalName, reposLen))
|
||||
|
||||
// If it fails, try to get the repository
|
||||
localRepo, exists := s.Repositories[repoInfo.LocalName]
|
||||
if !exists {
|
||||
return fmt.Errorf("Repository does not exist: %s", repoInfo.LocalName)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, endpoint := range endpoints {
|
||||
logrus.Debugf("Trying to push %s to %s %s", repoInfo.CanonicalName, endpoint.URL, endpoint.Version)
|
||||
|
||||
pusher, err := s.newPusher(endpoint, localRepo, repoInfo, imagePushConfig, sf)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if fallback, err := pusher.Push(); err != nil {
|
||||
if fallback {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
logrus.Debugf("Not continuing with error: %v", err)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
s.eventsService.Log("push", repoInfo.LocalName, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("no endpoints found for %s", repoInfo.CanonicalName)
|
||||
}
|
||||
return lastErr
|
||||
}
|
354
graph/push_v1.go
354
graph/push_v1.go
|
@ -1,354 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
type v1Pusher struct {
|
||||
*TagStore
|
||||
endpoint registry.APIEndpoint
|
||||
localRepo repository
|
||||
repoInfo *registry.RepositoryInfo
|
||||
config *ImagePushConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
session *registry.Session
|
||||
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func (p *v1Pusher) Push() (fallback bool, err error) {
|
||||
tlsConfig, err := p.registryService.TLSConfig(p.repoInfo.Index.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Adds Docker-specific headers as well as user-specified headers (metaHeaders)
|
||||
tr := transport.NewTransport(
|
||||
// TODO(tiborvass): was NoTimeout
|
||||
registry.NewTransport(tlsConfig),
|
||||
registry.DockerHeaders(p.config.MetaHeaders)...,
|
||||
)
|
||||
client := registry.HTTPClient(tr)
|
||||
v1Endpoint, err := p.endpoint.ToV1Endpoint(p.config.MetaHeaders)
|
||||
if err != nil {
|
||||
logrus.Debugf("Could not get v1 endpoint: %v", err)
|
||||
return true, err
|
||||
}
|
||||
p.session, err = registry.NewSession(client, p.config.AuthConfig, v1Endpoint)
|
||||
if err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
return true, err
|
||||
}
|
||||
if err := p.pushRepository(p.config.Tag); err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Retrieve the all the images to be uploaded in the correct order
|
||||
func (p *v1Pusher) getImageList(requestedTag string) ([]string, map[string][]string, error) {
|
||||
var (
|
||||
imageList []string
|
||||
imagesSeen = make(map[string]bool)
|
||||
tagsByImage = make(map[string][]string)
|
||||
)
|
||||
|
||||
for tag, id := range p.localRepo {
|
||||
if requestedTag != "" && requestedTag != tag {
|
||||
// Include only the requested tag.
|
||||
continue
|
||||
}
|
||||
|
||||
if utils.DigestReference(tag) {
|
||||
// Ignore digest references.
|
||||
continue
|
||||
}
|
||||
|
||||
var imageListForThisTag []string
|
||||
|
||||
tagsByImage[id] = append(tagsByImage[id], tag)
|
||||
|
||||
for img, err := p.graph.Get(id); img != nil; img, err = p.graph.GetParent(img) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if imagesSeen[img.ID] {
|
||||
// This image is already on the list, we can ignore it and all its parents
|
||||
break
|
||||
}
|
||||
|
||||
imagesSeen[img.ID] = true
|
||||
imageListForThisTag = append(imageListForThisTag, img.ID)
|
||||
}
|
||||
|
||||
// reverse the image list for this tag (so the "most"-parent image is first)
|
||||
for i, j := 0, len(imageListForThisTag)-1; i < j; i, j = i+1, j-1 {
|
||||
imageListForThisTag[i], imageListForThisTag[j] = imageListForThisTag[j], imageListForThisTag[i]
|
||||
}
|
||||
|
||||
// append to main image list
|
||||
imageList = append(imageList, imageListForThisTag...)
|
||||
}
|
||||
if len(imageList) == 0 {
|
||||
return nil, nil, fmt.Errorf("No images found for the requested repository / tag")
|
||||
}
|
||||
logrus.Debugf("Image list: %v", imageList)
|
||||
logrus.Debugf("Tags by image: %v", tagsByImage)
|
||||
|
||||
return imageList, tagsByImage, nil
|
||||
}
|
||||
|
||||
// createImageIndex returns an index of an image's layer IDs and tags.
|
||||
func (s *TagStore) createImageIndex(images []string, tags map[string][]string) []*registry.ImgData {
|
||||
var imageIndex []*registry.ImgData
|
||||
for _, id := range images {
|
||||
if tags, hasTags := tags[id]; hasTags {
|
||||
// If an image has tags you must add an entry in the image index
|
||||
// for each tag
|
||||
for _, tag := range tags {
|
||||
imageIndex = append(imageIndex, ®istry.ImgData{
|
||||
ID: id,
|
||||
Tag: tag,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
// If the image does not have a tag it still needs to be sent to the
|
||||
// registry with an empty tag so that it is associated with the repository
|
||||
imageIndex = append(imageIndex, ®istry.ImgData{
|
||||
ID: id,
|
||||
Tag: "",
|
||||
})
|
||||
}
|
||||
return imageIndex
|
||||
}
|
||||
|
||||
type imagePushData struct {
|
||||
id string
|
||||
compatibilityID string
|
||||
endpoint string
|
||||
}
|
||||
|
||||
// lookupImageOnEndpoint checks the specified endpoint to see if an image exists
|
||||
// and if it is absent then it sends the image id to the channel to be pushed.
|
||||
func (p *v1Pusher) lookupImageOnEndpoint(wg *sync.WaitGroup, images chan imagePushData, imagesToPush chan string) {
|
||||
defer wg.Done()
|
||||
for image := range images {
|
||||
if err := p.session.LookupRemoteImage(image.compatibilityID, image.endpoint); err != nil {
|
||||
logrus.Errorf("Error in LookupRemoteImage: %s", err)
|
||||
imagesToPush <- image.id
|
||||
continue
|
||||
}
|
||||
p.out.Write(p.sf.FormatStatus("", "Image %s already pushed, skipping", stringid.TruncateID(image.id)))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *v1Pusher) pushImageToEndpoint(endpoint string, imageIDs []string, tags map[string][]string, repo *registry.RepositoryData) error {
|
||||
workerCount := len(imageIDs)
|
||||
// start a maximum of 5 workers to check if images exist on the specified endpoint.
|
||||
if workerCount > 5 {
|
||||
workerCount = 5
|
||||
}
|
||||
var (
|
||||
wg = &sync.WaitGroup{}
|
||||
imageData = make(chan imagePushData, workerCount*2)
|
||||
imagesToPush = make(chan string, workerCount*2)
|
||||
pushes = make(chan map[string]struct{}, 1)
|
||||
)
|
||||
for i := 0; i < workerCount; i++ {
|
||||
wg.Add(1)
|
||||
go p.lookupImageOnEndpoint(wg, imageData, imagesToPush)
|
||||
}
|
||||
// start a go routine that consumes the images to push
|
||||
go func() {
|
||||
shouldPush := make(map[string]struct{})
|
||||
for id := range imagesToPush {
|
||||
shouldPush[id] = struct{}{}
|
||||
}
|
||||
pushes <- shouldPush
|
||||
}()
|
||||
for _, id := range imageIDs {
|
||||
compatibilityID, err := p.getV1ID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageData <- imagePushData{
|
||||
id: id,
|
||||
compatibilityID: compatibilityID,
|
||||
endpoint: endpoint,
|
||||
}
|
||||
}
|
||||
// close the channel to notify the workers that there will be no more images to check.
|
||||
close(imageData)
|
||||
wg.Wait()
|
||||
close(imagesToPush)
|
||||
// wait for all the images that require pushes to be collected into a consumable map.
|
||||
shouldPush := <-pushes
|
||||
// finish by pushing any images and tags to the endpoint. The order that the images are pushed
|
||||
// is very important that is why we are still iterating over the ordered list of imageIDs.
|
||||
for _, id := range imageIDs {
|
||||
if _, push := shouldPush[id]; push {
|
||||
if _, err := p.pushImage(id, endpoint); err != nil {
|
||||
// FIXME: Continue on error?
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, tag := range tags[id] {
|
||||
p.out.Write(p.sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", stringid.TruncateID(id), endpoint+"repositories/"+p.repoInfo.RemoteName+"/tags/"+tag))
|
||||
compatibilityID, err := p.getV1ID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.session.PushRegistryTag(p.repoInfo.RemoteName, compatibilityID, tag, endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushRepository pushes layers that do not already exist on the registry.
|
||||
func (p *v1Pusher) pushRepository(tag string) error {
|
||||
logrus.Debugf("Local repo: %s", p.localRepo)
|
||||
p.out = ioutils.NewWriteFlusher(p.config.OutStream)
|
||||
imgList, tags, err := p.getImageList(tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.out.Write(p.sf.FormatStatus("", "Sending image list"))
|
||||
|
||||
imageIndex := p.createImageIndex(imgList, tags)
|
||||
logrus.Debugf("Preparing to push %s with the following images and tags", p.localRepo)
|
||||
for _, data := range imageIndex {
|
||||
logrus.Debugf("Pushing ID: %s with Tag: %s", data.ID, data.Tag)
|
||||
|
||||
// convert IDs to compatibilityIDs, imageIndex only used in registry calls
|
||||
data.ID, err = p.getV1ID(data.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, found := p.poolAdd("push", p.repoInfo.LocalName); found {
|
||||
return fmt.Errorf("push or pull %s is already in progress", p.repoInfo.LocalName)
|
||||
}
|
||||
defer p.poolRemove("push", p.repoInfo.LocalName)
|
||||
|
||||
// Register all the images in a repository with the registry
|
||||
// If an image is not in this list it will not be associated with the repository
|
||||
repoData, err := p.session.PushImageJSONIndex(p.repoInfo.RemoteName, imageIndex, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nTag := 1
|
||||
if tag == "" {
|
||||
nTag = len(p.localRepo)
|
||||
}
|
||||
p.out.Write(p.sf.FormatStatus("", "Pushing repository %s (%d tags)", p.repoInfo.CanonicalName, nTag))
|
||||
// push the repository to each of the endpoints only if it does not exist.
|
||||
for _, endpoint := range repoData.Endpoints {
|
||||
if err := p.pushImageToEndpoint(endpoint, imgList, tags, repoData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = p.session.PushImageJSONIndex(p.repoInfo.RemoteName, imageIndex, true, repoData.Endpoints)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *v1Pusher) pushImage(imgID, ep string) (checksum string, err error) {
|
||||
jsonRaw, err := p.getV1Config(imgID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Cannot retrieve the path for {%s}: %s", imgID, err)
|
||||
}
|
||||
p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgID), "Pushing", nil))
|
||||
|
||||
compatibilityID, err := p.getV1ID(imgID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// General rule is to use ID for graph accesses and compatibilityID for
|
||||
// calls to session.registry()
|
||||
imgData := ®istry.ImgData{
|
||||
ID: compatibilityID,
|
||||
}
|
||||
|
||||
// Send the json
|
||||
if err := p.session.PushImageJSONRegistry(imgData, jsonRaw, ep); err != nil {
|
||||
if err == registry.ErrAlreadyExists {
|
||||
p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgID), "Image already pushed, skipping", nil))
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
layerData, err := p.graph.tempLayerArchive(imgID, p.sf, p.out)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to generate layer archive: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(layerData.Name())
|
||||
|
||||
// Send the layer
|
||||
logrus.Debugf("rendered layer for %s of [%d] size", imgID, layerData.Size)
|
||||
|
||||
checksum, checksumPayload, err := p.session.PushImageLayerRegistry(imgData.ID,
|
||||
progressreader.New(progressreader.Config{
|
||||
In: layerData,
|
||||
Out: p.out,
|
||||
Formatter: p.sf,
|
||||
Size: layerData.Size,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(imgID),
|
||||
Action: "Pushing",
|
||||
}), ep, jsonRaw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
imgData.Checksum = checksum
|
||||
imgData.ChecksumPayload = checksumPayload
|
||||
// Send the checksum
|
||||
if err := p.session.PushImageChecksumRegistry(imgData, ep); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgID), "Image successfully pushed", nil))
|
||||
return imgData.Checksum, nil
|
||||
}
|
||||
|
||||
// getV1ID returns the compatibilityID for the ID in the graph. compatibilityID
|
||||
// is read from from the v1Compatibility config file in the disk.
|
||||
func (p *v1Pusher) getV1ID(id string) (string, error) {
|
||||
jsonData, err := p.getV1Config(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
img, err := image.NewImgJSON(jsonData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return img.ID, nil
|
||||
}
|
||||
|
||||
// getV1Config returns v1Compatibility config for the image in the graph. If
|
||||
// there is no v1Compatibility file on disk for the image
|
||||
func (p *v1Pusher) getV1Config(id string) ([]byte, error) {
|
||||
jsonData, err := p.graph.generateV1CompatibilityChain(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonData, nil
|
||||
}
|
397
graph/push_v2.go
397
graph/push_v2.go
|
@ -1,397 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/utils"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
const compressionBufSize = 32768
|
||||
|
||||
type v2Pusher struct {
|
||||
*TagStore
|
||||
endpoint registry.APIEndpoint
|
||||
localRepo repository
|
||||
repoInfo *registry.RepositoryInfo
|
||||
config *ImagePushConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
repo distribution.Repository
|
||||
|
||||
// layersPushed is the set of layers known to exist on the remote side.
|
||||
// This avoids redundant queries when pushing multiple tags that
|
||||
// involve the same layers.
|
||||
layersPushed map[digest.Digest]bool
|
||||
}
|
||||
|
||||
func (p *v2Pusher) Push() (fallback bool, err error) {
|
||||
p.repo, err = newV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "push", "pull")
|
||||
if err != nil {
|
||||
logrus.Debugf("Error getting v2 registry: %v", err)
|
||||
return true, err
|
||||
}
|
||||
return false, p.pushV2Repository(p.config.Tag)
|
||||
}
|
||||
|
||||
func (p *v2Pusher) getImageTags(askedTag string) ([]string, error) {
|
||||
logrus.Debugf("Checking %q against %#v", askedTag, p.localRepo)
|
||||
if len(askedTag) > 0 {
|
||||
if _, ok := p.localRepo[askedTag]; !ok || utils.DigestReference(askedTag) {
|
||||
return nil, fmt.Errorf("Tag does not exist for %s", askedTag)
|
||||
}
|
||||
return []string{askedTag}, nil
|
||||
}
|
||||
var tags []string
|
||||
for tag := range p.localRepo {
|
||||
if !utils.DigestReference(tag) {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (p *v2Pusher) pushV2Repository(tag string) error {
|
||||
localName := p.repoInfo.LocalName
|
||||
if _, found := p.poolAdd("push", localName); found {
|
||||
return fmt.Errorf("push or pull %s is already in progress", localName)
|
||||
}
|
||||
defer p.poolRemove("push", localName)
|
||||
|
||||
tags, err := p.getImageTags(tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting tags for %s: %s", localName, err)
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
return fmt.Errorf("no tags to push for %s", localName)
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if err := p.pushV2Tag(tag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *v2Pusher) pushV2Tag(tag string) error {
|
||||
logrus.Debugf("Pushing repository: %s:%s", p.repo.Name(), tag)
|
||||
|
||||
layerID, exists := p.localRepo[tag]
|
||||
if !exists {
|
||||
return fmt.Errorf("tag does not exist: %s", tag)
|
||||
}
|
||||
|
||||
layersSeen := make(map[string]bool)
|
||||
|
||||
layer, err := p.graph.Get(layerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := &schema1.Manifest{
|
||||
Versioned: manifest.Versioned{
|
||||
SchemaVersion: 1,
|
||||
},
|
||||
Name: p.repo.Name(),
|
||||
Tag: tag,
|
||||
Architecture: layer.Architecture,
|
||||
FSLayers: []schema1.FSLayer{},
|
||||
History: []schema1.History{},
|
||||
}
|
||||
|
||||
var metadata runconfig.Config
|
||||
if layer != nil && layer.Config != nil {
|
||||
metadata = *layer.Config
|
||||
}
|
||||
|
||||
out := p.config.OutStream
|
||||
|
||||
for ; layer != nil; layer, err = p.graph.GetParent(layer) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// break early if layer has already been seen in this image,
|
||||
// this prevents infinite loops on layers which loopback, this
|
||||
// cannot be prevented since layer IDs are not merkle hashes
|
||||
// TODO(dmcgowan): throw error if no valid use case is found
|
||||
if layersSeen[layer.ID] {
|
||||
break
|
||||
}
|
||||
|
||||
// Skip the base layer on Windows. This cannot be pushed.
|
||||
if allowBaseParentImage && layer.Parent == "" {
|
||||
break
|
||||
}
|
||||
|
||||
logrus.Debugf("Pushing layer: %s", layer.ID)
|
||||
|
||||
if layer.Config != nil && metadata.Image != layer.ID {
|
||||
if err := runconfig.Merge(&metadata, layer.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var exists bool
|
||||
dgst, err := p.graph.getLayerDigestWithLock(layer.ID)
|
||||
switch err {
|
||||
case nil:
|
||||
if p.layersPushed[dgst] {
|
||||
exists = true
|
||||
// break out of switch, it is already known that
|
||||
// the push is not needed and therefore doing a
|
||||
// stat is unnecessary
|
||||
break
|
||||
}
|
||||
_, err := p.repo.Blobs(context.Background()).Stat(context.Background(), dgst)
|
||||
switch err {
|
||||
case nil:
|
||||
exists = true
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(layer.ID), "Image already exists", nil))
|
||||
case distribution.ErrBlobUnknown:
|
||||
// nop
|
||||
default:
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(layer.ID), "Image push failed", nil))
|
||||
return err
|
||||
}
|
||||
case errDigestNotSet:
|
||||
// nop
|
||||
case digest.ErrDigestInvalidFormat, digest.ErrDigestUnsupported:
|
||||
return fmt.Errorf("error getting image checksum: %v", err)
|
||||
}
|
||||
|
||||
// if digest was empty or not saved, or if blob does not exist on the remote repository,
|
||||
// then fetch it.
|
||||
if !exists {
|
||||
var pushDigest digest.Digest
|
||||
if pushDigest, err = p.pushV2Image(p.repo.Blobs(context.Background()), layer); err != nil {
|
||||
return err
|
||||
}
|
||||
if dgst == "" {
|
||||
// Cache new checksum
|
||||
if err := p.graph.setLayerDigestWithLock(layer.ID, pushDigest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
dgst = pushDigest
|
||||
}
|
||||
|
||||
// read v1Compatibility config, generate new if needed
|
||||
jsonData, err := p.graph.generateV1CompatibilityChain(layer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.FSLayers = append(m.FSLayers, schema1.FSLayer{BlobSum: dgst})
|
||||
m.History = append(m.History, schema1.History{V1Compatibility: string(jsonData)})
|
||||
|
||||
layersSeen[layer.ID] = true
|
||||
p.layersPushed[dgst] = true
|
||||
}
|
||||
|
||||
// Fix parent chain if necessary
|
||||
if err = fixHistory(m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("Signed manifest for %s:%s using daemon's key: %s", p.repo.Name(), tag, p.trustKey.KeyID())
|
||||
signed, err := schema1.Sign(m, p.trustKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestDigest, manifestSize, err := digestFromManifest(signed, p.repo.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if manifestDigest != "" {
|
||||
out.Write(p.sf.FormatStatus("", "%s: digest: %s size: %d", tag, manifestDigest, manifestSize))
|
||||
}
|
||||
|
||||
manSvc, err := p.repo.Manifests(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return manSvc.Put(signed)
|
||||
}
|
||||
|
||||
// fixHistory makes sure that the manifest has parent IDs that are consistent
|
||||
// with its image IDs. Because local image IDs are generated from the
|
||||
// configuration and filesystem contents, but IDs in the manifest are preserved
|
||||
// from the original pull, it's possible to have inconsistencies where parent
|
||||
// IDs don't match up with the other IDs in the manifest. This happens in the
|
||||
// case where an engine pulls images where are identical except the IDs from the
|
||||
// manifest - the local ID will be the same, and one of the v1Compatibility
|
||||
// files gets discarded.
|
||||
func fixHistory(m *schema1.Manifest) error {
|
||||
var lastID string
|
||||
|
||||
for i := len(m.History) - 1; i >= 0; i-- {
|
||||
var historyEntry map[string]*json.RawMessage
|
||||
if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), &historyEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idJSON, present := historyEntry["id"]
|
||||
if !present || idJSON == nil {
|
||||
return errors.New("missing id key in v1compatibility file")
|
||||
}
|
||||
var id string
|
||||
if err := json.Unmarshal(*idJSON, &id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parentJSON, present := historyEntry["parent"]
|
||||
|
||||
if i == len(m.History)-1 {
|
||||
// The base layer must not reference a parent layer,
|
||||
// otherwise the manifest is incomplete. There is an
|
||||
// exception for Windows to handle base layers.
|
||||
if !allowBaseParentImage && present && parentJSON != nil {
|
||||
var parent string
|
||||
if err := json.Unmarshal(*parentJSON, &parent); err != nil {
|
||||
return err
|
||||
}
|
||||
if parent != "" {
|
||||
logrus.Debugf("parent id mismatch detected; fixing. parent reference: %s", parent)
|
||||
delete(historyEntry, "parent")
|
||||
fixedHistory, err := json.Marshal(historyEntry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.History[i].V1Compatibility = string(fixedHistory)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For all other layers, the parent ID should equal the
|
||||
// ID of the next item in the history list. If it
|
||||
// doesn't, fix it up (but preserve all other fields,
|
||||
// possibly including fields that aren't known to this
|
||||
// engine version).
|
||||
if !present || parentJSON == nil {
|
||||
return errors.New("missing parent key in v1compatibility file")
|
||||
}
|
||||
var parent string
|
||||
if err := json.Unmarshal(*parentJSON, &parent); err != nil {
|
||||
return err
|
||||
}
|
||||
if parent != lastID {
|
||||
logrus.Debugf("parent id mismatch detected; fixing. parent reference: %s actual id: %s", parent, id)
|
||||
historyEntry["parent"] = rawJSON(lastID)
|
||||
fixedHistory, err := json.Marshal(historyEntry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.History[i].V1Compatibility = string(fixedHistory)
|
||||
}
|
||||
}
|
||||
lastID = id
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rawJSON(value interface{}) *json.RawMessage {
|
||||
jsonval, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return (*json.RawMessage)(&jsonval)
|
||||
}
|
||||
|
||||
func (p *v2Pusher) pushV2Image(bs distribution.BlobService, img *image.Image) (digest.Digest, error) {
|
||||
out := p.config.OutStream
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Preparing", nil))
|
||||
|
||||
image, err := p.graph.Get(img.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
arch, err := p.graph.tarLayer(image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer arch.Close()
|
||||
|
||||
// Send the layer
|
||||
layerUpload, err := bs.Create(context.Background())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer layerUpload.Close()
|
||||
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: ioutil.NopCloser(arch), // we'll take care of close here.
|
||||
Out: out,
|
||||
Formatter: p.sf,
|
||||
|
||||
// TODO(stevvooe): This may cause a size reporting error. Try to get
|
||||
// this from tar-split or elsewhere. The main issue here is that we
|
||||
// don't want to buffer to disk *just* to calculate the size.
|
||||
Size: img.Size,
|
||||
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(img.ID),
|
||||
Action: "Pushing",
|
||||
})
|
||||
|
||||
digester := digest.Canonical.New()
|
||||
// HACK: The MultiWriter doesn't write directly to layerUpload because
|
||||
// we must make sure the ReadFrom is used, not Write. Using Write would
|
||||
// send a PATCH request for every Write call.
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
// Use a bufio.Writer to avoid excessive chunking in HTTP request.
|
||||
bufWriter := bufio.NewWriterSize(io.MultiWriter(pipeWriter, digester.Hash()), compressionBufSize)
|
||||
compressor := gzip.NewWriter(bufWriter)
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(compressor, reader)
|
||||
if err == nil {
|
||||
err = compressor.Close()
|
||||
}
|
||||
if err == nil {
|
||||
err = bufWriter.Flush()
|
||||
}
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
} else {
|
||||
pipeWriter.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Pushing", nil))
|
||||
nn, err := layerUpload.ReadFrom(pipeReader)
|
||||
pipeReader.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dgst := digester.Digest()
|
||||
if _, err := layerUpload.Commit(context.Background(), distribution.Descriptor{Digest: dgst}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
logrus.Debugf("uploaded layer %s (%s), %d bytes", img.ID, dgst, nn)
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Pushed", nil))
|
||||
|
||||
return dgst, nil
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
// Lookup looks up an image by name in a TagStore and returns it as an
|
||||
// ImageInspect structure.
|
||||
func (s *TagStore) Lookup(name string) (*types.ImageInspect, error) {
|
||||
image, err := s.LookupImage(name)
|
||||
if err != nil || image == nil {
|
||||
return nil, fmt.Errorf("No such image: %s", name)
|
||||
}
|
||||
|
||||
var repoTags = make([]string, 0)
|
||||
var repoDigests = make([]string, 0)
|
||||
|
||||
s.Lock()
|
||||
for repoName, repository := range s.Repositories {
|
||||
for ref, id := range repository {
|
||||
if id == image.ID {
|
||||
imgRef := utils.ImageReference(repoName, ref)
|
||||
if utils.DigestReference(ref) {
|
||||
repoDigests = append(repoDigests, imgRef)
|
||||
} else {
|
||||
repoTags = append(repoTags, imgRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.Unlock()
|
||||
|
||||
imageInspect := &types.ImageInspect{
|
||||
ID: image.ID,
|
||||
RepoTags: repoTags,
|
||||
RepoDigests: repoDigests,
|
||||
Parent: image.Parent,
|
||||
Comment: image.Comment,
|
||||
Created: image.Created.Format(time.RFC3339Nano),
|
||||
Container: image.Container,
|
||||
ContainerConfig: &image.ContainerConfig,
|
||||
DockerVersion: image.DockerVersion,
|
||||
Author: image.Author,
|
||||
Config: image.Config,
|
||||
Architecture: image.Architecture,
|
||||
Os: image.OS,
|
||||
Size: image.Size,
|
||||
VirtualSize: s.graph.getParentsSize(image) + image.Size,
|
||||
}
|
||||
|
||||
imageInspect.GraphDriver.Name = s.graph.driver.String()
|
||||
|
||||
graphDriverData, err := s.graph.driver.GetMetadata(image.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageInspect.GraphDriver.Data = graphDriverData
|
||||
return imageInspect, nil
|
||||
}
|
||||
|
||||
// imageTarLayer return the tarLayer of the image
|
||||
func (s *TagStore) imageTarLayer(name string, dest io.Writer) error {
|
||||
if image, err := s.LookupImage(name); err == nil && image != nil {
|
||||
// On Windows, the base layer cannot be exported
|
||||
if runtime.GOOS != "windows" || image.Parent != "" {
|
||||
|
||||
fs, err := s.graph.tarLayer(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
written, err := io.Copy(dest, fs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("rendered layer for %s of [%d] size", image.ID, written)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("No such image: %s", name)
|
||||
}
|
431
graph/tags.go
431
graph/tags.go
|
@ -1,431 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/daemon/events"
|
||||
"github.com/docker/docker/graph/tags"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/broadcaster"
|
||||
"github.com/docker/docker/pkg/parsers"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
// ErrNameIsNotExist returned when there is no image with requested name.
|
||||
var ErrNameIsNotExist = errors.New("image with specified name does not exist")
|
||||
|
||||
// TagStore manages repositories. It encompasses the Graph used for versioned
|
||||
// storage, as well as various services involved in pushing and pulling
|
||||
// repositories.
|
||||
type TagStore struct {
|
||||
path string
|
||||
graph *Graph
|
||||
// Repositories is a map of repositories, indexed by name.
|
||||
Repositories map[string]repository
|
||||
trustKey libtrust.PrivateKey
|
||||
sync.Mutex
|
||||
// FIXME: move push/pull-related fields
|
||||
// to a helper type
|
||||
pullingPool map[string]*broadcaster.Buffered
|
||||
pushingPool map[string]*broadcaster.Buffered
|
||||
registryService *registry.Service
|
||||
eventsService *events.Events
|
||||
}
|
||||
|
||||
// repository maps tags to image IDs.
|
||||
type repository map[string]string
|
||||
|
||||
// TagStoreConfig provides parameters for a new TagStore.
|
||||
type TagStoreConfig struct {
|
||||
// Graph is the versioned image store
|
||||
Graph *Graph
|
||||
// Key is the private key to use for signing manifests.
|
||||
Key libtrust.PrivateKey
|
||||
// Registry is the registry service to use for TLS configuration and
|
||||
// endpoint lookup.
|
||||
Registry *registry.Service
|
||||
// Events is the events service to use for logging.
|
||||
Events *events.Events
|
||||
}
|
||||
|
||||
// NewTagStore creates a new TagStore at specified path, using the parameters
|
||||
// and services provided in cfg.
|
||||
func NewTagStore(path string, cfg *TagStoreConfig) (*TagStore, error) {
|
||||
abspath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store := &TagStore{
|
||||
path: abspath,
|
||||
graph: cfg.Graph,
|
||||
trustKey: cfg.Key,
|
||||
Repositories: make(map[string]repository),
|
||||
pullingPool: make(map[string]*broadcaster.Buffered),
|
||||
pushingPool: make(map[string]*broadcaster.Buffered),
|
||||
registryService: cfg.Registry,
|
||||
eventsService: cfg.Events,
|
||||
}
|
||||
// Load the json file if it exists, otherwise create it.
|
||||
if err := store.reload(); os.IsNotExist(err) {
|
||||
if err := store.save(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (store *TagStore) save() error {
|
||||
// Store the json ball
|
||||
jsonData, err := json.Marshal(store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(store.path, jsonData, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *TagStore) reload() error {
|
||||
f, err := os.Open(store.path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if err := json.NewDecoder(f).Decode(&store); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupImage returns pointer to an Image struct corresponding to the given
|
||||
// name. The name can include an optional tag; otherwise the default tag will
|
||||
// be used.
|
||||
func (store *TagStore) LookupImage(name string) (*image.Image, error) {
|
||||
repoName, ref := parsers.ParseRepositoryTag(name)
|
||||
if ref == "" {
|
||||
ref = tags.DefaultTag
|
||||
}
|
||||
var (
|
||||
err error
|
||||
img *image.Image
|
||||
)
|
||||
|
||||
img, err = store.getImage(repoName, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if img != nil {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// name must be an image ID.
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
if img, err = store.graph.Get(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// GetID returns ID for image name.
|
||||
func (store *TagStore) GetID(name string) (string, error) {
|
||||
repoName, ref := parsers.ParseRepositoryTag(name)
|
||||
if ref == "" {
|
||||
ref = tags.DefaultTag
|
||||
}
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
repoName = registry.NormalizeLocalName(repoName)
|
||||
repo, ok := store.Repositories[repoName]
|
||||
if !ok {
|
||||
return "", ErrNameIsNotExist
|
||||
}
|
||||
id, ok := repo[ref]
|
||||
if !ok {
|
||||
return "", ErrNameIsNotExist
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ByID returns a reverse-lookup table of all the names which refer to each
|
||||
// image - e.g. {"43b5f19b10584": {"base:latest", "base:v1"}}
|
||||
func (store *TagStore) ByID() map[string][]string {
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
byID := make(map[string][]string)
|
||||
for repoName, repository := range store.Repositories {
|
||||
for tag, id := range repository {
|
||||
name := utils.ImageReference(repoName, tag)
|
||||
if _, exists := byID[id]; !exists {
|
||||
byID[id] = []string{name}
|
||||
} else {
|
||||
byID[id] = append(byID[id], name)
|
||||
sort.Strings(byID[id])
|
||||
}
|
||||
}
|
||||
}
|
||||
return byID
|
||||
}
|
||||
|
||||
// HasReferences returns whether or not the given image is referenced in one or
|
||||
// more repositories.
|
||||
func (store *TagStore) HasReferences(img *image.Image) bool {
|
||||
return len(store.ByID()[img.ID]) > 0
|
||||
}
|
||||
|
||||
// Delete deletes a repository or a specific tag. If ref is empty, the entire
|
||||
// repository named repoName will be deleted; otherwise only the tag named by
|
||||
// ref will be deleted.
|
||||
func (store *TagStore) Delete(repoName, ref string) (bool, error) {
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
deleted := false
|
||||
if err := store.reload(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if ref == "" {
|
||||
// Delete the whole repository.
|
||||
delete(store.Repositories, repoName)
|
||||
return true, store.save()
|
||||
}
|
||||
|
||||
repoRefs, exists := store.Repositories[repoName]
|
||||
|
||||
if !exists {
|
||||
return false, fmt.Errorf("No such repository: %s", repoName)
|
||||
}
|
||||
|
||||
if _, exists := repoRefs[ref]; exists {
|
||||
delete(repoRefs, ref)
|
||||
if len(repoRefs) == 0 {
|
||||
delete(store.Repositories, repoName)
|
||||
}
|
||||
deleted = true
|
||||
}
|
||||
|
||||
return deleted, store.save()
|
||||
}
|
||||
|
||||
// Tag creates a tag in the repository reponame, pointing to the image named
|
||||
// imageName. If force is true, an existing tag with the same name may be
|
||||
// overwritten.
|
||||
func (store *TagStore) Tag(repoName, tag, imageName string, force bool) error {
|
||||
return store.setLoad(repoName, tag, imageName, force, nil)
|
||||
}
|
||||
|
||||
// setLoad stores the image to the store.
|
||||
// If the imageName is already in the repo then a '-f' flag should be used to replace existing image.
|
||||
func (store *TagStore) setLoad(repoName, tag, imageName string, force bool, out io.Writer) error {
|
||||
img, err := store.LookupImage(imageName)
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag == "" {
|
||||
tag = tags.DefaultTag
|
||||
}
|
||||
if err := validateRepoName(repoName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tags.ValidateTagName(tag); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := store.reload(); err != nil {
|
||||
return err
|
||||
}
|
||||
var repo repository
|
||||
repoName = registry.NormalizeLocalName(repoName)
|
||||
if r, exists := store.Repositories[repoName]; exists {
|
||||
repo = r
|
||||
if old, exists := store.Repositories[repoName][tag]; exists {
|
||||
|
||||
if !force {
|
||||
return fmt.Errorf("Conflict: Tag %s:%s is already set to image %s, if you want to replace it, please use -f option", repoName, tag, old[:12])
|
||||
}
|
||||
|
||||
if old != img.ID && out != nil {
|
||||
|
||||
fmt.Fprintf(out, "The image %s:%s already exists, renaming the old one with ID %s to empty string\n", repoName, tag, old[:12])
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
repo = make(map[string]string)
|
||||
store.Repositories[repoName] = repo
|
||||
}
|
||||
repo[tag] = img.ID
|
||||
return store.save()
|
||||
}
|
||||
|
||||
// setDigest creates a digest reference to an image ID.
|
||||
func (store *TagStore) setDigest(repoName, digest, imageName string) error {
|
||||
img, err := store.LookupImage(imageName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateRepoName(repoName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateDigest(digest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
if err := store.reload(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoName = registry.NormalizeLocalName(repoName)
|
||||
repoRefs, exists := store.Repositories[repoName]
|
||||
if !exists {
|
||||
repoRefs = repository{}
|
||||
store.Repositories[repoName] = repoRefs
|
||||
} else if oldID, exists := repoRefs[digest]; exists && oldID != img.ID {
|
||||
return fmt.Errorf("Conflict: Digest %s is already set to image %s", digest, oldID)
|
||||
}
|
||||
|
||||
repoRefs[digest] = img.ID
|
||||
return store.save()
|
||||
}
|
||||
|
||||
// get returns the repository tag/image map for a given repository.
|
||||
func (store *TagStore) get(repoName string) (repository, error) {
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
if err := store.reload(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repoName = registry.NormalizeLocalName(repoName)
|
||||
if r, exists := store.Repositories[repoName]; exists {
|
||||
return r, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// getImage returns a pointer to an Image structure describing the image
|
||||
// referred to by refOrID inside repository repoName.
|
||||
func (store *TagStore) getImage(repoName, refOrID string) (*image.Image, error) {
|
||||
repo, err := store.get(repoName)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if repo == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
if imgID, exists := repo[refOrID]; exists {
|
||||
return store.graph.Get(imgID)
|
||||
}
|
||||
|
||||
// If no matching tag is found, search through images for a matching image id
|
||||
// iff it looks like a short ID or would look like a short ID
|
||||
if stringid.IsShortID(stringid.TruncateID(refOrID)) {
|
||||
for _, revision := range repo {
|
||||
if strings.HasPrefix(revision, refOrID) {
|
||||
return store.graph.Get(revision)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// validateRepoName validates the name of a repository.
|
||||
func validateRepoName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("Repository name can't be empty")
|
||||
}
|
||||
if name == "scratch" {
|
||||
return fmt.Errorf("'scratch' is a reserved name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDigest(dgst string) error {
|
||||
if dgst == "" {
|
||||
return errors.New("digest can't be empty")
|
||||
}
|
||||
if _, err := digest.ParseDigest(dgst); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// poolAdd checks if a push or pull is already running, and returns
|
||||
// (broadcaster, true) if a running operation is found. Otherwise, it creates a
|
||||
// new one and returns (broadcaster, false).
|
||||
func (store *TagStore) poolAdd(kind, key string) (*broadcaster.Buffered, bool) {
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
|
||||
if p, exists := store.pullingPool[key]; exists {
|
||||
return p, true
|
||||
}
|
||||
if p, exists := store.pushingPool[key]; exists {
|
||||
return p, true
|
||||
}
|
||||
|
||||
broadcaster := broadcaster.NewBuffered()
|
||||
|
||||
switch kind {
|
||||
case "pull":
|
||||
store.pullingPool[key] = broadcaster
|
||||
case "push":
|
||||
store.pushingPool[key] = broadcaster
|
||||
default:
|
||||
panic("Unknown pool type")
|
||||
}
|
||||
|
||||
return broadcaster, false
|
||||
}
|
||||
|
||||
func (store *TagStore) poolRemoveWithError(kind, key string, broadcasterResult error) error {
|
||||
store.Lock()
|
||||
defer store.Unlock()
|
||||
switch kind {
|
||||
case "pull":
|
||||
if broadcaster, exists := store.pullingPool[key]; exists {
|
||||
broadcaster.CloseWithError(broadcasterResult)
|
||||
delete(store.pullingPool, key)
|
||||
}
|
||||
case "push":
|
||||
if broadcaster, exists := store.pushingPool[key]; exists {
|
||||
broadcaster.CloseWithError(broadcasterResult)
|
||||
delete(store.pushingPool, key)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Unknown pool type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *TagStore) poolRemove(kind, key string) error {
|
||||
return store.poolRemoveWithError(kind, key, nil)
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package tags
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
)
|
||||
|
||||
// DefaultTag defines the default tag used when performing images related actions and no tag string is specified
|
||||
const DefaultTag = "latest"
|
||||
|
||||
var anchoredTagRegexp = regexp.MustCompile(`^` + reference.TagRegexp.String() + `$`)
|
||||
|
||||
// ErrTagInvalidFormat is returned if tag is invalid.
|
||||
type ErrTagInvalidFormat struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (e ErrTagInvalidFormat) Error() string {
|
||||
return fmt.Sprintf("Illegal tag name (%s): only [A-Za-z0-9_.-] are allowed ('.' and '-' are NOT allowed in the initial), minimum 1, maximum 128 in length", e.name)
|
||||
}
|
||||
|
||||
// ValidateTagName validates the name of a tag.
|
||||
// It returns an error if the given name is an emtpy string.
|
||||
// If name is not valid, it returns ErrTagInvalidFormat
|
||||
func ValidateTagName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("tag name can't be empty")
|
||||
}
|
||||
|
||||
if !anchoredTagRegexp.MatchString(name) {
|
||||
return ErrTagInvalidFormat{name}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package tags
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidTagName(t *testing.T) {
|
||||
validTags := []string{"9", "foo", "foo-test", "bar.baz.boo"}
|
||||
for _, tag := range validTags {
|
||||
if err := ValidateTagName(tag); err != nil {
|
||||
t.Errorf("'%s' should've been a valid tag", tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidTagName(t *testing.T) {
|
||||
inValidTags := []string{"-9", ".foo", "-test", ".", "-"}
|
||||
for _, tag := range inValidTags {
|
||||
if err := ValidateTagName(tag); err == nil {
|
||||
t.Errorf("'%s' should've been an invalid tag", tag)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,205 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/daemon/events"
|
||||
"github.com/docker/docker/daemon/graphdriver"
|
||||
_ "github.com/docker/docker/daemon/graphdriver/vfs" // import the vfs driver so it is used in the tests
|
||||
"github.com/docker/docker/graph/tags"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
testOfficialImageName = "myapp"
|
||||
testOfficialImageID = "1a2d3c4d4e5fa2d2a21acea242a5e2345d3aefc3e7dfa2a2a2a21a2a2ad2d234"
|
||||
testOfficialImageIDShort = "1a2d3c4d4e5f"
|
||||
testPrivateImageName = "127.0.0.1:8000/privateapp"
|
||||
testPrivateImageID = "5bc255f8699e4ee89ac4469266c3d11515da88fdcbde45d7b069b636ff4efd81"
|
||||
testPrivateImageIDShort = "5bc255f8699e"
|
||||
testPrivateImageDigest = "sha256:bc8813ea7b3603864987522f02a76101c17ad122e1c46d790efc0fca78ca7bfb"
|
||||
testPrivateImageTag = "sometag"
|
||||
)
|
||||
|
||||
func fakeTar() (io.Reader, error) {
|
||||
uid := os.Getuid()
|
||||
gid := os.Getgid()
|
||||
|
||||
content := []byte("Hello world!\n")
|
||||
buf := new(bytes.Buffer)
|
||||
tw := tar.NewWriter(buf)
|
||||
for _, name := range []string{"/etc/postgres/postgres.conf", "/etc/passwd", "/var/log/postgres/postgres.conf"} {
|
||||
hdr := new(tar.Header)
|
||||
|
||||
// Leaving these fields blank requires root privileges
|
||||
hdr.Uid = uid
|
||||
hdr.Gid = gid
|
||||
|
||||
hdr.Size = int64(len(content))
|
||||
hdr.Name = name
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tw.Write([]byte(content))
|
||||
}
|
||||
tw.Close()
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func mkTestTagStore(root string, t *testing.T) *TagStore {
|
||||
driver, err := graphdriver.New(root, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
graph, err := NewGraph(root, driver, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tagCfg := &TagStoreConfig{
|
||||
Graph: graph,
|
||||
Events: events.New(),
|
||||
}
|
||||
store, err := NewTagStore(path.Join(root, "tags"), tagCfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
officialArchive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
img := &image.Image{ID: testOfficialImageID}
|
||||
if err := graph.Register(v1Descriptor{img}, officialArchive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Tag(testOfficialImageName, "", testOfficialImageID, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
privateArchive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
img = &image.Image{ID: testPrivateImageID}
|
||||
if err := graph.Register(v1Descriptor{img}, privateArchive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Tag(testPrivateImageName, "", testPrivateImageID, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.setDigest(testPrivateImageName, testPrivateImageDigest, testPrivateImageID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func TestLookupImage(t *testing.T) {
|
||||
tmp, err := utils.TestDirectory("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
store := mkTestTagStore(tmp, t)
|
||||
defer store.graph.driver.Cleanup()
|
||||
|
||||
officialLookups := []string{
|
||||
testOfficialImageID,
|
||||
testOfficialImageIDShort,
|
||||
testOfficialImageName + ":" + testOfficialImageID,
|
||||
testOfficialImageName + ":" + testOfficialImageIDShort,
|
||||
testOfficialImageName,
|
||||
testOfficialImageName + ":" + tags.DefaultTag,
|
||||
"docker.io/" + testOfficialImageName,
|
||||
"docker.io/" + testOfficialImageName + ":" + tags.DefaultTag,
|
||||
"index.docker.io/" + testOfficialImageName,
|
||||
"index.docker.io/" + testOfficialImageName + ":" + tags.DefaultTag,
|
||||
"library/" + testOfficialImageName,
|
||||
"library/" + testOfficialImageName + ":" + tags.DefaultTag,
|
||||
"docker.io/library/" + testOfficialImageName,
|
||||
"docker.io/library/" + testOfficialImageName + ":" + tags.DefaultTag,
|
||||
"index.docker.io/library/" + testOfficialImageName,
|
||||
"index.docker.io/library/" + testOfficialImageName + ":" + tags.DefaultTag,
|
||||
}
|
||||
|
||||
privateLookups := []string{
|
||||
testPrivateImageID,
|
||||
testPrivateImageIDShort,
|
||||
testPrivateImageName + ":" + testPrivateImageID,
|
||||
testPrivateImageName + ":" + testPrivateImageIDShort,
|
||||
testPrivateImageName,
|
||||
testPrivateImageName + ":" + tags.DefaultTag,
|
||||
}
|
||||
|
||||
invalidLookups := []string{
|
||||
testOfficialImageName + ":" + "fail",
|
||||
"fail:fail",
|
||||
}
|
||||
|
||||
digestLookups := []string{
|
||||
testPrivateImageName + "@" + testPrivateImageDigest,
|
||||
}
|
||||
|
||||
for _, name := range officialLookups {
|
||||
if img, err := store.LookupImage(name); err != nil {
|
||||
t.Errorf("Error looking up %s: %s", name, err)
|
||||
} else if img == nil {
|
||||
t.Errorf("Expected 1 image, none found: %s", name)
|
||||
} else if img.ID != testOfficialImageID {
|
||||
t.Errorf("Expected ID '%s' found '%s'", testOfficialImageID, img.ID)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range privateLookups {
|
||||
if img, err := store.LookupImage(name); err != nil {
|
||||
t.Errorf("Error looking up %s: %s", name, err)
|
||||
} else if img == nil {
|
||||
t.Errorf("Expected 1 image, none found: %s", name)
|
||||
} else if img.ID != testPrivateImageID {
|
||||
t.Errorf("Expected ID '%s' found '%s'", testPrivateImageID, img.ID)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range invalidLookups {
|
||||
if img, err := store.LookupImage(name); err == nil {
|
||||
t.Errorf("Expected error, none found: %s", name)
|
||||
} else if img != nil {
|
||||
t.Errorf("Expected 0 image, 1 found: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range digestLookups {
|
||||
if img, err := store.LookupImage(name); err != nil {
|
||||
t.Errorf("Error looking up %s: %s", name, err)
|
||||
} else if img == nil {
|
||||
t.Errorf("Expected 1 image, none found: %s", name)
|
||||
} else if img.ID != testPrivateImageID {
|
||||
t.Errorf("Expected ID '%s' found '%s'", testPrivateImageID, img.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDigest(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expectError bool
|
||||
}{
|
||||
{"", true},
|
||||
{"latest", true},
|
||||
{"sha256:b", false},
|
||||
{"tarsum+v1+sha256:bY852-_.+=", false},
|
||||
{"#$%#$^:$%^#$%", true},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
err := validateDigest(test.input)
|
||||
gotError := err != nil
|
||||
if e, a := test.expectError, gotError; e != a {
|
||||
t.Errorf("%d: with input %s, expected error=%t, got %t: %s", i, test.input, test.expectError, gotError, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
sha256:f2722a8ec6926e02fa9f2674072cbc2a25cf0f449f27350f613cd843b02c9105
|
|
@ -1 +0,0 @@
|
|||
{"architecture":"amd64","config":{"Hostname":"fb1f7270da95","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["foo=bar"],"Cmd":null,"Image":"361a94d06b2b781b2f1ee6c72e1cbbfbbd032a103e26a3db75b431743829ae4f","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"fb1f7270da9519308361b99dc8e0d30f12c24dfd28537c2337ece995ac853a16","container_config":{"Hostname":"fb1f7270da95","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["foo=bar"],"Cmd":["/bin/sh","-c","#(nop) ADD file:11998b2a4d664a75cd0c3f4e4cb1837434e0f997ba157a0ac1d3c68a07aa2f4f in /"],"Image":"361a94d06b2b781b2f1ee6c72e1cbbfbbd032a103e26a3db75b431743829ae4f","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2015-09-08T21:30:30.807853054Z","docker_version":"1.9.0-dev","layer_id":"sha256:31176893850e05d308cdbfef88877e460d50c8063883fb13eb5753097da6422a","os":"linux","parent_id":"sha256:ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02"}
|
|
@ -1 +0,0 @@
|
|||
sha256:31176893850e05d308cdbfef88877e460d50c8063883fb13eb5753097da6422a
|
|
@ -1 +0,0 @@
|
|||
sha256:ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02
|
|
@ -1 +0,0 @@
|
|||
{"id":"8dfb96b5d09e6cf6f376d81f1e2770ee5ede309f9bd9e079688c9782649ab326","parent":"361a94d06b2b781b2f1ee6c72e1cbbfbbd032a103e26a3db75b431743829ae4f","created":"2015-09-08T21:30:30.807853054Z","container":"fb1f7270da9519308361b99dc8e0d30f12c24dfd28537c2337ece995ac853a16","container_config":{"Hostname":"fb1f7270da95","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["foo=bar"],"Cmd":["/bin/sh","-c","#(nop) ADD file:11998b2a4d664a75cd0c3f4e4cb1837434e0f997ba157a0ac1d3c68a07aa2f4f in /"],"Image":"361a94d06b2b781b2f1ee6c72e1cbbfbbd032a103e26a3db75b431743829ae4f","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"docker_version":"1.9.0-dev","config":{"Hostname":"fb1f7270da95","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["foo=bar"],"Cmd":null,"Image":"361a94d06b2b781b2f1ee6c72e1cbbfbbd032a103e26a3db75b431743829ae4f","Volumes":null,"VolumeDriver":"","WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"architecture":"amd64","os":"linux"}
|
|
@ -1 +0,0 @@
|
|||
sha256:fd6ebfedda8ea140a9380767e15bd32c6e899303cfe34bc4580c931f2f816f89
|
|
@ -1,2 +0,0 @@
|
|||
{"architecture":"amd64","config":{"Hostname":"03797203757d","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":null,"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"OnBuild":[],"Labels":{}},"container":"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253","container_config":{"Hostname":"03797203757d","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":["/bin/sh","-c","#(nop) ENTRYPOINT [\"/go/bin/dnsdock\"]"],"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"OnBuild":[],"Labels":{}},"created":"2015-08-19T16:49:11.368300679Z","docker_version":"1.6.2","layer_id":"sha256:31176893850e05d308cdbfef88877e460d50c8063883fb13eb5753097da6422a","os":"linux","parent_id":"sha256:ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02"}
|
||||
|
|
@ -1 +0,0 @@
|
|||
sha256:31176893850e05d308cdbfef88877e460d50c8063883fb13eb5753097da6422a
|
|
@ -1 +0,0 @@
|
|||
sha256:ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02
|
|
@ -1 +0,0 @@
|
|||
{"id":"3b38edc92eb7c074812e217b41a6ade66888531009d6286a6f5f36a06f9841b9","parent":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","created":"2015-08-19T16:49:11.368300679Z","container":"d91be3479d5b1e84b0c00d18eea9dc777ca0ad166d51174b24283e2e6f104253","container_config":{"Hostname":"03797203757d","Domainname":"","User":"","Memory":0,"MemorySwap":0,"CpuShares":0,"Cpuset":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"PortSpecs":null,"ExposedPorts":null,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":["/bin/sh","-c","#(nop) ENTRYPOINT [\"/go/bin/dnsdock\"]"],"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"NetworkDisabled":false,"MacAddress":"","OnBuild":[],"Labels":{}},"docker_version":"1.6.2","config":{"Hostname":"03797203757d","Domainname":"","User":"","Memory":0,"MemorySwap":0,"CpuShares":0,"Cpuset":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"PortSpecs":null,"ExposedPorts":null,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/go/bin:/usr/src/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","GOLANG_VERSION=1.4.1","GOPATH=/go"],"Cmd":null,"Image":"ec3025ca8cc9bcab039e193e20ec647c2da3c53a74020f2ba611601f9b2c6c02","Volumes":null,"WorkingDir":"/go","Entrypoint":["/go/bin/dnsdock"],"NetworkDisabled":false,"MacAddress":"","OnBuild":[],"Labels":{}},"architecture":"amd64","os":"linux","Size":0}
|
192
image/fs.go
Normal file
192
image/fs.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
)
|
||||
|
||||
// IDWalkFunc is function called by StoreBackend.Walk
|
||||
type IDWalkFunc func(id ID) error
|
||||
|
||||
// StoreBackend provides interface for image.Store persistence
|
||||
type StoreBackend interface {
|
||||
Walk(f IDWalkFunc) error
|
||||
Get(id ID) ([]byte, error)
|
||||
Set(data []byte) (ID, error)
|
||||
Delete(id ID) error
|
||||
SetMetadata(id ID, key string, data []byte) error
|
||||
GetMetadata(id ID, key string) ([]byte, error)
|
||||
DeleteMetadata(id ID, key string) error
|
||||
}
|
||||
|
||||
// fs implements StoreBackend using the filesystem.
|
||||
type fs struct {
|
||||
sync.RWMutex
|
||||
root string
|
||||
}
|
||||
|
||||
const (
|
||||
contentDirName = "content"
|
||||
metadataDirName = "metadata"
|
||||
)
|
||||
|
||||
// NewFSStoreBackend returns new filesystem based backend for image.Store
|
||||
func NewFSStoreBackend(root string) (StoreBackend, error) {
|
||||
return newFSStore(root)
|
||||
}
|
||||
|
||||
func newFSStore(root string) (*fs, error) {
|
||||
s := &fs{
|
||||
root: root,
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, contentDirName, string(digest.Canonical)), 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(root, metadataDirName, string(digest.Canonical)), 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *fs) contentFile(id ID) string {
|
||||
dgst := digest.Digest(id)
|
||||
return filepath.Join(s.root, contentDirName, string(dgst.Algorithm()), dgst.Hex())
|
||||
}
|
||||
|
||||
func (s *fs) metadataDir(id ID) string {
|
||||
dgst := digest.Digest(id)
|
||||
return filepath.Join(s.root, metadataDirName, string(dgst.Algorithm()), dgst.Hex())
|
||||
}
|
||||
|
||||
// Walk calls the supplied callback for each image ID in the storage backend.
|
||||
func (s *fs) Walk(f IDWalkFunc) error {
|
||||
// Only Canonical digest (sha256) is currently supported
|
||||
s.RLock()
|
||||
dir, err := ioutil.ReadDir(filepath.Join(s.root, contentDirName, string(digest.Canonical)))
|
||||
s.RUnlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range dir {
|
||||
dgst := digest.NewDigestFromHex(string(digest.Canonical), v.Name())
|
||||
if err := dgst.Validate(); err != nil {
|
||||
logrus.Debugf("Skipping invalid digest %s: %s", dgst, err)
|
||||
continue
|
||||
}
|
||||
if err := f(ID(dgst)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the content stored under a given ID.
|
||||
func (s *fs) Get(id ID) ([]byte, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.get(id)
|
||||
}
|
||||
|
||||
func (s *fs) get(id ID) ([]byte, error) {
|
||||
content, err := ioutil.ReadFile(s.contentFile(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// todo: maybe optional
|
||||
validated, err := digest.FromBytes(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ID(validated) != id {
|
||||
return nil, fmt.Errorf("failed to verify image: %v", id)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Set stores content under a given ID.
|
||||
func (s *fs) Set(data []byte) (ID, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if len(data) == 0 {
|
||||
return "", fmt.Errorf("Invalid empty data")
|
||||
}
|
||||
|
||||
dgst, err := digest.FromBytes(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := ID(dgst)
|
||||
filePath := s.contentFile(id)
|
||||
tempFilePath := s.contentFile(id) + ".tmp"
|
||||
if err := ioutil.WriteFile(tempFilePath, data, 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.Rename(tempFilePath, filePath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Delete removes content and metadata files associated with the ID.
|
||||
func (s *fs) Delete(id ID) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
if err := os.RemoveAll(s.metadataDir(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Remove(s.contentFile(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMetadata sets metadata for a given ID. It fails if there's no base file.
|
||||
func (s *fs) SetMetadata(id ID, key string, data []byte) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if _, err := s.get(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseDir := filepath.Join(s.metadataDir(id))
|
||||
if err := os.MkdirAll(baseDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
filePath := filepath.Join(s.metadataDir(id), key)
|
||||
tempFilePath := filePath + ".tmp"
|
||||
if err := ioutil.WriteFile(tempFilePath, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tempFilePath, filePath)
|
||||
}
|
||||
|
||||
// GetMetadata returns metadata for a given ID.
|
||||
func (s *fs) GetMetadata(id ID, key string) ([]byte, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
if _, err := s.get(id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ioutil.ReadFile(filepath.Join(s.metadataDir(id), key))
|
||||
}
|
||||
|
||||
// DeleteMetadata removes the metadata associated with an ID.
|
||||
func (s *fs) DeleteMetadata(id ID, key string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
return os.RemoveAll(filepath.Join(s.metadataDir(id), key))
|
||||
}
|
391
image/fs_test.go
Normal file
391
image/fs_test.go
Normal file
|
@ -0,0 +1,391 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
)
|
||||
|
||||
func TestFSGetSet(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testGetSet(t, fs)
|
||||
}
|
||||
|
||||
func TestFSGetInvalidData(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := fs.Set([]byte("foobar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dgst := digest.Digest(id)
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpdir, contentDirName, string(dgst.Algorithm()), dgst.Hex()), []byte("foobar2"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = fs.Get(id)
|
||||
if err == nil {
|
||||
t.Fatal("Expected get to fail after data modification.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSInvalidSet(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := digest.FromBytes([]byte("foobar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.Mkdir(filepath.Join(tmpdir, contentDirName, string(id.Algorithm()), id.Hex()), 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = fs.Set([]byte("foobar"))
|
||||
if err == nil {
|
||||
t.Fatal("Expecting error from invalid filesystem data.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSInvalidRoot(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
tcases := []struct {
|
||||
root, invalidFile string
|
||||
}{
|
||||
{"root", "root"},
|
||||
{"root", "root/content"},
|
||||
{"root", "root/metadata"},
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
root := filepath.Join(tmpdir, tc.root)
|
||||
filePath := filepath.Join(tmpdir, tc.invalidFile)
|
||||
err := os.MkdirAll(filepath.Dir(filePath), 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
_, err = NewFSStoreBackend(root)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error from root %q and invlid file %q", tc.root, tc.invalidFile)
|
||||
}
|
||||
|
||||
os.RemoveAll(root)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testMetadataGetSet(t *testing.T, store StoreBackend) {
|
||||
id, err := store.Set([]byte("foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := store.Set([]byte("bar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tcases := []struct {
|
||||
id ID
|
||||
key string
|
||||
value []byte
|
||||
}{
|
||||
{id, "tkey", []byte("tval1")},
|
||||
{id, "tkey2", []byte("tval2")},
|
||||
{id2, "tkey", []byte("tval3")},
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
err = store.SetMetadata(tc.id, tc.key, tc.value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual, err := store.GetMetadata(tc.id, tc.key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Compare(actual, tc.value) != 0 {
|
||||
t.Fatalf("Metadata expected %q, got %q", tc.value, actual)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = store.GetMetadata(id2, "tkey2")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for getting metadata for unknown key")
|
||||
}
|
||||
|
||||
id3, err := digest.FromBytes([]byte("baz"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.SetMetadata(ID(id3), "tkey", []byte("tval"))
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for setting metadata for unknown ID.")
|
||||
}
|
||||
|
||||
_, err = store.GetMetadata(ID(id3), "tkey")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for getting metadata for unknown ID.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSMetadataGetSet(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testMetadataGetSet(t, fs)
|
||||
}
|
||||
|
||||
func TestFSDelete(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testDelete(t, fs)
|
||||
}
|
||||
|
||||
func TestFSWalker(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testWalker(t, fs)
|
||||
}
|
||||
|
||||
func TestFSInvalidWalker(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "images-fs-store")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
fs, err := NewFSStoreBackend(tmpdir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fooID, err := fs.Set([]byte("foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpdir, contentDirName, "sha256/foobar"), []byte("foobar"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n := 0
|
||||
err = fs.Walk(func(id ID) error {
|
||||
if id != fooID {
|
||||
t.Fatalf("Invalid walker ID %q, expected %q", id, fooID)
|
||||
}
|
||||
n++
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Invalid data should not have caused walker error, got %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("Expected 1 walk initialization, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetSet(t *testing.T, store StoreBackend) {
|
||||
type tcase struct {
|
||||
input []byte
|
||||
expected ID
|
||||
}
|
||||
tcases := []tcase{
|
||||
{[]byte("foobar"), ID("sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2")},
|
||||
}
|
||||
|
||||
randomInput := make([]byte, 8*1024)
|
||||
_, err := rand.Read(randomInput)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// skipping use of digest pkg because its used by the imlementation
|
||||
h := sha256.New()
|
||||
_, err = h.Write(randomInput)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tcases = append(tcases, tcase{
|
||||
input: randomInput,
|
||||
expected: ID("sha256:" + hex.EncodeToString(h.Sum(nil))),
|
||||
})
|
||||
|
||||
for _, tc := range tcases {
|
||||
id, err := store.Set([]byte(tc.input))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id != tc.expected {
|
||||
t.Fatalf("Expected ID %q, got %q", tc.expected, id)
|
||||
}
|
||||
}
|
||||
|
||||
for _, emptyData := range [][]byte{nil, {}} {
|
||||
_, err := store.Set(emptyData)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for nil input.")
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
data, err := store.Get(tc.expected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Compare(data, tc.input) != 0 {
|
||||
t.Fatalf("Expected data %q, got %q", tc.input, data)
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range []ID{"foobar:abc", "sha256:abc", "sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2a"} {
|
||||
_, err := store.Get(key)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for ID %q.", key)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testDelete(t *testing.T, store StoreBackend) {
|
||||
id, err := store.Set([]byte("foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := store.Set([]byte("bar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Delete(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = store.Get(id)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected getting deleted item %q to fail", id)
|
||||
}
|
||||
_, err = store.Get(id2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = store.Delete(id2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = store.Get(id2)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected getting deleted item %q to fail", id2)
|
||||
}
|
||||
}
|
||||
|
||||
func testWalker(t *testing.T, store StoreBackend) {
|
||||
id, err := store.Set([]byte("foo"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := store.Set([]byte("bar"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tcases := make(map[ID]struct{})
|
||||
tcases[id] = struct{}{}
|
||||
tcases[id2] = struct{}{}
|
||||
n := 0
|
||||
err = store.Walk(func(id ID) error {
|
||||
delete(tcases, id)
|
||||
n++
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if n != 2 {
|
||||
t.Fatalf("Expected 2 walk initializations, got %d", n)
|
||||
}
|
||||
if len(tcases) != 0 {
|
||||
t.Fatalf("Expected empty unwalked set, got %+v", tcases)
|
||||
}
|
||||
|
||||
// stop on error
|
||||
tcases = make(map[ID]struct{})
|
||||
tcases[id] = struct{}{}
|
||||
err = store.Walk(func(id ID) error {
|
||||
return errors.New("")
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("Exected error from walker.")
|
||||
}
|
||||
}
|
163
image/image.go
163
image/image.go
|
@ -2,36 +2,23 @@ package image
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
derr "github.com/docker/docker/errors"
|
||||
"github.com/docker/docker/pkg/version"
|
||||
"github.com/docker/docker/runconfig"
|
||||
)
|
||||
|
||||
var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
|
||||
// ID is the content-addressable ID of an image.
|
||||
type ID digest.Digest
|
||||
|
||||
// noFallbackMinVersion is the minimum version for which v1compatibility
|
||||
// information will not be marshaled through the Image struct to remove
|
||||
// blank fields.
|
||||
var noFallbackMinVersion = version.Version("1.8.3")
|
||||
|
||||
// Descriptor provides the information necessary to register an image in
|
||||
// the graph.
|
||||
type Descriptor interface {
|
||||
ID() string
|
||||
Parent() string
|
||||
MarshalConfig() ([]byte, error)
|
||||
func (id ID) String() string {
|
||||
return digest.Digest(id).String()
|
||||
}
|
||||
|
||||
// Image stores the image configuration.
|
||||
// All fields in this struct must be marked `omitempty` to keep getting
|
||||
// predictable hashes from the old `v1Compatibility` configuration.
|
||||
type Image struct {
|
||||
// V1Image stores the V1 image configuration.
|
||||
type V1Image struct {
|
||||
// ID a unique 64 character identifier of the image
|
||||
ID string `json:"id,omitempty"`
|
||||
// Parent id of the image
|
||||
|
@ -55,95 +42,87 @@ type Image struct {
|
|||
// OS is the operating system used to build and run the image
|
||||
OS string `json:"os,omitempty"`
|
||||
// Size is the total size of the image including all layers it is composed of
|
||||
Size int64 `json:",omitempty"` // capitalized for backwards compatibility
|
||||
// ParentID specifies the strong, content address of the parent configuration.
|
||||
ParentID digest.Digest `json:"parent_id,omitempty"`
|
||||
// LayerID provides the content address of the associated layer.
|
||||
LayerID digest.Digest `json:"layer_id,omitempty"`
|
||||
Size int64 `json:",omitempty"`
|
||||
}
|
||||
|
||||
// NewImgJSON creates an Image configuration from json.
|
||||
func NewImgJSON(src []byte) (*Image, error) {
|
||||
ret := &Image{}
|
||||
// Image stores the image configuration
|
||||
type Image struct {
|
||||
V1Image
|
||||
Parent ID `json:"parent,omitempty"`
|
||||
RootFS *RootFS `json:"rootfs,omitempty"`
|
||||
History []History `json:"history,omitempty"`
|
||||
|
||||
// FIXME: Is there a cleaner way to "purify" the input json?
|
||||
if err := json.Unmarshal(src, ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
// rawJSON caches the immutable JSON associated with this image.
|
||||
rawJSON []byte
|
||||
|
||||
// computedID is the ID computed from the hash of the image config.
|
||||
// Not to be confused with the legacy V1 ID in V1Image.
|
||||
computedID ID
|
||||
}
|
||||
|
||||
// ValidateID checks whether an ID string is a valid image ID.
|
||||
func ValidateID(id string) error {
|
||||
if ok := validHex.MatchString(id); !ok {
|
||||
return derr.ErrorCodeInvalidImageID.WithArgs(id)
|
||||
}
|
||||
return nil
|
||||
// RawJSON returns the immutable JSON associated with the image.
|
||||
func (img *Image) RawJSON() []byte {
|
||||
return img.rawJSON
|
||||
}
|
||||
|
||||
// MakeImageConfig returns immutable configuration JSON for image based on the
|
||||
// v1Compatibility object, layer digest and parent StrongID. SHA256() of this
|
||||
// config is the new image ID (strongID).
|
||||
func MakeImageConfig(v1Compatibility []byte, layerID, parentID digest.Digest) ([]byte, error) {
|
||||
// ID returns the image's content-addressable ID.
|
||||
func (img *Image) ID() ID {
|
||||
return img.computedID
|
||||
}
|
||||
|
||||
// Detect images created after 1.8.3
|
||||
img, err := NewImgJSON(v1Compatibility)
|
||||
// MarshalJSON serializes the image to JSON. It sorts the top-level keys so
|
||||
// that JSON that's been manipulated by a push/pull cycle with a legacy
|
||||
// registry won't end up with a different key order.
|
||||
func (img *Image) MarshalJSON() ([]byte, error) {
|
||||
type MarshalImage Image
|
||||
|
||||
pass1, err := json.Marshal(MarshalImage(*img))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
useFallback := version.Version(img.DockerVersion).LessThan(noFallbackMinVersion)
|
||||
|
||||
if useFallback {
|
||||
// Fallback for pre-1.8.3. Calculate base config based on Image struct
|
||||
// so that fields with default values added by Docker will use same ID
|
||||
logrus.Debugf("Using fallback hash for %v", layerID)
|
||||
|
||||
v1Compatibility, err = json.Marshal(img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var c map[string]*json.RawMessage
|
||||
if err := json.Unmarshal(v1Compatibility, &c); err != nil {
|
||||
if err := json.Unmarshal(pass1, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := layerID.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid layerID: %v", err)
|
||||
}
|
||||
|
||||
c["layer_id"] = rawJSON(layerID)
|
||||
|
||||
if parentID != "" {
|
||||
if err := parentID.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid parentID %v", err)
|
||||
}
|
||||
c["parent_id"] = rawJSON(parentID)
|
||||
}
|
||||
|
||||
delete(c, "id")
|
||||
delete(c, "parent")
|
||||
delete(c, "Size") // Size is calculated from data on disk and is inconsitent
|
||||
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// StrongID returns image ID for the config JSON.
|
||||
func StrongID(configJSON []byte) (digest.Digest, error) {
|
||||
digester := digest.Canonical.New()
|
||||
if _, err := digester.Hash().Write(configJSON); err != nil {
|
||||
return "", err
|
||||
}
|
||||
dgst := digester.Digest()
|
||||
logrus.Debugf("H(%v) = %v", string(configJSON), dgst)
|
||||
return dgst, nil
|
||||
// History stores build commands that were used to create an image
|
||||
type History struct {
|
||||
// Created timestamp for build point
|
||||
Created time.Time `json:"created"`
|
||||
// Author of the build point
|
||||
Author string `json:"author,omitempty"`
|
||||
// CreatedBy keeps the Dockerfile command used while building image.
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
// Comment is custom mesage set by the user when creating the image.
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// EmptyLayer is set to true if this history item did not generate a
|
||||
// layer. Otherwise, the history item is associated with the next
|
||||
// layer in the RootFS section.
|
||||
EmptyLayer bool `json:"empty_layer,omitempty"`
|
||||
}
|
||||
|
||||
func rawJSON(value interface{}) *json.RawMessage {
|
||||
jsonval, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return (*json.RawMessage)(&jsonval)
|
||||
// Exporter provides interface for exporting and importing images
|
||||
type Exporter interface {
|
||||
Load(io.ReadCloser, io.Writer) error
|
||||
// TODO: Load(net.Context, io.ReadCloser, <- chan StatusMessage) error
|
||||
Save([]string, io.Writer) error
|
||||
}
|
||||
|
||||
// NewFromJSON creates an Image configuration from json.
|
||||
func NewFromJSON(src []byte) (*Image, error) {
|
||||
img := &Image{}
|
||||
|
||||
if err := json.Unmarshal(src, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if img.RootFS == nil {
|
||||
return nil, errors.New("Invalid image JSON, no RootFS key.")
|
||||
}
|
||||
|
||||
img.rawJSON = src
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue