Add initial Architectures support (and the beginnings of a "put-shared" subcommand for bringing all the SharedTags and arch-specific images together with manifest lists/indexes)

This commit is contained in:
Tianon Gravi 2017-02-15 12:36:07 -08:00
parent 362855e06f
commit fdf3501d49
21 changed files with 954 additions and 101 deletions

View File

@ -81,7 +81,7 @@ func cmdBuild(c *cli.Context) error {
return cli.NewMultiError(fmt.Errorf(`failed fetching git repo for %q (tags %q)`, r.RepoName, entry.TagsString()), err)
}
archive, err := gitArchive(commit, entry.Directory)
archive, err := gitArchive(commit, entry.ArchDirectory(arch))
if err != nil {
return cli.NewMultiError(fmt.Errorf(`failed generating git archive for %q (tags %q)`, r.RepoName, entry.TagsString()), err)
}

View File

@ -0,0 +1,46 @@
package main
import (
"fmt"
"os"
"path"
"strings"
"github.com/codegangsta/cli"
)
func cmdPutShared(c *cli.Context) error {
repos, err := repos(c.Bool("all"), c.Args()...)
if err != nil {
return cli.NewMultiError(fmt.Errorf(`failed gathering repo list`), err)
}
namespace := c.String("namespace")
if namespace == "" {
return fmt.Errorf(`"--namespace" is a required flag for "put-shared"`)
}
fmt.Fprintf(os.Stderr, "warning: this subcommand is still a big WIP -- it doesn't do anything yet!\n")
for _, repo := range repos {
r, err := fetch(repo)
if err != nil {
return cli.NewMultiError(fmt.Errorf(`failed fetching repo %q`, repo), err)
}
// TODO handle all multi-architecture tags first (regardless of whether they have SharedTags)
targetRepo := path.Join(namespace, r.RepoName)
for _, group := range r.Manifest.GetSharedTagGroups() {
// TODO build up a YAML file
entryTags := []string{}
for _, entry := range group.Entries {
entryTags = append(entryTags, entry.Tags[0])
}
fmt.Printf("Putting %s (tags %s) <= %s\n", targetRepo, strings.Join(group.SharedTags, ", "), strings.Join(entryTags, ", "))
}
}
return nil
}

View File

