├── pkg ├── templatelib │ ├── doc.go │ ├── lib_test.go │ ├── lib.go │ └── lib_example_test.go ├── execpipe │ ├── execpipe_example_test.go │ ├── execpipe_test.go │ └── execpipe.go └── stripper │ ├── comments_example_test.go │ └── comments.go ├── manifest ├── parse_test.go ├── parse.go ├── testdata │ └── bash ├── fetch.go ├── line-based.go ├── example_test.go └── rfc2822.go ├── .travis.yml ├── README.md ├── architecture └── oci-platform.go └── LICENSE /pkg/templatelib/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package templatelib implements a group of useful functions for use with the stdlib text/template package. 3 | 4 | Usage: 5 | 6 | tmpl, err := template.New("some-template").Funcs(templatelib.FuncMap).Parse("Hi, {{ join " " .Names }}") 7 | */ 8 | package templatelib 9 | -------------------------------------------------------------------------------- /pkg/execpipe/execpipe_example_test.go: -------------------------------------------------------------------------------- 1 | package execpipe_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/docker-library/go-dockerlibrary/pkg/execpipe" 10 | ) 11 | 12 | func Example() { 13 | pipe, err := execpipe.RunCommand("go", "version") 14 | if err != nil { 15 | panic(err) 16 | } 17 | defer pipe.Close() 18 | 19 | var buf bytes.Buffer 20 | io.Copy(&buf, pipe) 21 | 22 | fmt.Println(strings.SplitN(buf.String(), " version ", 2)[0]) 23 | 24 | // Output: 25 | // go 26 | } 27 | -------------------------------------------------------------------------------- /manifest/parse_test.go: -------------------------------------------------------------------------------- 1 | package manifest_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/docker-library/go-dockerlibrary/manifest" 8 | ) 9 | 10 | func TestParseError(t *testing.T) { 11 | invalidManifest := `this is just completely bogus and invalid no matter how you slice it` 12 | 13 | man, err := manifest.Parse(strings.NewReader(invalidManifest)) 14 | if err == nil { 15 | t.Errorf("Expected error, got valid manifest instead:\n%s", man) 16 | } 17 | if !strings.HasPrefix(err.Error(), "Bad line:") { 18 | t.Errorf("Unexpected error: %v", err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go_import_path: github.com/docker-library/go-dockerlibrary 4 | 5 | go: 6 | - 1.13.x 7 | - 1.12.x 8 | 9 | os: 10 | - linux 11 | - windows 12 | 13 | script: 14 | - | 15 | set -e +x 16 | rm -f coverage.txt 17 | for d in $(go list ./...); do 18 | ( set -x; go test -v -race -cover -coverprofile=profile.out -covermode=atomic "$d" ) 19 | if [ -f profile.out ]; then 20 | cat profile.out >> coverage.txt 21 | rm profile.out 22 | fi 23 | done 24 | 25 | after_script: 26 | - curl -fL https://codecov.io/bash -o codecov.sh && bash codecov.sh 27 | -------------------------------------------------------------------------------- /pkg/stripper/comments_example_test.go: -------------------------------------------------------------------------------- 1 | package stripper_test 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strings" 7 | 8 | "github.com/docker-library/go-dockerlibrary/pkg/stripper" 9 | ) 10 | 11 | func ExampleCommentStripper() { 12 | r := strings.NewReader(` 13 | # opening comment 14 | a: b 15 | # comment! 16 | c: d # not a comment 17 | 18 | # another cheeky comment 19 | e: f 20 | `) 21 | 22 | comStrip := stripper.NewCommentStripper(r) 23 | 24 | // using CopyBuffer to force smaller Read sizes (better testing coverage that way) 25 | io.CopyBuffer(os.Stdout, comStrip, make([]byte, 32)) 26 | 27 | // Output: 28 | // a: b 29 | // c: d # not a comment 30 | // 31 | // e: f 32 | } 33 | -------------------------------------------------------------------------------- /manifest/parse.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // try parsing as a 2822 manifest, but fallback to line-based if that fails 9 | func Parse(reader io.Reader) (*Manifest2822, error) { 10 | buf := &bytes.Buffer{} 11 | 12 | // try parsing as 2822, but also copy back into a new buffer so that if it fails, we can re-parse as line-based 13 | manifest, err2822 := Parse2822(io.TeeReader(reader, buf)) 14 | if err2822 != nil { 15 | manifest, err := ParseLineBased(buf) 16 | if err != nil { 17 | // if we fail parsing line-based, eat the error and return the 2822 parsing error instead 18 | // https://github.com/docker-library/bashbrew/issues/16 19 | return nil, err2822 20 | } 21 | return manifest, nil 22 | } 23 | 24 | return manifest, nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/execpipe/execpipe_test.go: -------------------------------------------------------------------------------- 1 | package execpipe_test 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/docker-library/go-dockerlibrary/pkg/execpipe" 9 | ) 10 | 11 | func TestStdoutPipeError(t *testing.T) { 12 | cmd := exec.Command("nothing", "really", "matters", "in", "the", "end") 13 | 14 | // set "Stdout" so that "cmd.StdoutPipe" fails 15 | // https://golang.org/src/os/exec/exec.go?s=16834:16883#L587 16 | cmd.Stdout = os.Stdout 17 | 18 | _, err := execpipe.Run(cmd) 19 | if err == nil { 20 | t.Errorf("Expected execpipe.Run to fail -- it did not") 21 | } 22 | } 23 | 24 | func TestStartError(t *testing.T) { 25 | // craft a definitely-invalid command so that "cmd.Start" fails 26 | // https://golang.org/src/os/exec/exec.go?s=8739:8766#L303 27 | _, err := execpipe.RunCommand("nothing-really-matters-in-the-end--bogus-command") 28 | if err == nil { 29 | t.Errorf("Expected execpipe.RunCommand to fail -- it did not") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | **IMPORTANT NOTE:** this repository has become part of [`github.com/docker-library/bashbrew`](https://github.com/docker-library/bashbrew) (as of [docker-library/bashbrew#17](https://github.com/docker-library/bashbrew/pull/17)). Please adjust your usage accordingly! 4 | 5 | # `import "github.com/docker-library/go-dockerlibrary/manifest"` 6 | 7 | [![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) 8 | 9 | This package contains the core parsing elements of [the `bashbrew` tool used by the Docker Official Images](https://github.com/docker-library/bashbrew#readme). 10 | -------------------------------------------------------------------------------- /pkg/execpipe/execpipe.go: -------------------------------------------------------------------------------- 1 | package execpipe 2 | 3 | import ( 4 | "io" 5 | "os/exec" 6 | ) 7 | 8 | // "io.ReadCloser" interface to a command's output where "Close()" is effectively "Wait()" 9 | type Pipe struct { 10 | cmd *exec.Cmd 11 | out io.ReadCloser 12 | } 13 | 14 | // convenience wrapper for "Run" 15 | func RunCommand(cmd string, args ...string) (*Pipe, error) { 16 | return Run(exec.Command(cmd, args...)) 17 | } 18 | 19 | // start "cmd", capturing stdout in a pipe (be sure to call "Close" when finished reading to reap the process) 20 | func Run(cmd *exec.Cmd) (*Pipe, error) { 21 | pipe := &Pipe{ 22 | cmd: cmd, 23 | } 24 | if out, err := pipe.cmd.StdoutPipe(); err != nil { 25 | return nil, err 26 | } else { 27 | pipe.out = out 28 | } 29 | if err := pipe.cmd.Start(); err != nil { 30 | pipe.out.Close() 31 | return nil, err 32 | } 33 | return pipe, nil 34 | } 35 | 36 | func (pipe *Pipe) Read(p []byte) (n int, err error) { 37 | return pipe.out.Read(p) 38 | } 39 | 40 | func (pipe *Pipe) Close() error { 41 | return pipe.cmd.Wait() 42 | } 43 | -------------------------------------------------------------------------------- /pkg/stripper/comments.go: -------------------------------------------------------------------------------- 1 | package stripper 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | type CommentStripper struct { 12 | Comment string 13 | Delimiter byte 14 | Whitespace bool 15 | 16 | r *bufio.Reader 17 | buf bytes.Buffer 18 | } 19 | 20 | func NewCommentStripper(r io.Reader) *CommentStripper { 21 | return &CommentStripper{ 22 | Comment: "#", 23 | Delimiter: '\n', 24 | Whitespace: true, 25 | 26 | r: bufio.NewReader(r), 27 | } 28 | } 29 | 30 | func (r *CommentStripper) Read(p []byte) (int, error) { 31 | for { 32 | if r.buf.Len() >= len(p) { 33 | return r.buf.Read(p) 34 | } 35 | line, err := r.r.ReadString(r.Delimiter) 36 | if len(line) > 0 { 37 | checkLine := line 38 | if r.Whitespace { 39 | checkLine = strings.TrimLeftFunc(checkLine, unicode.IsSpace) 40 | } 41 | if strings.HasPrefix(checkLine, r.Comment) { 42 | // yay, skip this line 43 | continue 44 | } 45 | r.buf.WriteString(line) 46 | } 47 | if err != nil { 48 | if r.buf.Len() > 0 { 49 | return r.buf.Read(p) 50 | } 51 | return 0, err 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /architecture/oci-platform.go: -------------------------------------------------------------------------------- 1 | package architecture 2 | 3 | // https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md#image-index-property-descriptions 4 | // see "platform" (under "manifests") 5 | type OCIPlatform struct { 6 | OS string `json:"os"` 7 | Architecture string `json:"architecture"` 8 | Variant string `json:"variant,omitempty"` 9 | 10 | //OSVersion string `json:"os.version,omitempty"` 11 | //OSFeatures []string `json:"os.features,omitempty"` 12 | } 13 | 14 | var SupportedArches = map[string]OCIPlatform{ 15 | "amd64": {OS: "linux", Architecture: "amd64"}, 16 | "arm32v5": {OS: "linux", Architecture: "arm", Variant: "v5"}, 17 | "arm32v6": {OS: "linux", Architecture: "arm", Variant: "v6"}, 18 | "arm32v7": {OS: "linux", Architecture: "arm", Variant: "v7"}, 19 | "arm64v8": {OS: "linux", Architecture: "arm64", Variant: "v8"}, 20 | "i386": {OS: "linux", Architecture: "386"}, 21 | "mips64le": {OS: "linux", Architecture: "mips64le"}, 22 | "ppc64le": {OS: "linux", Architecture: "ppc64le"}, 23 | "s390x": {OS: "linux", Architecture: "s390x"}, 24 | 25 | "windows-amd64": {OS: "windows", Architecture: "amd64"}, 26 | } 27 | -------------------------------------------------------------------------------- /manifest/testdata/bash: -------------------------------------------------------------------------------- 1 | # this is a snapshot of https://github.com/docker-library/official-images/raw/1a3c4cd6d5cd53bd538a6f56a69f94c5b35325a7/library/bash 2 | 3 | # this file is generated via https://github.com/tianon/docker-bash/blob/cd1de3dfc885b3395cd354ddb988922350b092a7/generate-stackbrew-library.sh 4 | 5 | Maintainers: Tianon Gravi (@tianon) 6 | GitRepo: https://github.com/tianon/docker-bash.git 7 | 8 | Tags: 4.4.12, 4.4, 4, latest 9 | GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e 10 | Directory: 4.4 11 | 12 | Tags: 4.3.48, 4.3 13 | GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e 14 | Directory: 4.3 15 | 16 | Tags: 4.2.53, 4.2 17 | GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e 18 | Directory: 4.2 19 | 20 | Tags: 4.1.17, 4.1 21 | GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e 22 | Directory: 4.1 23 | 24 | Tags: 4.0.44, 4.0 25 | GitCommit: 4438745d601d10d300e363f24205a3ca75307803 26 | Directory: 4.0 27 | 28 | Tags: 3.2.57, 3.2, 3 29 | GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e 30 | Directory: 3.2 31 | 32 | Tags: 3.1.23, 3.1 33 | GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e 34 | Directory: 3.1 35 | 36 | Tags: 3.0.22, 3.0 37 | GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e 38 | Directory: 3.0 39 | -------------------------------------------------------------------------------- /pkg/templatelib/lib_test.go: -------------------------------------------------------------------------------- 1 | package templatelib_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "text/template" 7 | "unsafe" 8 | 9 | "github.com/docker-library/go-dockerlibrary/pkg/templatelib" 10 | ) 11 | 12 | func TestTernaryPanic(t *testing.T) { 13 | // one of the only places template.IsTrue will return "false" for the "ok" value is an UnsafePointer (hence this test) 14 | 15 | tmpl, err := template.New("unsafe-pointer").Funcs(templatelib.FuncMap).Parse(`{{ ternary "true" "false" . }}`) 16 | if err != nil { 17 | t.Errorf("Unexpected error: %v", err) 18 | } 19 | 20 | err = tmpl.Execute(nil, unsafe.Pointer(uintptr(0))) 21 | if err == nil { 22 | t.Errorf("Expected error, executed successfully instead") 23 | } 24 | if !strings.HasSuffix(err.Error(), `template.IsTrue() says things are NOT OK`) { 25 | t.Errorf("Expected specific error, got: %v", err) 26 | } 27 | } 28 | 29 | func TestJoinPanic(t *testing.T) { 30 | tmpl, err := template.New("join-no-arg").Funcs(templatelib.FuncMap).Parse(`{{ join }}`) 31 | if err != nil { 32 | t.Errorf("Unexpected error: %v", err) 33 | } 34 | 35 | err = tmpl.Execute(nil, nil) 36 | if err == nil { 37 | t.Errorf("Expected error, executed successfully instead") 38 | } 39 | if !strings.HasSuffix(err.Error(), `"join" requires at least one argument`) { 40 | t.Errorf("Expected specific error, got: %v", err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /manifest/fetch.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | func validateTagName(man *Manifest2822, repoName, tagName string) error { 13 | if tagName != "" && (man.GetTag(tagName) == nil && len(man.GetSharedTag(tagName)) == 0) { 14 | return fmt.Errorf("tag not found in manifest for %q: %q", repoName, tagName) 15 | } 16 | return nil 17 | } 18 | 19 | // "library" is the default "library directory" 20 | // returns the parsed version of (in order): 21 | // if "repo" is a URL, the remote contents of that URL 22 | // if "repo" is a relative path like "./repo", that file 23 | // the file "library/repo" 24 | // (repoName, tagName, man, err) 25 | func Fetch(library, repo string) (string, string, *Manifest2822, error) { 26 | repoName := filepath.Base(repo) 27 | tagName := "" 28 | if tagIndex := strings.IndexRune(repoName, ':'); tagIndex > 0 { 29 | tagName = repoName[tagIndex+1:] 30 | repoName = repoName[:tagIndex] 31 | repo = strings.TrimSuffix(repo, ":"+tagName) 32 | } 33 | 34 | u, err := url.Parse(repo) 35 | if err == nil && u.IsAbs() && (u.Scheme == "http" || u.Scheme == "https") { 36 | // must be remote URL! 37 | resp, err := http.Get(repo) 38 | if err != nil { 39 | return repoName, tagName, nil, err 40 | } 41 | defer resp.Body.Close() 42 | man, err := Parse(resp.Body) 43 | if err != nil { 44 | return repoName, tagName, man, err 45 | } 46 | return repoName, tagName, man, validateTagName(man, repoName, tagName) 47 | } 48 | 49 | // try file paths 50 | filePaths := []string{} 51 | if filepath.IsAbs(repo) || strings.IndexRune(repo, filepath.Separator) >= 0 || strings.IndexRune(repo, '/') >= 0 { 52 | filePaths = append(filePaths, repo) 53 | } 54 | if !filepath.IsAbs(repo) { 55 | filePaths = append(filePaths, filepath.Join(library, repo)) 56 | } 57 | for _, fileName := range filePaths { 58 | f, err := os.Open(fileName) 59 | if err != nil && !os.IsNotExist(err) { 60 | return repoName, tagName, nil, err 61 | } 62 | if err == nil { 63 | defer f.Close() 64 | man, err := Parse(f) 65 | if err != nil { 66 | return repoName, tagName, man, err 67 | } 68 | return repoName, tagName, man, validateTagName(man, repoName, tagName) 69 | } 70 | } 71 | 72 | return repoName, tagName, nil, fmt.Errorf("unable to find a manifest named %q (in %q or as a remote URL)", repo, library) 73 | } 74 | -------------------------------------------------------------------------------- /manifest/line-based.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | const DefaultLineBasedFetch = "refs/heads/*" // backwards compatibility 11 | 12 | // TODO write more of a proper parser? (probably not worthwhile given that 2822 is the preferred format) 13 | func ParseLineBasedLine(line string, defaults Manifest2822Entry) (*Manifest2822Entry, error) { 14 | entry := defaults.Clone() 15 | 16 | parts := strings.SplitN(line, ":", 2) 17 | if len(parts) < 2 { 18 | return nil, fmt.Errorf("manifest line missing ':': %s", line) 19 | } 20 | entry.Tags = []string{strings.TrimSpace(parts[0])} 21 | 22 | parts = strings.SplitN(parts[1], "@", 2) 23 | if len(parts) < 2 { 24 | return nil, fmt.Errorf("manifest line missing '@': %s", line) 25 | } 26 | entry.GitRepo = strings.TrimSpace(parts[0]) 27 | 28 | parts = strings.SplitN(parts[1], " ", 2) 29 | entry.GitCommit = strings.TrimSpace(parts[0]) 30 | if len(parts) > 1 { 31 | entry.Directory = strings.TrimSpace(parts[1]) 32 | } 33 | 34 | if entry.GitFetch == DefaultLineBasedFetch && !GitCommitRegex.MatchString(entry.GitCommit) { 35 | // doesn't look like a commit, must be a tag 36 | entry.GitFetch = "refs/tags/" + entry.GitCommit 37 | entry.GitCommit = "FETCH_HEAD" 38 | } 39 | 40 | return &entry, nil 41 | } 42 | 43 | func ParseLineBased(readerIn io.Reader) (*Manifest2822, error) { 44 | reader := bufio.NewReader(readerIn) 45 | 46 | manifest := &Manifest2822{ 47 | Global: DefaultManifestEntry.Clone(), 48 | } 49 | manifest.Global.GitFetch = DefaultLineBasedFetch 50 | 51 | for { 52 | line, err := reader.ReadString('\n') 53 | 54 | line = strings.TrimSpace(line) 55 | if len(line) > 0 { 56 | if line[0] == '#' { 57 | maintainerLine := strings.TrimPrefix(line, "# maintainer: ") 58 | if line != maintainerLine { 59 | // if the prefix was removed, it must be a maintainer line! 60 | manifest.Global.Maintainers = append(manifest.Global.Maintainers, maintainerLine) 61 | } 62 | } else { 63 | entry, parseErr := ParseLineBasedLine(line, manifest.Global) 64 | if parseErr != nil { 65 | return nil, parseErr 66 | } 67 | 68 | err = manifest.AddEntry(*entry) 69 | if err != nil { 70 | return nil, err 71 | } 72 | } 73 | } 74 | 75 | if err == io.EOF { 76 | break 77 | } 78 | if err != nil { 79 | return nil, err 80 | } 81 | } 82 | 83 | if len(manifest.Global.Maintainers) < 1 { 84 | return nil, fmt.Errorf("missing Maintainers") 85 | } 86 | if invalidMaintainers := manifest.Global.InvalidMaintainers(); len(invalidMaintainers) > 0 { 87 | return nil, fmt.Errorf("invalid Maintainers: %q (expected format %q)", strings.Join(invalidMaintainers, ", "), MaintainersFormat) 88 | } 89 | 90 | return manifest, nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/templatelib/lib.go: -------------------------------------------------------------------------------- 1 | package templatelib 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | func swapStringsFuncBoolArgsOrder(a func(string, string) bool) func(string, string) bool { 13 | return func(str1 string, str2 string) bool { 14 | return a(str2, str1) 15 | } 16 | } 17 | 18 | func thingsActionFactory(name string, actOnFirst bool, action func([]interface{}, interface{}) interface{}) func(args ...interface{}) interface{} { 19 | return func(args ...interface{}) interface{} { 20 | if len(args) < 1 { 21 | panic(fmt.Sprintf(`%q requires at least one argument`, name)) 22 | } 23 | 24 | actArgs := []interface{}{} 25 | for _, val := range args { 26 | v := reflect.ValueOf(val) 27 | 28 | switch v.Kind() { 29 | case reflect.Slice, reflect.Array: 30 | for i := 0; i < v.Len(); i++ { 31 | actArgs = append(actArgs, v.Index(i).Interface()) 32 | } 33 | default: 34 | actArgs = append(actArgs, v.Interface()) 35 | } 36 | } 37 | 38 | var arg interface{} 39 | if actOnFirst { 40 | arg = actArgs[0] 41 | actArgs = actArgs[1:] 42 | } else { 43 | arg = actArgs[len(actArgs)-1] 44 | actArgs = actArgs[:len(actArgs)-1] 45 | } 46 | 47 | return action(actArgs, arg) 48 | } 49 | } 50 | 51 | func stringsActionFactory(name string, actOnFirst bool, action func([]string, string) string) func(args ...interface{}) interface{} { 52 | return thingsActionFactory(name, actOnFirst, func(args []interface{}, arg interface{}) interface{} { 53 | str := arg.(string) 54 | strs := []string{} 55 | for _, val := range args { 56 | strs = append(strs, val.(string)) 57 | } 58 | return action(strs, str) 59 | }) 60 | } 61 | 62 | func stringsModifierActionFactory(a func(string, string) string) func([]string, string) string { 63 | return func(strs []string, str string) string { 64 | for _, mod := range strs { 65 | str = a(str, mod) 66 | } 67 | return str 68 | } 69 | } 70 | 71 | var FuncMap = template.FuncMap{ 72 | // {{- $isGitHub := hasPrefix "https://github.com/" $url -}} 73 | // {{- $isHtml := hasSuffix ".html" $url -}} 74 | "hasPrefix": swapStringsFuncBoolArgsOrder(strings.HasPrefix), 75 | "hasSuffix": swapStringsFuncBoolArgsOrder(strings.HasSuffix), 76 | 77 | // {{- $hugeIfTrue := .SomeValue | ternary "HUGE" "not so huge" -}} 78 | // if .SomeValue is truthy, $hugeIfTrue will be "HUGE" 79 | // (otherwise, "not so huge") 80 | "ternary": func(truthy interface{}, falsey interface{}, val interface{}) interface{} { 81 | if t, ok := template.IsTrue(val); !ok { 82 | panic(fmt.Sprintf(`template.IsTrue(%+v) says things are NOT OK`, val)) 83 | } else if t { 84 | return truthy 85 | } else { 86 | return falsey 87 | } 88 | }, 89 | 90 | // First Tag: {{- .Tags | first -}} 91 | // Last Tag: {{- .Tags | last -}} 92 | "first": thingsActionFactory("first", true, func(args []interface{}, arg interface{}) interface{} { return arg }), 93 | "last": thingsActionFactory("last", false, func(args []interface{}, arg interface{}) interface{} { return arg }), 94 | 95 | // JSON data dump: {{ json . }} 96 | // (especially nice for taking data and piping it to "jq") 97 | // (ie "some-tool inspect --format '{{ json . }}' some-things | jq .") 98 | "json": func(v interface{}) (string, error) { 99 | j, err := json.Marshal(v) 100 | return string(j), err 101 | }, 102 | 103 | // Everybody: {{- join ", " .Names -}} 104 | // Concat: {{- join "/" "https://github.com" "jsmith" "some-repo" -}} 105 | "join": stringsActionFactory("join", true, strings.Join), 106 | 107 | // {{- $mungedUrl := $url | replace "git://" "https://" | trimSuffixes ".git" -}} 108 | // turns: git://github.com/jsmith/some-repo.git 109 | // into: https://github.com/jsmith/some-repo 110 | "trimPrefixes": stringsActionFactory("trimPrefixes", false, stringsModifierActionFactory(strings.TrimPrefix)), 111 | "trimSuffixes": stringsActionFactory("trimSuffixes", false, stringsModifierActionFactory(strings.TrimSuffix)), 112 | "replace": stringsActionFactory("replace", false, func(strs []string, str string) string { 113 | return strings.NewReplacer(strs...).Replace(str) 114 | }), 115 | 116 | // {{- getenv "PATH" -}} 117 | // {{- getenv "HOME" "no HOME set" -}} 118 | // {{- getenv "HOME" "is set" "is NOT set (or is empty)" -}} 119 | "getenv": thingsActionFactory("getenv", true, func(args []interface{}, arg interface{}) interface{} { 120 | var ( 121 | val = os.Getenv(arg.(string)) 122 | setVal interface{} = val 123 | unsetVal interface{} = "" 124 | ) 125 | if len(args) == 2 { 126 | setVal, unsetVal = args[0], args[1] 127 | } else if len(args) == 1 { 128 | unsetVal = args[0] 129 | } else if len(args) != 0 { 130 | panic(fmt.Sprintf(`expected between 1 and 3 arguments to "getenv", got %d`, len(args)+1)) 131 | } 132 | if val != "" { 133 | return setVal 134 | } else { 135 | return unsetVal 136 | } 137 | }), 138 | } 139 | -------------------------------------------------------------------------------- /pkg/templatelib/lib_example_test.go: -------------------------------------------------------------------------------- 1 | package templatelib_test 2 | 3 | import ( 4 | "os" 5 | "text/template" 6 | 7 | "github.com/docker-library/go-dockerlibrary/pkg/templatelib" 8 | ) 9 | 10 | func Example_prefixSuffix() { 11 | tmpl, err := template.New("github-or-html").Funcs(templatelib.FuncMap).Parse(` 12 | {{- . -}} 13 | 14 | {{- if hasPrefix "https://github.com/" . -}} 15 | {{- " " -}} GitHub 16 | {{- end -}} 17 | 18 | {{- if hasSuffix ".html" . -}} 19 | {{- " " -}} HTML 20 | {{- end -}} 21 | 22 | {{- "\n" -}} 23 | `) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | err = tmpl.Execute(os.Stdout, "https://github.com/example/example") 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | err = tmpl.Execute(os.Stdout, "https://example.com/test.html") 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | err = tmpl.Execute(os.Stdout, "https://example.com") 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | err = tmpl.Execute(os.Stdout, "https://github.com/example/example/raw/master/test.html") 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | // Output: 49 | // https://github.com/example/example GitHub 50 | // https://example.com/test.html HTML 51 | // https://example.com 52 | // https://github.com/example/example/raw/master/test.html GitHub HTML 53 | } 54 | 55 | func Example_ternary() { 56 | tmpl, err := template.New("huge-if-true").Funcs(templatelib.FuncMap).Parse(` 57 | {{- range $a := . -}} 58 | {{ printf "%#v: %s\n" $a (ternary "HUGE" "not so huge" $a) }} 59 | {{- end -}} 60 | `) 61 | 62 | err = tmpl.Execute(os.Stdout, []interface{}{ 63 | true, 64 | false, 65 | "true", 66 | "false", 67 | "", 68 | nil, 69 | 1, 70 | 0, 71 | 9001, 72 | []bool{}, 73 | []bool{false}, 74 | }) 75 | if err != nil { 76 | panic(err) 77 | } 78 | 79 | // Output: 80 | // true: HUGE 81 | // false: not so huge 82 | // "true": HUGE 83 | // "false": HUGE 84 | // "": not so huge 85 | // : not so huge 86 | // 1: HUGE 87 | // 0: not so huge 88 | // 9001: HUGE 89 | // []bool{}: not so huge 90 | // []bool{false}: HUGE 91 | } 92 | 93 | func Example_firstLast() { 94 | tmpl, err := template.New("first-and-last").Funcs(templatelib.FuncMap).Parse(`First: {{ . | first }}, Last: {{ . | last }}`) 95 | 96 | err = tmpl.Execute(os.Stdout, []interface{}{ 97 | "a", 98 | "b", 99 | "c", 100 | }) 101 | if err != nil { 102 | panic(err) 103 | } 104 | 105 | // Output: 106 | // First: a, Last: c 107 | } 108 | 109 | func Example_json() { 110 | tmpl, err := template.New("json").Funcs(templatelib.FuncMap).Parse(` 111 | {{- json . -}} 112 | `) 113 | 114 | err = tmpl.Execute(os.Stdout, map[string]interface{}{ 115 | "a": []string{"1", "2", "3"}, 116 | "b": map[string]bool{"1": true, "2": false, "3": true}, 117 | "c": nil, 118 | }) 119 | if err != nil { 120 | panic(err) 121 | } 122 | 123 | // Output: 124 | // {"a":["1","2","3"],"b":{"1":true,"2":false,"3":true},"c":null} 125 | } 126 | 127 | func Example_join() { 128 | tmpl, err := template.New("join").Funcs(templatelib.FuncMap).Parse(` 129 | Array: {{ . | join ", " }}{{ "\n" -}} 130 | Args: {{ join ", " "a" "b" "c" -}} 131 | `) 132 | 133 | err = tmpl.Execute(os.Stdout, []string{ 134 | "1", 135 | "2", 136 | "3", 137 | }) 138 | if err != nil { 139 | panic(err) 140 | } 141 | 142 | // Output: 143 | // Array: 1, 2, 3 144 | // Args: a, b, c 145 | } 146 | 147 | func Example_trimReplaceGitToHttps() { 148 | tmpl, err := template.New("git-to-https").Funcs(templatelib.FuncMap).Parse(` 149 | {{- range . -}} 150 | {{- . | replace "git://" "https://" | trimSuffixes ".git" }}{{ "\n" -}} 151 | {{- end -}} 152 | `) 153 | 154 | err = tmpl.Execute(os.Stdout, []string{ 155 | "git://github.com/jsmith/some-repo.git", 156 | "https://github.com/jsmith/some-repo.git", 157 | "https://github.com/jsmith/some-repo", 158 | }) 159 | if err != nil { 160 | panic(err) 161 | } 162 | 163 | // Output: 164 | // https://github.com/jsmith/some-repo 165 | // https://github.com/jsmith/some-repo 166 | // https://github.com/jsmith/some-repo 167 | } 168 | 169 | func Example_trimReplaceGitToGo() { 170 | tmpl, err := template.New("git-to-go").Funcs(templatelib.FuncMap).Parse(` 171 | {{- range . -}} 172 | {{- . | trimPrefixes "git://" "http://" "https://" "ssh://" | trimSuffixes ".git" }}{{ "\n" -}} 173 | {{- end -}} 174 | `) 175 | 176 | err = tmpl.Execute(os.Stdout, []string{ 177 | "git://github.com/jsmith/some-repo.git", 178 | "https://github.com/jsmith/some-repo.git", 179 | "https://github.com/jsmith/some-repo", 180 | "ssh://github.com/jsmith/some-repo.git", 181 | "github.com/jsmith/some-repo", 182 | }) 183 | if err != nil { 184 | panic(err) 185 | } 186 | 187 | // Output: 188 | // github.com/jsmith/some-repo 189 | // github.com/jsmith/some-repo 190 | // github.com/jsmith/some-repo 191 | // github.com/jsmith/some-repo 192 | // github.com/jsmith/some-repo 193 | } 194 | 195 | func Example_getenv() { 196 | tmpl, err := template.New("getenv").Funcs(templatelib.FuncMap).Parse(` 197 | The FOO environment variable {{ getenv "FOO" "is set" "is not set" }}. {{- "\n" -}} 198 | BAR: {{ getenv "BAR" "not set" }} {{- "\n" -}} 199 | BAZ: {{ getenv "BAZ" "not set" }} {{- "\n" -}} 200 | {{- $env := getenv "FOOBARBAZ" -}} 201 | {{- if eq $env "" -}} 202 | FOOBARBAZ {{- "\n" -}} 203 | {{- end -}} 204 | `) 205 | 206 | os.Setenv("FOO", "") 207 | os.Unsetenv("BAR") 208 | os.Setenv("BAZ", "foobar") 209 | os.Unsetenv("FOOBARBAZ") 210 | 211 | err = tmpl.Execute(os.Stdout, nil) 212 | if err != nil { 213 | panic(err) 214 | } 215 | 216 | // Output: 217 | // The FOO environment variable is not set. 218 | // BAR: not set 219 | // BAZ: foobar 220 | // FOOBARBAZ 221 | } 222 | -------------------------------------------------------------------------------- /manifest/example_test.go: -------------------------------------------------------------------------------- 1 | package manifest_test 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/docker-library/go-dockerlibrary/manifest" 9 | ) 10 | 11 | func Example() { 12 | man, err := manifest.Parse(bufio.NewReader(strings.NewReader(`# RFC 2822 13 | 14 | # I LOVE CAKE 15 | 16 | Maintainers: InfoSiftr (@infosiftr), 17 | Johan Euphrosine (@proppy) 18 | GitFetch: refs/heads/master 19 | GitRepo: https://github.com/docker-library/golang.git 20 | SharedTags: latest 21 | arm64v8-GitRepo: https://github.com/docker-library/golang.git 22 | Architectures: amd64, amd64 23 | 24 | 25 | # hi 26 | 27 | 28 | # blasphemer 29 | 30 | 31 | # Go 1.6 32 | Tags: 1.6.1, 1.6, 1 33 | arm64v8-GitRepo: https://github.com/docker-library/golang.git 34 | Directory: 1.6 35 | GitCommit: 0ce80411b9f41e9c3a21fc0a1bffba6ae761825a 36 | Constraints: some-random-build-server 37 | 38 | 39 | # Go 1.5 40 | Tags: 1.5.3 41 | GitCommit: d7e2a8d90a9b8f5dfd5bcd428e0c33b68c40cc19 42 | SharedTags: 1.5.3-debian, 1.5-debian 43 | Directory: 1.5 44 | s390x-GitCommit: b6c460e7cd79b595267870a98013ec3078b490df 45 | i386-GitFetch: refs/heads/i386 46 | ppc64le-Directory: 1.5/ppc64le/ 47 | 48 | 49 | Tags: 1.5 50 | SharedTags: 1.5-debian 51 | GitCommit: d7e2a8d90a9b8f5dfd5bcd428e0c33b68c40cc19 52 | Directory: 1.5 53 | s390x-GitCommit: b6c460e7cd79b595267870a98013ec3078b490df 54 | i386-GitFetch: refs/heads/i386 55 | ppc64le-Directory: 1.5/ppc64le 56 | 57 | Tags: 1.5-alpine 58 | GitCommit: d7e2a8d90a9b8f5dfd5bcd428e0c33b68c40cc19 59 | Directory: 1.5 60 | File: Dockerfile.alpine 61 | s390x-File: Dockerfile.alpine.s390x.bad-boy 62 | 63 | SharedTags: raspbian 64 | GitCommit: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef 65 | Tags: raspbian-s390x 66 | Architectures: s390x, i386 67 | File: Dockerfile-raspbian 68 | s390x-File: Dockerfile 69 | 70 | 71 | `))) 72 | if err != nil { 73 | panic(err) 74 | } 75 | fmt.Printf("-------------\n2822:\n%s\n", man) 76 | 77 | fmt.Printf("\nShared Tag Groups:\n") 78 | for _, group := range man.GetSharedTagGroups() { 79 | fmt.Printf("\n - %s\n", strings.Join(group.SharedTags, ", ")) 80 | for _, entry := range group.Entries { 81 | fmt.Printf(" - %s\n", entry.TagsString()) 82 | } 83 | } 84 | fmt.Printf("\n") 85 | 86 | man, err = manifest.Parse(bufio.NewReader(strings.NewReader(` 87 | # maintainer: InfoSiftr (@infosiftr) 88 | # maintainer: John Smith (@example-jsmith) 89 | 90 | # first set 91 | a: b@c d 92 | e: b@c d 93 | 94 | # second set 95 | f: g@h 96 | i: g@h j 97 | `))) 98 | if err != nil { 99 | panic(err) 100 | } 101 | fmt.Printf("-------------\nline-based:\n%v\n", man) 102 | 103 | // Output: 104 | // ------------- 105 | // 2822: 106 | // Maintainers: InfoSiftr (@infosiftr), Johan Euphrosine (@proppy) 107 | // SharedTags: latest 108 | // GitRepo: https://github.com/docker-library/golang.git 109 | // arm64v8-GitRepo: https://github.com/docker-library/golang.git 110 | // 111 | // Tags: 1.6.1, 1.6, 1 112 | // GitCommit: 0ce80411b9f41e9c3a21fc0a1bffba6ae761825a 113 | // Directory: 1.6 114 | // Constraints: some-random-build-server 115 | // 116 | // Tags: 1.5.3, 1.5 117 | // SharedTags: 1.5.3-debian, 1.5-debian 118 | // GitCommit: d7e2a8d90a9b8f5dfd5bcd428e0c33b68c40cc19 119 | // Directory: 1.5 120 | // i386-GitFetch: refs/heads/i386 121 | // ppc64le-Directory: 1.5/ppc64le 122 | // s390x-GitCommit: b6c460e7cd79b595267870a98013ec3078b490df 123 | // 124 | // Tags: 1.5-alpine 125 | // GitCommit: d7e2a8d90a9b8f5dfd5bcd428e0c33b68c40cc19 126 | // Directory: 1.5 127 | // File: Dockerfile.alpine 128 | // s390x-File: Dockerfile.alpine.s390x.bad-boy 129 | // 130 | // Tags: raspbian-s390x 131 | // SharedTags: raspbian 132 | // Architectures: i386, s390x 133 | // GitCommit: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef 134 | // File: Dockerfile-raspbian 135 | // s390x-File: Dockerfile 136 | // 137 | // Shared Tag Groups: 138 | // 139 | // - latest 140 | // - 1.6.1, 1.6, 1 141 | // - 1.5-alpine 142 | // 143 | // - 1.5.3-debian, 1.5-debian 144 | // - 1.5.3, 1.5 145 | // 146 | // - raspbian 147 | // - raspbian-s390x 148 | // 149 | // ------------- 150 | // line-based: 151 | // Maintainers: InfoSiftr (@infosiftr), John Smith (@example-jsmith) 152 | // GitFetch: refs/heads/* 153 | // 154 | // Tags: a, e 155 | // GitRepo: b 156 | // GitCommit: c 157 | // Directory: d 158 | // 159 | // Tags: f 160 | // GitRepo: g 161 | // GitFetch: refs/tags/h 162 | // GitCommit: FETCH_HEAD 163 | // 164 | // Tags: i 165 | // GitRepo: g 166 | // GitFetch: refs/tags/h 167 | // GitCommit: FETCH_HEAD 168 | // Directory: j 169 | } 170 | 171 | func ExampleFetch_local() { 172 | repoName, tagName, man, err := manifest.Fetch("testdata", "bash:4.4") 173 | if err != nil { 174 | panic(err) 175 | } 176 | 177 | fmt.Printf("%s:%s\n\n", repoName, tagName) 178 | 179 | fmt.Println(man.GetTag(tagName).ClearDefaults(manifest.DefaultManifestEntry).String()) 180 | 181 | // Output: 182 | // bash:4.4 183 | // 184 | // Maintainers: Tianon Gravi (@tianon) 185 | // Tags: 4.4.12, 4.4, 4, latest 186 | // GitRepo: https://github.com/tianon/docker-bash.git 187 | // GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e 188 | // Directory: 4.4 189 | } 190 | 191 | func ExampleFetch_remote() { 192 | 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") 193 | if err != nil { 194 | panic(err) 195 | } 196 | 197 | fmt.Printf("%s:%s\n\n", repoName, tagName) 198 | 199 | fmt.Println(man.GetTag(tagName).ClearDefaults(manifest.DefaultManifestEntry).String()) 200 | 201 | // Output: 202 | // bash:4.4 203 | // 204 | // Maintainers: Tianon Gravi (@tianon) 205 | // Tags: 4.4.12, 4.4, 4, latest 206 | // GitRepo: https://github.com/tianon/docker-bash.git 207 | // GitCommit: 1cbb5cf49b4c53bd5a986abf7a1afeb9a80eac1e 208 | // Directory: 4.4 209 | } 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2014 Docker, Inc. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /manifest/rfc2822.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "path" 8 | "regexp" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/docker-library/go-dockerlibrary/architecture" 13 | "github.com/docker-library/go-dockerlibrary/pkg/stripper" 14 | 15 | "pault.ag/go/debian/control" 16 | ) 17 | 18 | var ( 19 | GitCommitRegex = regexp.MustCompile(`^[0-9a-f]{1,64}$`) 20 | GitFetchRegex = regexp.MustCompile(`^refs/(heads|tags)/[^*?:]+$`) 21 | 22 | // https://github.com/docker/distribution/blob/v2.7.1/reference/regexp.go#L37 23 | ValidTagRegex = regexp.MustCompile(`^\w[\w.-]{0,127}$`) 24 | ) 25 | 26 | type Manifest2822 struct { 27 | Global Manifest2822Entry 28 | Entries []Manifest2822Entry 29 | } 30 | 31 | type Manifest2822Entry struct { 32 | control.Paragraph 33 | 34 | Maintainers []string `delim:"," strip:"\n\r\t "` 35 | 36 | Tags []string `delim:"," strip:"\n\r\t "` 37 | SharedTags []string `delim:"," strip:"\n\r\t "` 38 | 39 | Architectures []string `delim:"," strip:"\n\r\t "` 40 | 41 | GitRepo string 42 | GitFetch string 43 | GitCommit string 44 | Directory string 45 | File string 46 | 47 | // architecture-specific versions of the above fields 48 | ArchValues map[string]string 49 | // "ARCH-FIELD: VALUE" 50 | // ala, "s390x-GitCommit: deadbeef" 51 | // (sourced from Paragraph.Values via .SeedArchValues()) 52 | 53 | Constraints []string `delim:"," strip:"\n\r\t "` 54 | } 55 | 56 | var ( 57 | DefaultArchitecture = "amd64" 58 | 59 | DefaultManifestEntry = Manifest2822Entry{ 60 | Architectures: []string{DefaultArchitecture}, 61 | 62 | GitFetch: "refs/heads/master", 63 | Directory: ".", 64 | File: "Dockerfile", 65 | } 66 | ) 67 | 68 | func deepCopyStringsMap(a map[string]string) map[string]string { 69 | b := map[string]string{} 70 | for k, v := range a { 71 | b[k] = v 72 | } 73 | return b 74 | } 75 | 76 | func (entry Manifest2822Entry) Clone() Manifest2822Entry { 77 | // SLICES! grr 78 | entry.Maintainers = append([]string{}, entry.Maintainers...) 79 | entry.Tags = append([]string{}, entry.Tags...) 80 | entry.SharedTags = append([]string{}, entry.SharedTags...) 81 | entry.Architectures = append([]string{}, entry.Architectures...) 82 | entry.Constraints = append([]string{}, entry.Constraints...) 83 | // and MAPS, oh my 84 | entry.ArchValues = deepCopyStringsMap(entry.ArchValues) 85 | return entry 86 | } 87 | 88 | func (entry *Manifest2822Entry) SeedArchValues() { 89 | for field, val := range entry.Paragraph.Values { 90 | if strings.HasSuffix(field, "-GitRepo") || strings.HasSuffix(field, "-GitFetch") || strings.HasSuffix(field, "-GitCommit") || strings.HasSuffix(field, "-Directory") || strings.HasSuffix(field, "-File") { 91 | entry.ArchValues[field] = val 92 | } 93 | } 94 | } 95 | func (entry *Manifest2822Entry) CleanDirectoryValues() { 96 | entry.Directory = path.Clean(entry.Directory) 97 | for field, val := range entry.ArchValues { 98 | if strings.HasSuffix(field, "-Directory") && val != "" { 99 | entry.ArchValues[field] = path.Clean(val) 100 | } 101 | } 102 | } 103 | 104 | const StringSeparator2822 = ", " 105 | 106 | func (entry Manifest2822Entry) MaintainersString() string { 107 | return strings.Join(entry.Maintainers, StringSeparator2822) 108 | } 109 | 110 | func (entry Manifest2822Entry) TagsString() string { 111 | return strings.Join(entry.Tags, StringSeparator2822) 112 | } 113 | 114 | func (entry Manifest2822Entry) SharedTagsString() string { 115 | return strings.Join(entry.SharedTags, StringSeparator2822) 116 | } 117 | 118 | func (entry Manifest2822Entry) ArchitecturesString() string { 119 | return strings.Join(entry.Architectures, StringSeparator2822) 120 | } 121 | 122 | func (entry Manifest2822Entry) ConstraintsString() string { 123 | return strings.Join(entry.Constraints, StringSeparator2822) 124 | } 125 | 126 | // if this method returns "true", then a.Tags and b.Tags can safely be combined (for the purposes of building) 127 | func (a Manifest2822Entry) SameBuildArtifacts(b Manifest2822Entry) bool { 128 | // check xxxarch-GitRepo, etc. fields for sameness first 129 | for _, key := range append(a.archFields(), b.archFields()...) { 130 | if a.ArchValues[key] != b.ArchValues[key] { 131 | return false 132 | } 133 | } 134 | 135 | return a.ArchitecturesString() == b.ArchitecturesString() && a.GitRepo == b.GitRepo && a.GitFetch == b.GitFetch && a.GitCommit == b.GitCommit && a.Directory == b.Directory && a.File == b.File && a.ConstraintsString() == b.ConstraintsString() 136 | } 137 | 138 | // returns a list of architecture-specific fields in an Entry 139 | func (entry Manifest2822Entry) archFields() []string { 140 | ret := []string{} 141 | for key, val := range entry.ArchValues { 142 | if val != "" { 143 | ret = append(ret, key) 144 | } 145 | } 146 | sort.Strings(ret) 147 | return ret 148 | } 149 | 150 | // returns a new Entry with any of the values that are equal to the values in "defaults" cleared 151 | func (entry Manifest2822Entry) ClearDefaults(defaults Manifest2822Entry) Manifest2822Entry { 152 | entry = entry.Clone() // make absolutely certain we have a deep clone 153 | if entry.MaintainersString() == defaults.MaintainersString() { 154 | entry.Maintainers = nil 155 | } 156 | if entry.TagsString() == defaults.TagsString() { 157 | entry.Tags = nil 158 | } 159 | if entry.SharedTagsString() == defaults.SharedTagsString() { 160 | entry.SharedTags = nil 161 | } 162 | if entry.ArchitecturesString() == defaults.ArchitecturesString() { 163 | entry.Architectures = nil 164 | } 165 | if entry.GitRepo == defaults.GitRepo { 166 | entry.GitRepo = "" 167 | } 168 | if entry.GitFetch == defaults.GitFetch { 169 | entry.GitFetch = "" 170 | } 171 | if entry.GitCommit == defaults.GitCommit { 172 | entry.GitCommit = "" 173 | } 174 | if entry.Directory == defaults.Directory { 175 | entry.Directory = "" 176 | } 177 | if entry.File == defaults.File { 178 | entry.File = "" 179 | } 180 | for _, key := range defaults.archFields() { 181 | if defaults.ArchValues[key] == entry.ArchValues[key] { 182 | delete(entry.ArchValues, key) 183 | } 184 | } 185 | if entry.ConstraintsString() == defaults.ConstraintsString() { 186 | entry.Constraints = nil 187 | } 188 | return entry 189 | } 190 | 191 | func (entry Manifest2822Entry) String() string { 192 | ret := []string{} 193 | if str := entry.MaintainersString(); str != "" { 194 | ret = append(ret, "Maintainers: "+str) 195 | } 196 | if str := entry.TagsString(); str != "" { 197 | ret = append(ret, "Tags: "+str) 198 | } 199 | if str := entry.SharedTagsString(); str != "" { 200 | ret = append(ret, "SharedTags: "+str) 201 | } 202 | if str := entry.ArchitecturesString(); str != "" { 203 | ret = append(ret, "Architectures: "+str) 204 | } 205 | if str := entry.GitRepo; str != "" { 206 | ret = append(ret, "GitRepo: "+str) 207 | } 208 | if str := entry.GitFetch; str != "" { 209 | ret = append(ret, "GitFetch: "+str) 210 | } 211 | if str := entry.GitCommit; str != "" { 212 | ret = append(ret, "GitCommit: "+str) 213 | } 214 | if str := entry.Directory; str != "" { 215 | ret = append(ret, "Directory: "+str) 216 | } 217 | if str := entry.File; str != "" { 218 | ret = append(ret, "File: "+str) 219 | } 220 | for _, key := range entry.archFields() { 221 | ret = append(ret, key+": "+entry.ArchValues[key]) 222 | } 223 | if str := entry.ConstraintsString(); str != "" { 224 | ret = append(ret, "Constraints: "+str) 225 | } 226 | return strings.Join(ret, "\n") 227 | } 228 | 229 | func (manifest Manifest2822) String() string { 230 | entries := []Manifest2822Entry{manifest.Global.ClearDefaults(DefaultManifestEntry)} 231 | entries = append(entries, manifest.Entries...) 232 | 233 | ret := []string{} 234 | for i, entry := range entries { 235 | if i > 0 { 236 | entry = entry.ClearDefaults(manifest.Global) 237 | } 238 | ret = append(ret, entry.String()) 239 | } 240 | 241 | return strings.Join(ret, "\n\n") 242 | } 243 | 244 | func (entry *Manifest2822Entry) SetGitRepo(arch string, repo string) { 245 | if entry.ArchValues == nil { 246 | entry.ArchValues = map[string]string{} 247 | } 248 | entry.ArchValues[arch+"-GitRepo"] = repo 249 | } 250 | 251 | func (entry Manifest2822Entry) ArchGitRepo(arch string) string { 252 | if val, ok := entry.ArchValues[arch+"-GitRepo"]; ok && val != "" { 253 | return val 254 | } 255 | return entry.GitRepo 256 | } 257 | 258 | func (entry Manifest2822Entry) ArchGitFetch(arch string) string { 259 | if val, ok := entry.ArchValues[arch+"-GitFetch"]; ok && val != "" { 260 | return val 261 | } 262 | return entry.GitFetch 263 | } 264 | 265 | func (entry *Manifest2822Entry) SetGitCommit(arch string, commit string) { 266 | if entry.ArchValues == nil { 267 | entry.ArchValues = map[string]string{} 268 | } 269 | entry.ArchValues[arch+"-GitCommit"] = commit 270 | } 271 | 272 | func (entry Manifest2822Entry) ArchGitCommit(arch string) string { 273 | if val, ok := entry.ArchValues[arch+"-GitCommit"]; ok && val != "" { 274 | return val 275 | } 276 | return entry.GitCommit 277 | } 278 | 279 | func (entry Manifest2822Entry) ArchDirectory(arch string) string { 280 | if val, ok := entry.ArchValues[arch+"-Directory"]; ok && val != "" { 281 | return val 282 | } 283 | return entry.Directory 284 | } 285 | 286 | func (entry Manifest2822Entry) ArchFile(arch string) string { 287 | if val, ok := entry.ArchValues[arch+"-File"]; ok && val != "" { 288 | return val 289 | } 290 | return entry.File 291 | } 292 | 293 | func (entry Manifest2822Entry) HasTag(tag string) bool { 294 | for _, existingTag := range entry.Tags { 295 | if tag == existingTag { 296 | return true 297 | } 298 | } 299 | return false 300 | } 301 | 302 | // HasSharedTag returns true if the given tag exists in entry.SharedTags. 303 | func (entry Manifest2822Entry) HasSharedTag(tag string) bool { 304 | for _, existingTag := range entry.SharedTags { 305 | if tag == existingTag { 306 | return true 307 | } 308 | } 309 | return false 310 | } 311 | 312 | // HasArchitecture returns true if the given architecture exists in entry.Architectures 313 | func (entry Manifest2822Entry) HasArchitecture(arch string) bool { 314 | for _, existingArch := range entry.Architectures { 315 | if arch == existingArch { 316 | return true 317 | } 318 | } 319 | return false 320 | } 321 | 322 | func (manifest Manifest2822) GetTag(tag string) *Manifest2822Entry { 323 | for i, entry := range manifest.Entries { 324 | if entry.HasTag(tag) { 325 | return &manifest.Entries[i] 326 | } 327 | } 328 | return nil 329 | } 330 | 331 | // 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). 332 | func (manifest Manifest2822) GetSharedTag(tag string) []*Manifest2822Entry { 333 | ret := []*Manifest2822Entry{} 334 | for i, entry := range manifest.Entries { 335 | if entry.HasSharedTag(tag) { 336 | ret = append(ret, &manifest.Entries[i]) 337 | } 338 | } 339 | return ret 340 | } 341 | 342 | // 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). 343 | func (manifest Manifest2822) GetAllSharedTags() []string { 344 | fakeEntry := Manifest2822Entry{} 345 | for _, entry := range manifest.Entries { 346 | fakeEntry.SharedTags = append(fakeEntry.SharedTags, entry.SharedTags...) 347 | } 348 | fakeEntry.DeduplicateSharedTags() 349 | return fakeEntry.SharedTags 350 | } 351 | 352 | type SharedTagGroup struct { 353 | SharedTags []string 354 | Entries []*Manifest2822Entry 355 | } 356 | 357 | // 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). 358 | func (manifest Manifest2822) GetSharedTagGroups() []SharedTagGroup { 359 | inter := map[string][]string{} 360 | interOrder := []string{} // order matters, and maps randomize order 361 | interKeySep := "," 362 | for _, sharedTag := range manifest.GetAllSharedTags() { 363 | interKeyParts := []string{} 364 | for _, entry := range manifest.GetSharedTag(sharedTag) { 365 | interKeyParts = append(interKeyParts, entry.Tags[0]) 366 | } 367 | interKey := strings.Join(interKeyParts, interKeySep) 368 | if _, ok := inter[interKey]; !ok { 369 | interOrder = append(interOrder, interKey) 370 | } 371 | inter[interKey] = append(inter[interKey], sharedTag) 372 | } 373 | ret := []SharedTagGroup{} 374 | for _, tags := range interOrder { 375 | group := SharedTagGroup{ 376 | SharedTags: inter[tags], 377 | Entries: []*Manifest2822Entry{}, 378 | } 379 | for _, tag := range strings.Split(tags, interKeySep) { 380 | group.Entries = append(group.Entries, manifest.GetTag(tag)) 381 | } 382 | ret = append(ret, group) 383 | } 384 | return ret 385 | } 386 | 387 | func (manifest *Manifest2822) AddEntry(entry Manifest2822Entry) error { 388 | if len(entry.Tags) < 1 { 389 | return fmt.Errorf("missing Tags") 390 | } 391 | if entry.GitRepo == "" || entry.GitFetch == "" || entry.GitCommit == "" { 392 | return fmt.Errorf("Tags %q missing one of GitRepo, GitFetch, or GitCommit", entry.TagsString()) 393 | } 394 | if invalidMaintainers := entry.InvalidMaintainers(); len(invalidMaintainers) > 0 { 395 | return fmt.Errorf("Tags %q has invalid Maintainers: %q (expected format %q)", entry.TagsString(), strings.Join(invalidMaintainers, ", "), MaintainersFormat) 396 | } 397 | 398 | entry.DeduplicateSharedTags() 399 | entry.CleanDirectoryValues() 400 | 401 | if invalidTags := entry.InvalidTags(); len(invalidTags) > 0 { 402 | return fmt.Errorf("Tags %q has invalid (Shared)Tags: %q", entry.TagsString(), strings.Join(invalidTags, ", ")) 403 | } 404 | if invalidArchitectures := entry.InvalidArchitectures(); len(invalidArchitectures) > 0 { 405 | return fmt.Errorf("Tags %q has invalid Architectures: %q", entry.TagsString(), strings.Join(invalidArchitectures, ", ")) 406 | } 407 | 408 | seenTag := map[string]bool{} 409 | for _, tag := range entry.Tags { 410 | if otherEntry := manifest.GetTag(tag); otherEntry != nil { 411 | return fmt.Errorf("Tags %q includes duplicate tag: %q (duplicated in %q)", entry.TagsString(), tag, otherEntry.TagsString()) 412 | } 413 | if otherEntries := manifest.GetSharedTag(tag); len(otherEntries) > 0 { 414 | return fmt.Errorf("Tags %q includes tag conflicting with a shared tag: %q (shared tag in %q)", entry.TagsString(), tag, otherEntries[0].TagsString()) 415 | } 416 | if seenTag[tag] { 417 | return fmt.Errorf("Tags %q includes duplicate tag: %q", entry.TagsString(), tag) 418 | } 419 | seenTag[tag] = true 420 | } 421 | for _, tag := range entry.SharedTags { 422 | if otherEntry := manifest.GetTag(tag); otherEntry != nil { 423 | return fmt.Errorf("Tags %q includes conflicting shared tag: %q (duplicated in %q)", entry.TagsString(), tag, otherEntry.TagsString()) 424 | } 425 | if seenTag[tag] { 426 | return fmt.Errorf("Tags %q includes duplicate tag: %q (in SharedTags)", entry.TagsString(), tag) 427 | } 428 | seenTag[tag] = true 429 | } 430 | 431 | for i, existingEntry := range manifest.Entries { 432 | if existingEntry.SameBuildArtifacts(entry) { 433 | manifest.Entries[i].Tags = append(existingEntry.Tags, entry.Tags...) 434 | manifest.Entries[i].SharedTags = append(existingEntry.SharedTags, entry.SharedTags...) 435 | manifest.Entries[i].DeduplicateSharedTags() 436 | return nil 437 | } 438 | } 439 | 440 | manifest.Entries = append(manifest.Entries, entry) 441 | 442 | return nil 443 | } 444 | 445 | const ( 446 | MaintainersNameRegex = `[^\s<>()][^<>()]*` 447 | MaintainersEmailRegex = `[^\s<>()]+` 448 | MaintainersGitHubRegex = `[^\s<>()]+` 449 | 450 | MaintainersFormat = `Full Name (@github-handle) OR Full Name (@github-handle)` 451 | ) 452 | 453 | var ( 454 | MaintainersRegex = regexp.MustCompile(`^(` + MaintainersNameRegex + `)(?:\s+<(` + MaintainersEmailRegex + `)>)?\s+[(]@(` + MaintainersGitHubRegex + `)[)]$`) 455 | ) 456 | 457 | func (entry Manifest2822Entry) InvalidMaintainers() []string { 458 | invalid := []string{} 459 | for _, maintainer := range entry.Maintainers { 460 | if !MaintainersRegex.MatchString(maintainer) { 461 | invalid = append(invalid, maintainer) 462 | } 463 | } 464 | return invalid 465 | } 466 | 467 | func (entry Manifest2822Entry) InvalidTags() []string { 468 | invalid := []string{} 469 | for _, tag := range append(append([]string{}, entry.Tags...), entry.SharedTags...) { 470 | if !ValidTagRegex.MatchString(tag) { 471 | invalid = append(invalid, tag) 472 | } 473 | } 474 | return invalid 475 | } 476 | 477 | func (entry Manifest2822Entry) InvalidArchitectures() []string { 478 | invalid := []string{} 479 | for _, arch := range entry.Architectures { 480 | if _, ok := architecture.SupportedArches[arch]; !ok { 481 | invalid = append(invalid, arch) 482 | } 483 | } 484 | return invalid 485 | } 486 | 487 | // DeduplicateSharedTags will remove duplicate values from entry.SharedTags, preserving order. 488 | func (entry *Manifest2822Entry) DeduplicateSharedTags() { 489 | aggregate := []string{} 490 | seen := map[string]bool{} 491 | for _, tag := range entry.SharedTags { 492 | if seen[tag] { 493 | continue 494 | } 495 | seen[tag] = true 496 | aggregate = append(aggregate, tag) 497 | } 498 | entry.SharedTags = aggregate 499 | } 500 | 501 | // DeduplicateArchitectures will remove duplicate values from entry.Architectures and sort the result. 502 | func (entry *Manifest2822Entry) DeduplicateArchitectures() { 503 | aggregate := []string{} 504 | seen := map[string]bool{} 505 | for _, arch := range entry.Architectures { 506 | if seen[arch] { 507 | continue 508 | } 509 | seen[arch] = true 510 | aggregate = append(aggregate, arch) 511 | } 512 | sort.Strings(aggregate) 513 | entry.Architectures = aggregate 514 | } 515 | 516 | type decoderWrapper struct { 517 | *control.Decoder 518 | } 519 | 520 | func (decoder *decoderWrapper) Decode(entry *Manifest2822Entry) error { 521 | // reset Architectures and SharedTags so that they can be either inherited or replaced, not additive 522 | sharedTags := entry.SharedTags 523 | entry.SharedTags = nil 524 | arches := entry.Architectures 525 | entry.Architectures = nil 526 | 527 | for { 528 | err := decoder.Decoder.Decode(entry) 529 | if err != nil { 530 | return err 531 | } 532 | 533 | // ignore empty paragraphs (blank lines at the start, excess blank lines between paragraphs, excess blank lines at EOF) 534 | if len(entry.Paragraph.Order) == 0 { 535 | continue 536 | } 537 | 538 | // if we had no SharedTags or Architectures, restore our "default" (original) values 539 | if len(entry.SharedTags) == 0 { 540 | entry.SharedTags = sharedTags 541 | } 542 | if len(entry.Architectures) == 0 { 543 | entry.Architectures = arches 544 | } 545 | entry.DeduplicateArchitectures() 546 | 547 | // pull out any new architecture-specific values from Paragraph.Values 548 | entry.SeedArchValues() 549 | 550 | return nil 551 | } 552 | } 553 | 554 | func Parse2822(readerIn io.Reader) (*Manifest2822, error) { 555 | reader := stripper.NewCommentStripper(readerIn) 556 | 557 | realDecoder, err := control.NewDecoder(bufio.NewReader(reader), nil) 558 | if err != nil { 559 | return nil, err 560 | } 561 | decoder := decoderWrapper{realDecoder} 562 | 563 | manifest := Manifest2822{ 564 | Global: DefaultManifestEntry.Clone(), 565 | } 566 | 567 | if err := decoder.Decode(&manifest.Global); err != nil { 568 | return nil, err 569 | } 570 | if len(manifest.Global.Maintainers) < 1 { 571 | return nil, fmt.Errorf("missing Maintainers") 572 | } 573 | if invalidMaintainers := manifest.Global.InvalidMaintainers(); len(invalidMaintainers) > 0 { 574 | return nil, fmt.Errorf("invalid Maintainers: %q (expected format %q)", strings.Join(invalidMaintainers, ", "), MaintainersFormat) 575 | } 576 | if len(manifest.Global.Tags) > 0 { 577 | return nil, fmt.Errorf("global Tags not permitted") 578 | } 579 | if invalidArchitectures := manifest.Global.InvalidArchitectures(); len(invalidArchitectures) > 0 { 580 | return nil, fmt.Errorf("invalid global Architectures: %q", strings.Join(invalidArchitectures, ", ")) 581 | } 582 | 583 | for { 584 | entry := manifest.Global.Clone() 585 | 586 | err := decoder.Decode(&entry) 587 | if err == io.EOF { 588 | break 589 | } 590 | if err != nil { 591 | return nil, err 592 | } 593 | 594 | if !GitFetchRegex.MatchString(entry.GitFetch) { 595 | return nil, fmt.Errorf(`Tags %q has invalid GitFetch (must be "refs/heads/..." or "refs/tags/..."): %q`, entry.TagsString(), entry.GitFetch) 596 | } 597 | if !GitCommitRegex.MatchString(entry.GitCommit) { 598 | return nil, fmt.Errorf(`Tags %q has invalid GitCommit (must be a commit, not a tag or ref): %q`, entry.TagsString(), entry.GitCommit) 599 | } 600 | 601 | err = manifest.AddEntry(entry) 602 | if err != nil { 603 | return nil, err 604 | } 605 | } 606 | 607 | return &manifest, nil 608 | } 609 | --------------------------------------------------------------------------------