@ -18,6 +18,8 @@ type FlagsConfigEntry struct {
Commands []string `delim:"," strip:"\n\r\t "`
// TODO arch namespace mappings (for intermediate pushing before put-shared, and for put-shared to pull from to join together in one big happy family)
Library string
Cache string
Debug string
@ -26,6 +28,7 @@ type FlagsConfigEntry struct {
BuildOrder string
Pull string
Arch string
Constraints []string `delim:"," strip:"\n\r\t "`
ExclusiveConstraints string
ApplyConstraints string
@ -55,6 +58,9 @@ func (dst *FlagsConfigEntry) Apply(src FlagsConfigEntry) {
if src.Pull != "" {
dst.Pull = src.Pull
}
if src.Arch != "" {
dst.Arch = src.Arch
}
if len(src.Constraints) > 0 {
dst.Constraints = src.Constraints[:]
}
@ -73,6 +79,7 @@ func (config FlagsConfigEntry) Vars() map[string]map[string]interface{} {
"cache": config.Cache,
"debug": config.Debug,
"arch": config.Arch,
"constraint": config.Constraints,
"exclusive-constraints": config.ExclusiveConstraints,
},

View File

@ -24,7 +24,7 @@ func (r Repo) DockerFrom(entry *manifest.Manifest2822Entry) (string, error) {
return "", err
}
dockerfileFile := path.Join(entry.Directory, "Dockerfile")
dockerfileFile := path.Join(entry.ArchDirectory(arch), "Dockerfile")
cacheKey := strings.Join([]string{
commit,
@ -130,9 +130,9 @@ func (r Repo) dockerBuildUniqueBits(entry *manifest.Manifest2822Entry) ([]string
dockerFromIdCache[from] = fromId
}
return []string{
entry.GitRepo,
entry.GitCommit,
entry.Directory,
entry.ArchGitRepo(arch),
entry.ArchGitCommit(arch),
entry.ArchDirectory(arch),
fromId,
}, nil
}

View File

@ -102,12 +102,12 @@ var gitRepoCache = map[string]string{}
func (r Repo) fetchGitRepo(entry *manifest.Manifest2822Entry) (string, error) {
cacheKey := strings.Join([]string{
entry.GitRepo,
entry.GitFetch,
entry.GitCommit,
entry.ArchGitRepo(arch),
entry.ArchGitFetch(arch),
entry.ArchGitCommit(arch),
}, "\n")
if commit, ok := gitRepoCache[cacheKey]; ok {
entry.GitCommit = commit
entry.SetGitCommit(arch, commit)
return commit, nil
}
@ -116,27 +116,27 @@ func (r Repo) fetchGitRepo(entry *manifest.Manifest2822Entry) (string, error) {
return "", err
}
if manifest.GitCommitRegex.MatchString(entry.GitCommit) {
commit, err := getGitCommit(entry.GitCommit)
if manifest.GitCommitRegex.MatchString(entry.ArchGitCommit(arch)) {
commit, err := getGitCommit(entry.ArchGitCommit(arch))
if err == nil {
gitRepoCache[cacheKey] = commit
entry.GitCommit = commit
entry.SetGitCommit(arch, commit)
return commit, nil
}
}
fetchString := entry.GitFetch + ":"
if entry.GitCommit == "FETCH_HEAD" {
fetchString := entry.ArchGitFetch(arch) + ":"
if entry.ArchGitCommit(arch) == "FETCH_HEAD" {
// fetch remote tag references to a local tag ref so that we can cache them and not re-fetch every time
localRef := "refs/tags/" + gitNormalizeForTagUsage(cacheKey)
commit, err := getGitCommit(localRef)
if err == nil {
gitRepoCache[cacheKey] = commit
entry.GitCommit = commit
entry.SetGitCommit(arch, commit)
return commit, nil
}
fetchString += localRef
} else if entry.GitFetch == manifest.DefaultLineBasedFetch {
} else if entry.ArchGitFetch(arch) == manifest.DefaultLineBasedFetch {
// backwards compat (see manifest/line-based.go in go-dockerlibrary)
refBase := "refs/remotes"
refBaseDir := filepath.Join(gitCache(), refBase)
@ -154,17 +154,17 @@ func (r Repo) fetchGitRepo(entry *manifest.Manifest2822Entry) (string, error) {
// we create a temporary remote dir so that we can clean it up completely afterwards
}
if strings.HasPrefix(entry.GitRepo, "git://github.com/") {
fmt.Fprintf(os.Stderr, "warning: insecure protocol git:// detected: %s\n", entry.GitRepo)
entry.GitRepo = strings.Replace(entry.GitRepo, "git://", "https://", 1)
if strings.HasPrefix(entry.ArchGitRepo(arch), "git://github.com/") {
fmt.Fprintf(os.Stderr, "warning: insecure protocol git:// detected: %s\n", entry.ArchGitRepo(arch))
entry.SetGitRepo(arch, strings.Replace(entry.ArchGitRepo(arch), "git://", "https://", 1))
}
_, err = git("fetch", "--quiet", "--no-tags", entry.GitRepo, fetchString)
_, err = git("fetch", "--quiet", "--no-tags", entry.ArchGitRepo(arch), fetchString)
if err != nil {
return "", err
}
commit, err := getGitCommit(entry.GitCommit)
commit, err := getGitCommit(entry.ArchGitCommit(arch))
if err != nil {
return "", err
}
@ -175,6 +175,6 @@ func (r Repo) fetchGitRepo(entry *manifest.Manifest2822Entry) (string, error) {
}
gitRepoCache[cacheKey] = commit
entry.GitCommit = commit
entry.SetGitCommit(arch, commit)
return commit, nil
}

View File

@ -6,6 +6,8 @@ import (
"path/filepath"
"github.com/codegangsta/cli"
"github.com/docker-library/go-dockerlibrary/manifest"
)
// TODO somewhere, ensure that the Docker engine we're talking to is API version 1.22+ (Docker 1.10+)
@ -18,6 +20,7 @@ var (
defaultLibrary string
defaultCache string
arch string
constraints []string
exclusiveConstraints bool
@ -27,6 +30,7 @@ var (
// separated so that FlagsConfig.ApplyTo can access them
flagEnvVars = map[string]string{
"debug": "BASHBREW_DEBUG",
"arch": "BASHBREW_ARCH",
"config": "BASHBREW_CONFIG",
"library": "BASHBREW_LIBRARY",
"cache": "BASHBREW_CACHE",
@ -72,6 +76,12 @@ func main() {
Usage: "do not apply any sorting, even via --build-order",
},
cli.StringFlag{
Name: "arch",
Value: manifest.DefaultArchitecture,
EnvVar: flagEnvVars["arch"],
Usage: "the current platform architecture",
},
cli.StringSliceFlag{
Name: "constraint",
Usage: "build constraints (see Constraints in Manifest2822Entry)",
@ -127,6 +137,7 @@ func main() {
debugFlag = c.GlobalBool("debug")
noSortFlag = c.GlobalBool("no-sort")
arch = c.GlobalString("arch")
constraints = c.GlobalStringSlice("constraint")
exclusiveConstraints = c.GlobalBool("exclusive-constraints")
@ -222,7 +233,18 @@ func main() {
Before: subcommandBeforeFactory("push"),
Action: cmdPush,
},
{
Name: "put-shared",
Usage: `updated shared tags in the registry`,
Flags: []cli.Flag{
commonFlags["all"],
commonFlags["namespace"],
},
Before: subcommandBeforeFactory("put-shared"),
Action: cmdPutShared,
},
// TODO --depth flag for children and parents
{
Name: "children",
Aliases: []string{

View File

@ -79,6 +79,15 @@ var haveOutputSkippedMessage = map[string]bool{}
func (r Repo) SkipConstraints(entry manifest.Manifest2822Entry) bool {
repoTag := r.RepoName + ":" + entry.Tags[0]
// TODO decide if "arch" and "constraints" should be handled separately (but probably not)
if !entry.HasArchitecture(arch) {
if !haveOutputSkippedMessage[repoTag] {
fmt.Fprintf(os.Stderr, "skipping %q (due to architecture %q; only %q supported)\n", repoTag, arch, entry.ArchitecturesString())
haveOutputSkippedMessage[repoTag] = true
}
return true
}
if len(entry.Constraints) == 0 {
if exclusiveConstraints {
if !haveOutputSkippedMessage[repoTag] {

2
go/vendor/manifest vendored
View File

@ -10,7 +10,7 @@
{
"importpath": "github.com/docker-library/go-dockerlibrary",
"repository": "https://github.com/docker-library/go-dockerlibrary",
"revision": "08ef5a968ebdd83dcc42998a96b6528837b55273",
"revision": "663a091da13fc848e27a16048fb39c4e4067056e",
"branch": "master"
},
{

View File

@ -0,0 +1,5 @@
# `import "github.com/docker-library/go-dockerlibrary/manifest"`
[![Travis Build Status](https://travis-ci.org/docker-library/go-dockerlibrary.svg?branch=master)](https://travis-ci.org/docker-library/go-dockerlibrary) [![GoDoc](https://godoc.org/github.com/docker-library/go-dockerlibrary?status.svg)](https://godoc.org/github.com/docker-library/go-dockerlibrary) [![codecov](https://codecov.io/gh/docker-library/go-dockerlibrary/branch/master/graph/badge.svg)](https://codecov.io/gh/docker-library/go-dockerlibrary)
This package contains the core parsing elements of [the `bashbrew` tool used by the Docker Official Images](https://github.com/docker-library/official-images/tree/master/bashbrew).

View File

@ -1,67 +0,0 @@
// +build ignore
package main
import (
"bufio"
"fmt"
"strings"
"github.com/docker-library/go-dockerlibrary/manifest"
)
func main() {
// TODO comment parsing
man, err := manifest.Parse(bufio.NewReader(strings.NewReader(`# RFC 2822
# I LOVE CAKE
Maintainers: InfoSiftr <github@infosiftr.com> (@infosiftr),
Johan Euphrosine <proppy@google.com> (@proppy)
GitRepo: https://github.com/docker-library/golang.git
GitFetch: refs/heads/master
# hi
# blasphemer
# Go 1.6
Tags: 1.6.1, 1.6, 1, latest
GitCommit: 0ce80411b9f41e9c3a21fc0a1bffba6ae761825a
Directory: 1.6
# Go 1.5
Tags: 1.5.3
GitCommit: d7e2a8d90a9b8f5dfd5bcd428e0c33b68c40cc19
Directory: 1.5
Tags: 1.5
GitCommit: d7e2a8d90a9b8f5dfd5bcd428e0c33b68c40cc19
Directory: 1.5
`)))
if err != nil {
panic(err)
}
fmt.Printf("-------------\n2822:\n%s\n", man)
man, err = manifest.Parse(bufio.NewReader(strings.NewReader(`
# first set
a: b@c d
e: b@c d
# second set
f: g@h
i: g@h j
`)))
if err != nil {
panic(err)
}
fmt.Printf("-------------\nline-based:\n%v\n", man)
}

View File

@ -0,0 +1,192 @@
package manifest_test
import (
"bufio"
"fmt"
"strings"
"github.com/docker-library/go-dockerlibrary/manifest"
)
func Example() {
man, err := manifest.Parse(bufio.NewReader(strings.NewReader(`# RFC 2822
# I LOVE CAKE
Maintainers: InfoSiftr <github@infosiftr.com> (@infosiftr),
Johan Euphrosine <proppy@google.com> (@proppy)
GitFetch: refs/heads/master
GitRepo: https://github.com/docker-library/golang.git
SharedTags: latest
arm64v8-GitRepo: https://github.com/docker-library/golang.git
Architectures: amd64
# hi
# blasphemer
# Go 1.6
Tags: 1.6.1, 1.6, 1
arm64v8-GitRepo: https://github.com/docker-library/golang.git
Directory: 1.6
GitCommit: 0ce80411b9f41e9c3a21fc0a1bffba6ae761825a
Constraints: some-random-build-server
# Go 1.5
Tags: 1.5.3
GitCommit: d7e2a8d90a9b8f5dfd5bcd428e0c33b68c40cc19
SharedTags: 1.5.3-debian, 1.5-debian
Directory: 1.5
s390x-GitCommit: b6c460e7cd79b595267870a98013ec3078b490df
i386-GitFetch: refs/heads/i386
ppc64le-Directory: 1.5/ppc64le
Tags: 1.5
SharedTags: 1.5-debian
GitCommit: d7e2a8d90a9b8f5dfd5bcd428e0c33b68c40cc19
Directory: 1.5
s390x-GitCommit: b6c460e7cd79b595267870a98013ec3078b490df
i386-GitFetch: refs/heads/i386
ppc64le-Directory: 1.5/ppc64le
SharedTags: raspbian
GitCommit: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef
Tags: raspbian-s390x
Architectures: s390x
`)))
if err != nil {
panic(err)
}
fmt.Printf("-------------\n2822:\n%s\n", man)
fmt.Printf("\nShared Tag Groups:\n")
for _, group := range man.GetSharedTagGroups() {
fmt.Printf("\n - %s\n", strings.Join(group.SharedTags, ", "))
for _, entry := range group.Entries {
fmt.Printf(" - %s\n", entry.TagsString())
}
}
fmt.Printf("\n")
man, err = manifest.Parse(bufio.NewReader(strings.NewReader(`
# maintainer: InfoSiftr <github@infosiftr.com> (@infosiftr)
# maintainer: John Smith <jsmith@example.com> (@example-jsmith)
# first set
a: b@c d
e: b@c d
# second set
f: g@h
i: g@h j
`)))
if err != nil {
panic(err)
}
fmt.Printf("-------------\nline-based:\n%v\n", man)
// Output:
// -------------
// 2822:
// Maintainers: InfoSiftr <github@infosiftr.com> (@infosiftr), Johan Euphrosine <proppy@google.com> (@proppy)
// SharedTags: latest
// GitRepo: https://github.com/docker-library/golang.git
// arm64v8-GitRepo: https://github.com/docker-library/golang.git
//
// Tags: 1.6.1, 1.6, 1
// GitCommit: 0ce80411b9f41e9c3a21fc0a1bffba6ae761825a
// Directory: 1.6
// Constraints: some-random-build-server
//
// Tags: 1.5.3, 1.5
// SharedTags: 1.5.3-debian, 1.5-debian
// GitCommit: d7e2a8d90a9b8f5dfd5bcd428e0c33b68c40cc19
// Directory: 1.5
// i386-GitFetch: refs/heads/i386
// ppc64le-Directory: 1.5/ppc64le
// s390x-GitCommit: b6c460e7cd79b595267870a98013ec3078b490df
//
// Tags: raspbian-s390x
// SharedTags: raspbian
// Architectures: s390x
// GitCommit: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef
//
// Shared Tag Groups:
//
// - latest
// - 1.6.1, 1.6, 1
//
// - 1.5.3-debian, 1.5-debian
// - 1.5.3, 1.5
//
// - raspbian
// - raspbian-s390x
//
// -------------
// line-based:
// Maintainers: InfoSiftr <github@infosiftr.com> (@infosiftr), John Smith <jsmith@example.com> (@example-jsmith)
// GitFetch: refs/heads/*
//
// Tags: a, e
// GitRepo: b
// GitCommit: c
// Directory: d
//
// Tags: f
// GitRepo: g
// GitFetch: refs/tags/h
// GitCommit: FETCH_HEAD
//
// Tags: i
// GitRepo: g
// GitFetch: refs/tags/h
// GitCommit: FETCH_HEAD
// Directory: j
}
func ExampleFetch_local() {
repoName, tagName, man, err := manifest.Fetch("testdata", "bash:4.4")
if err != nil {
panic(err)
}
fmt.Printf("%s:%s\n\n", repoName, tagName)
fmt.Println(man.GetTag(tagName).ClearDefaults(manifest.DefaultManifestEntry).String())
// Output:
// bash:4.4
//
// Maintainers: Tianon Gravi <admwiggin@gmail.com> (@tianon)
// Tags: 4.4.12, 4.4, 4, latest
// GitRepo: https://github.com/tianon/docker-bash.git
// GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e
// Directory: 4.4
}
func ExampleFetch_remote() {
repoName, tagName, man, err := manifest.Fetch("/home/jsmith/docker/official-images/library", "https://github.com/docker-library/official-images/raw/1a3c4cd6d5cd53bd538a6f56a69f94c5b35325a7/library/bash:4.4")
if err != nil {
panic(err)
}
fmt.Printf("%s:%s\n\n", repoName, tagName)
fmt.Println(man.GetTag(tagName).ClearDefaults(manifest.DefaultManifestEntry).String())
// Output:
// bash:4.4
//
// Maintainers: Tianon Gravi <admwiggin@gmail.com> (@tianon)
// Tags: 4.4.12, 4.4, 4, latest
// GitRepo: https://github.com/tianon/docker-bash.git
// GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e
// Directory: 4.4
}

View File

@ -0,0 +1,20 @@
package manifest_test
import (
"strings"
"testing"
"github.com/docker-library/go-dockerlibrary/manifest"
)
func TestParseError(t *testing.T) {
invalidManifest := `this is just completely bogus and invalid no matter how you slice it`
man, err := manifest.Parse(strings.NewReader(invalidManifest))
if err == nil {
t.Errorf("Expected error, got valid manifest instead:\n%s", man)
}
if !strings.HasPrefix(err.Error(), "cannot parse manifest in either format:") {
t.Errorf("Unexpected error: %v", err)
}
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io"
"regexp"
"sort"
"strings"
"github.com/docker-library/go-dockerlibrary/pkg/stripper"
@ -27,25 +28,37 @@ type Manifest2822Entry struct {
Maintainers []string `delim:"," strip:"\n\r\t "`
Tags []string `delim:"," strip:"\n\r\t "`
Tags []string `delim:"," strip:"\n\r\t "`
SharedTags []string `delim:"," strip:"\n\r\t "`
Architectures []string `delim:"," strip:"\n\r\t "`
GitRepo string
GitFetch string
GitCommit string
Directory string
// architecture-specific versions of the above fields are in Paragraph.Values as ARCH-Field, ala s390x-Directory
Constraints []string `delim:"," strip:"\n\r\t "`
}
var DefaultManifestEntry = Manifest2822Entry{
GitFetch: "refs/heads/master",
Directory: ".",
}
var (
DefaultArchitecture = "amd64"
DefaultManifestEntry = Manifest2822Entry{
Architectures: []string{DefaultArchitecture},
GitFetch: "refs/heads/master",
Directory: ".",
}
)
func (entry Manifest2822Entry) Clone() Manifest2822Entry {
// SLICES! grr
entry.Maintainers = append([]string{}, entry.Maintainers...)
entry.Tags = append([]string{}, entry.Tags...)
entry.SharedTags = append([]string{}, entry.SharedTags...)
entry.Architectures = append([]string{}, entry.Architectures...)
entry.Constraints = append([]string{}, entry.Constraints...)
return entry
}
@ -60,23 +73,60 @@ func (entry Manifest2822Entry) TagsString() string {
return strings.Join(entry.Tags, StringSeparator2822)
}
func (entry Manifest2822Entry) SharedTagsString() string {
return strings.Join(entry.SharedTags, StringSeparator2822)
}
func (entry Manifest2822Entry) ArchitecturesString() string {
return strings.Join(entry.Architectures, StringSeparator2822)
}
func (entry Manifest2822Entry) ConstraintsString() string {
return strings.Join(entry.Constraints, StringSeparator2822)
}
// if this method returns "true", then a.Tags and b.Tags can safely be combined (for the purposes of building)
func (a Manifest2822Entry) SameBuildArtifacts(b Manifest2822Entry) bool {
return a.GitRepo == b.GitRepo && a.GitFetch == b.GitFetch && a.GitCommit == b.GitCommit && a.Directory == b.Directory && a.ConstraintsString() == b.ConstraintsString()
// check xxxarch-GitRepo, etc. fields for sameness first
for _, key := range append(a.archFields(), b.archFields()...) {
if a.Paragraph.Values[key] != b.Paragraph.Values[key] {
return false
}
}
return a.ArchitecturesString() == b.ArchitecturesString() && a.GitRepo == b.GitRepo && a.GitFetch == b.GitFetch && a.GitCommit == b.GitCommit && a.Directory == b.Directory && a.ConstraintsString() == b.ConstraintsString()
}
func isArchField(field string) bool {
return strings.HasSuffix(field, "-GitRepo") || strings.HasSuffix(field, "-GitFetch") || strings.HasSuffix(field, "-GitCommit") || strings.HasSuffix(field, "-Directory")
}
// returns a list of architecture-specific fields in an Entry
func (entry Manifest2822Entry) archFields() []string {
ret := []string{}
for key, val := range entry.Paragraph.Values {
if isArchField(key) && val != "" {
ret = append(ret, key)
}
}
return ret
}
// returns a new Entry with any of the values that are equal to the values in "defaults" cleared
func (entry Manifest2822Entry) ClearDefaults(defaults Manifest2822Entry) Manifest2822Entry {
entry = entry.Clone() // make absolutely certain we have a deep clone
if entry.MaintainersString() == defaults.MaintainersString() {
entry.Maintainers = nil
}
if entry.TagsString() == defaults.TagsString() {
entry.Tags = nil
}
if entry.SharedTagsString() == defaults.SharedTagsString() {
entry.SharedTags = nil
}
if entry.ArchitecturesString() == defaults.ArchitecturesString() {
entry.Architectures = nil
}
if entry.GitRepo == defaults.GitRepo {
entry.GitRepo = ""
}
@ -89,6 +139,11 @@ func (entry Manifest2822Entry) ClearDefaults(defaults Manifest2822Entry) Manifes
if entry.Directory == defaults.Directory {
entry.Directory = ""
}
for _, key := range defaults.archFields() {
if defaults.Paragraph.Values[key] == entry.Paragraph.Values[key] {
delete(entry.Paragraph.Values, key)
}
}
if entry.ConstraintsString() == defaults.ConstraintsString() {
entry.Constraints = nil
}
@ -103,6 +158,12 @@ func (entry Manifest2822Entry) String() string {
if str := entry.TagsString(); str != "" {
ret = append(ret, "Tags: "+str)
}
if str := entry.SharedTagsString(); str != "" {
ret = append(ret, "SharedTags: "+str)
}
if str := entry.ArchitecturesString(); str != "" {
ret = append(ret, "Architectures: "+str)
}
if str := entry.GitRepo; str != "" {
ret = append(ret, "GitRepo: "+str)
}
@ -115,6 +176,11 @@ func (entry Manifest2822Entry) String() string {
if str := entry.Directory; str != "" {
ret = append(ret, "Directory: "+str)
}
archFields := entry.archFields()
sort.Strings(archFields) // consistent ordering
for _, key := range archFields {
ret = append(ret, key+": "+entry.Paragraph.Values[key])
}
if str := entry.ConstraintsString(); str != "" {
ret = append(ret, "Constraints: "+str)
}
@ -136,6 +202,48 @@ func (manifest Manifest2822) String() string {
return strings.Join(ret, "\n\n")
}
func (entry *Manifest2822Entry) SetGitRepo(arch string, repo string) {
if entry.Paragraph.Values == nil {
entry.Paragraph.Values = map[string]string{}
}
entry.Paragraph.Values[arch+"-GitRepo"] = repo
}
func (entry Manifest2822Entry) ArchGitRepo(arch string) string {
if val, ok := entry.Paragraph.Values[arch+"-GitRepo"]; ok && val != "" {
return val
}
return entry.GitRepo
}
func (entry Manifest2822Entry) ArchGitFetch(arch string) string {
if val, ok := entry.Paragraph.Values[arch+"-GitFetch"]; ok && val != "" {
return val
}
return entry.GitFetch
}
func (entry *Manifest2822Entry) SetGitCommit(arch string, commit string) {
if entry.Paragraph.Values == nil {
entry.Paragraph.Values = map[string]string{}
}
entry.Paragraph.Values[arch+"-GitCommit"] = commit
}
func (entry Manifest2822Entry) ArchGitCommit(arch string) string {
if val, ok := entry.Paragraph.Values[arch+"-GitCommit"]; ok && val != "" {
return val
}
return entry.GitCommit
}
func (entry Manifest2822Entry) ArchDirectory(arch string) string {
if val, ok := entry.Paragraph.Values[arch+"-Directory"]; ok && val != "" {
return val
}
return entry.Directory
}
func (entry Manifest2822Entry) HasTag(tag string) bool {
for _, existingTag := range entry.Tags {
if tag == existingTag {
@ -145,6 +253,26 @@ func (entry Manifest2822Entry) HasTag(tag string) bool {
return false
}
// HasSharedTag returns true if the given tag exists in entry.SharedTags.
func (entry Manifest2822Entry) HasSharedTag(tag string) bool {
for _, existingTag := range entry.SharedTags {
if tag == existingTag {
return true
}
}
return false
}
// HasArchitecture returns true if the given architecture exists in entry.Architectures
func (entry Manifest2822Entry) HasArchitecture(arch string) bool {
for _, existingArch := range entry.Architectures {
if arch == existingArch {
return true
}
}
return false
}
func (manifest Manifest2822) GetTag(tag string) *Manifest2822Entry {
for _, entry := range manifest.Entries {
if entry.HasTag(tag) {
@ -154,6 +282,62 @@ func (manifest Manifest2822) GetTag(tag string) *Manifest2822Entry {
return nil
}
// GetSharedTag returns a list of entries with the given tag in entry.SharedTags (or the empty list if there are no entries with the given tag).
func (manifest Manifest2822) GetSharedTag(tag string) []Manifest2822Entry {
ret := []Manifest2822Entry{}
for _, entry := range manifest.Entries {
if entry.HasSharedTag(tag) {
ret = append(ret, entry)
}
}
return ret
}
// GetAllSharedTags returns a list of the sum of all SharedTags in all entries of this image manifest (in the order they appear in the file).
func (manifest Manifest2822) GetAllSharedTags() []string {
fakeEntry := Manifest2822Entry{}
for _, entry := range manifest.Entries {
fakeEntry.SharedTags = append(fakeEntry.SharedTags, entry.SharedTags...)
}
fakeEntry.DeduplicateSharedTags()
return fakeEntry.SharedTags
}
type SharedTagGroup struct {
SharedTags []string
Entries []*Manifest2822Entry
}
// GetSharedTagGroups returns a map of shared tag groups to the list of entries they share (as described in https://github.com/docker-library/go-dockerlibrary/pull/2#issuecomment-277853597).
func (manifest Manifest2822) GetSharedTagGroups() []SharedTagGroup {
inter := map[string][]string{}
interOrder := []string{} // order matters, and maps randomize order
interKeySep := ","
for _, sharedTag := range manifest.GetAllSharedTags() {
interKeyParts := []string{}
for _, entry := range manifest.GetSharedTag(sharedTag) {
interKeyParts = append(interKeyParts, entry.Tags[0])
}
interKey := strings.Join(interKeyParts, interKeySep)
if _, ok := inter[interKey]; !ok {
interOrder = append(interOrder, interKey)
}
inter[interKey] = append(inter[interKey], sharedTag)
}
ret := []SharedTagGroup{}
for _, tags := range interOrder {
group := SharedTagGroup{
SharedTags: inter[tags],
Entries: []*Manifest2822Entry{},
}
for _, tag := range strings.Split(tags, interKeySep) {
group.Entries = append(group.Entries, manifest.GetTag(tag))
}
ret = append(ret, group)
}
return ret
}
func (manifest *Manifest2822) AddEntry(entry Manifest2822Entry) error {
if len(entry.Tags) < 1 {
return fmt.Errorf("missing Tags")
@ -165,20 +349,36 @@ func (manifest *Manifest2822) AddEntry(entry Manifest2822Entry) error {
return fmt.Errorf("Tags %q has invalid Maintainers: %q (expected format %q)", strings.Join(invalidMaintainers, ", "), MaintainersFormat)
}
entry.DeduplicateSharedTags()
seenTag := map[string]bool{}
for _, tag := range entry.Tags {
if otherEntry := manifest.GetTag(tag); otherEntry != nil {
return fmt.Errorf("Tags %q includes duplicate tag: %q (duplicated in %q)", entry.TagsString(), tag, otherEntry.TagsString())
}
if otherEntries := manifest.GetSharedTag(tag); len(otherEntries) > 0 {
return fmt.Errorf("Tags %q includes tag conflicting with a shared tag: %q (shared tag in %q)", entry.TagsString(), tag, otherEntries[0].TagsString())
}
if seenTag[tag] {
return fmt.Errorf("Tags %q includes duplicate tag: %q", entry.TagsString(), tag)
}
seenTag[tag] = true
}
for _, tag := range entry.SharedTags {
if otherEntry := manifest.GetTag(tag); otherEntry != nil {
return fmt.Errorf("Tags %q includes conflicting shared tag: %q (duplicated in %q)", entry.TagsString(), tag, otherEntry.TagsString())
}
if seenTag[tag] {
return fmt.Errorf("Tags %q includes duplicate tag: %q (in SharedTags)", entry.TagsString(), tag)
}
seenTag[tag] = true
}
for i, existingEntry := range manifest.Entries {
if existingEntry.SameBuildArtifacts(entry) {
manifest.Entries[i].Tags = append(existingEntry.Tags, entry.Tags...)
manifest.Entries[i].SharedTags = append(existingEntry.SharedTags, entry.SharedTags...)
manifest.Entries[i].DeduplicateSharedTags()
return nil
}
}
@ -210,20 +410,51 @@ func (entry Manifest2822Entry) InvalidMaintainers() []string {
return invalid
}
// DeduplicateSharedTags will remove duplicate values from entry.SharedTags, preserving order.
func (entry *Manifest2822Entry) DeduplicateSharedTags() {
aggregate := []string{}
seen := map[string]bool{}
for _, tag := range entry.SharedTags {
if seen[tag] {
continue
}
seen[tag] = true
aggregate = append(aggregate, tag)
}
entry.SharedTags = aggregate
}
type decoderWrapper struct {
*control.Decoder
}
func (decoder *decoderWrapper) Decode(entry *Manifest2822Entry) error {
// reset Architectures and SharedTags so that they can be either inherited or replaced, not additive
sharedTags := entry.SharedTags
entry.SharedTags = nil
arches := entry.Architectures
entry.Architectures = nil
for {
err := decoder.Decoder.Decode(entry)
if err != nil {
return err
}
// ignore empty paragraphs (blank lines at the start, excess blank lines between paragraphs, excess blank lines at EOF)
if len(entry.Paragraph.Order) > 0 {
return nil
if len(entry.Paragraph.Order) == 0 {
continue
}
// if we had no SharedTags or Architectures, restore our "default" (original) values
if len(entry.SharedTags) == 0 {
entry.SharedTags = sharedTags
}
if len(entry.Architectures) == 0 {
entry.Architectures = arches
}
return nil
}
}

View File

@ -0,0 +1,38 @@
# this is a snapshot of https://github.com/docker-library/official-images/raw/1a3c4cd6d5cd53bd538a6f56a69f94c5b35325a7/library/bash
# this file is generated via https://github.com/tianon/docker-bash/blob/cd1de3dfc885b3395cd354ddb988922350b092a7/generate-stackbrew-library.sh
Maintainers: Tianon Gravi <admwiggin@gmail.com> (@tianon)
GitRepo: https://github.com/tianon/docker-bash.git
Tags: 4.4.12, 4.4, 4, latest
GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e
Directory: 4.4
Tags: 4.3.48, 4.3
GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e
Directory: 4.3
Tags: 4.2.53, 4.2
GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e
Directory: 4.2
Tags: 4.1.17, 4.1
GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e
Directory: 4.1
Tags: 4.0.44, 4.0
GitCommit: 4438745d601d10d300e363f24205a3ca75307803
Directory: 4.0
Tags: 3.2.57, 3.2, 3
GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e
Directory: 3.2
Tags: 3.1.23, 3.1
GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e
Directory: 3.1
Tags: 3.0.22, 3.0
GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e
Directory: 3.0

View File

@ -0,0 +1,26 @@
package execpipe_test
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/docker-library/go-dockerlibrary/pkg/execpipe"
)
func Example() {
pipe, err := execpipe.RunCommand("go", "version")
if err != nil {
panic(err)
}
defer pipe.Close()
var buf bytes.Buffer
io.Copy(&buf, pipe)
fmt.Println(strings.SplitN(buf.String(), " version ", 2)[0])
// Output:
// go
}

View File

@ -0,0 +1,31 @@
package execpipe_test
import (
"os"
"os/exec"
"testing"
"github.com/docker-library/go-dockerlibrary/pkg/execpipe"
)
func TestStdoutPipeError(t *testing.T) {
cmd := exec.Command("nothing", "really", "matters", "in", "the", "end")
// set "Stdout" so that "cmd.StdoutPipe" fails
// https://golang.org/src/os/exec/exec.go?s=16834:16883#L587
cmd.Stdout = os.Stdout
_, err := execpipe.Run(cmd)
if err == nil {
t.Errorf("Expected execpipe.Run to fail -- it did not")
}
}
func TestStartError(t *testing.T) {
// craft a definitely-invalid command so that "cmd.Start" fails
// https://golang.org/src/os/exec/exec.go?s=8739:8766#L303
_, err := execpipe.RunCommand("nothing-really-matters-in-the-end--bogus-command")
if err == nil {
t.Errorf("Expected execpipe.RunCommand to fail -- it did not")
}
}

View File

@ -0,0 +1,32 @@
package stripper_test
import (
"io"
"os"
"strings"
"github.com/docker-library/go-dockerlibrary/pkg/stripper"
)
func ExampleCommentStripper() {
r := strings.NewReader(`
# opening comment
a: b
# comment!
c: d # not a comment
# another cheeky comment
e: f
`)
comStrip := stripper.NewCommentStripper(r)
// using CopyBuffer to force smaller Read sizes (better testing coverage that way)
io.CopyBuffer(os.Stdout, comStrip, make([]byte, 32))
// Output:
// a: b
// c: d # not a comment
//
// e: f
}

View File

@ -0,0 +1,8 @@
/*
Package templatelib implements a group of useful functions for use with the stdlib text/template package.
Usage:
tmpl, err := template.New("some-template").Funcs(templatelib.FuncMap).Parse("Hi, {{ join " " .Names }}")
*/
package templatelib

View File

@ -67,12 +67,15 @@ func stringsModifierActionFactory(a func(string, string) string) func([]string,
}
}
// TODO write some tests for these
var FuncMap = template.FuncMap{
// {{- $isGitHub := hasPrefix "https://github.com/" $url -}}
// {{- $isHtml := hasSuffix ".html" $url -}}
"hasPrefix": swapStringsFuncBoolArgsOrder(strings.HasPrefix),
"hasSuffix": swapStringsFuncBoolArgsOrder(strings.HasSuffix),
// {{- $hugeIfTrue := .SomeValue | ternary "HUGE" "not so huge" -}}
// if .SomeValue is truthy, $hugeIfTrue will be "HUGE"
// (otherwise, "not so huge")
"ternary": func(truthy interface{}, falsey interface{}, val interface{}) interface{} {
if t, ok := template.IsTrue(val); !ok {
panic(fmt.Sprintf(`template.IsTrue(%+v) says things are NOT OK`, val))
@ -83,14 +86,26 @@ var FuncMap = template.FuncMap{
}
},
// First Tag: {{- .Tags | first -}}
// Last Tag: {{- .Tags | last -}}
"first": thingsActionFactory("first", true, func(args []interface{}, arg interface{}) interface{} { return arg }),
"last": thingsActionFactory("last", false, func(args []interface{}, arg interface{}) interface{} { return arg }),
// JSON data dump: {{ json . }}
// (especially nice for taking data and piping it to "jq")
// (ie "some-tool inspect --format '{{ json . }}' some-things | jq .")
"json": func(v interface{}) (string, error) {
j, err := json.Marshal(v)
return string(j), err
},
"join": stringsActionFactory("join", true, strings.Join),
// Everybody: {{- join ", " .Names -}}
// Concat: {{- join "/" "https://github.com" "jsmith" "some-repo" -}}
"join": stringsActionFactory("join", true, strings.Join),
// {{- $mungedUrl := $url | replace "git://" "https://" | trimSuffixes ".git" -}}
// turns: git://github.com/jsmith/some-repo.git
// into: https://github.com/jsmith/some-repo
"trimPrefixes": stringsActionFactory("trimPrefixes", false, stringsModifierActionFactory(strings.TrimPrefix)),
"trimSuffixes": stringsActionFactory("trimSuffixes", false, stringsModifierActionFactory(strings.TrimSuffix)),
"replace": stringsActionFactory("replace", false, func(strs []string, str string) string {

View File

@ -0,0 +1,193 @@
package templatelib_test
import (
"os"
"text/template"
"github.com/docker-library/go-dockerlibrary/pkg/templatelib"
)
func Example_prefixSuffix() {
tmpl, err := template.New("github-or-html").Funcs(templatelib.FuncMap).Parse(`
{{- . -}}
{{- if hasPrefix "https://github.com/" . -}}
{{- " " -}} GitHub
{{- end -}}
{{- if hasSuffix ".html" . -}}
{{- " " -}} HTML
{{- end -}}
{{- "\n" -}}
`)
if err != nil {
panic(err)
}
err = tmpl.Execute(os.Stdout, "https://github.com/example/example")
if err != nil {
panic(err)
}
err = tmpl.Execute(os.Stdout, "https://example.com/test.html")
if err != nil {
panic(err)
}
err = tmpl.Execute(os.Stdout, "https://example.com")
if err != nil {
panic(err)
}
err = tmpl.Execute(os.Stdout, "https://github.com/example/example/raw/master/test.html")
if err != nil {
panic(err)
}
// Output:
// https://github.com/example/example GitHub
// https://example.com/test.html HTML
// https://example.com
// https://github.com/example/example/raw/master/test.html GitHub HTML
}
func Example_ternary() {
tmpl, err := template.New("huge-if-true").Funcs(templatelib.FuncMap).Parse(`
{{- range $a := . -}}
{{ printf "%#v: %s\n" $a (ternary "HUGE" "not so huge" $a) }}
{{- end -}}
`)
err = tmpl.Execute(os.Stdout, []interface{}{
true,
false,
"true",
"false",
"",
nil,
1,
0,
9001,
[]bool{},
[]bool{false},
})
if err != nil {
panic(err)
}
// Output:
// true: HUGE
// false: not so huge
// "true": HUGE
// "false": HUGE
// "": not so huge
// <nil>: not so huge
// 1: HUGE
// 0: not so huge
// 9001: HUGE
// []bool{}: not so huge
// []bool{false}: HUGE
}
func Example_firstLast() {
tmpl, err := template.New("first-and-last").Funcs(templatelib.FuncMap).Parse(`First: {{ . | first }}, Last: {{ . | last }}`)
err = tmpl.Execute(os.Stdout, []interface{}{
"a",
"b",
"c",
})
if err != nil {
panic(err)
}
// Output:
// First: a, Last: c
}
func Example_json() {
tmpl, err := template.New("json").Funcs(templatelib.FuncMap).Parse(`
{{- json . -}}
`)
err = tmpl.Execute(os.Stdout, map[string]interface{}{
"a": []string{"1", "2", "3"},
"b": map[string]bool{"1": true, "2": false, "3": true},
"c": nil,
})
if err != nil {
panic(err)
}
// Output:
// {"a":["1","2","3"],"b":{"1":true,"2":false,"3":true},"c":null}
}
func Example_join() {
tmpl, err := template.New("join").Funcs(templatelib.FuncMap).Parse(`
Array: {{ . | join ", " }}{{ "\n" -}}
Args: {{ join ", " "a" "b" "c" -}}
`)
err = tmpl.Execute(os.Stdout, []string{
"1",
"2",
"3",
})
if err != nil {
panic(err)
}
// Output:
// Array: 1, 2, 3
// Args: a, b, c
}
func Example_trimReplaceGitToHttps() {
tmpl, err := template.New("git-to-https").Funcs(templatelib.FuncMap).Parse(`
{{- range . -}}
{{- . | replace "git://" "https://" | trimSuffixes ".git" }}{{ "\n" -}}
{{- end -}}
`)
err = tmpl.Execute(os.Stdout, []string{
"git://github.com/jsmith/some-repo.git",
"https://github.com/jsmith/some-repo.git",
"https://github.com/jsmith/some-repo",
})
if err != nil {
panic(err)
}
// Output:
// https://github.com/jsmith/some-repo
// https://github.com/jsmith/some-repo
// https://github.com/jsmith/some-repo
}
func Example_trimReplaceGitToGo() {
tmpl, err := template.New("git-to-go").Funcs(templatelib.FuncMap).Parse(`
{{- range . -}}
{{- . | trimPrefixes "git://" "http://" "https://" "ssh://" | trimSuffixes ".git" }}{{ "\n" -}}
{{- end -}}
`)
err = tmpl.Execute(os.Stdout, []string{
"git://github.com/jsmith/some-repo.git",
"https://github.com/jsmith/some-repo.git",
"https://github.com/jsmith/some-repo",
"ssh://github.com/jsmith/some-repo.git",
"github.com/jsmith/some-repo",
})
if err != nil {
panic(err)
}
// Output:
// github.com/jsmith/some-repo
// github.com/jsmith/some-repo
// github.com/jsmith/some-repo
// github.com/jsmith/some-repo
// github.com/jsmith/some-repo
}

View File

@ -0,0 +1,45 @@
package templatelib_test
import (
"testing"
"text/template"
"unsafe"
"github.com/docker-library/go-dockerlibrary/pkg/templatelib"
)
func TestTernaryPanic(t *testing.T) {
// one of the only places template.IsTrue will return "false" for the "ok" value is an UnsafePointer (hence this test)
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected panic, executed successfully instead")
} else if errText, ok := r.(string); !ok || errText != `template.IsTrue(<nil>) says things are NOT OK` {
t.Errorf("Unexpected panic: %v", errText)
}
}()
tmpl, err := template.New("unsafe-pointer").Funcs(templatelib.FuncMap).Parse(`{{ ternary "true" "false" . }}`)
err = tmpl.Execute(nil, unsafe.Pointer(uintptr(0)))
if err != nil {
t.Errorf("Expected panic, got error instead: %v", err)
}
}
func TestJoinPanic(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected panic, executed successfully instead")
} else if errText, ok := r.(string); !ok || errText != `"join" requires at least one argument` {
t.Errorf("Unexpected panic: %v", r)
}
}()
tmpl, err := template.New("join-no-arg").Funcs(templatelib.FuncMap).Parse(`{{ join }}`)
err = tmpl.Execute(nil, nil)
if err != nil {
t.Errorf("Expected panic, got error instead: %v", err)
}
}