Skip to content

Commit 0c6df94

Browse files
committed
Move Dockerfile parsing to a dedicated package
Also, add a bunch of test cases / code coverage
1 parent 98f6610 commit 0c6df94

File tree

4 files changed

+281
-120
lines changed

4 files changed

+281
-120
lines changed

cmd/bashbrew/docker.go

Lines changed: 7 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package main
22

33
import (
4-
"bufio"
54
"bytes"
65
"crypto/sha256"
76
"encoding/hex"
@@ -10,21 +9,13 @@ import (
109
"os"
1110
"os/exec"
1211
"path"
13-
"strconv"
1412
"strings"
1513

1614
"github.com/docker-library/bashbrew/manifest"
15+
"github.com/docker-library/bashbrew/pkg/dockerfile"
1716
"github.com/urfave/cli"
1817
)
1918

20-
type dockerfileMetadata struct {
21-
StageFroms []string // every image "FROM" instruction value (or the parent stage's FROM value in the case of a named stage)
22-
StageNames []string // the name of any named stage (in order)
23-
StageNameFroms map[string]string // map of stage names to FROM values (or the parent stage's FROM value in the case of a named stage), useful for resolving stage names to FROM values
24-
25-
Froms []string // every "FROM" or "COPY --from=xxx" value (minus named and/or numbered stages in the case of "--from=")
26-
}
27-
2819
// this returns the "FROM" value for the last stage (which essentially determines the "base" for the final published image)
2920
func (r Repo) ArchLastStageFrom(arch string, entry *manifest.Manifest2822Entry) (string, error) {
3021
dockerfileMeta, err := r.archDockerfileMetadata(arch, entry)
@@ -46,15 +37,15 @@ func (r Repo) ArchDockerFroms(arch string, entry *manifest.Manifest2822Entry) ([
4637
return dockerfileMeta.Froms, nil
4738
}
4839

49-
func (r Repo) dockerfileMetadata(entry *manifest.Manifest2822Entry) (*dockerfileMetadata, error) {
40+
func (r Repo) dockerfileMetadata(entry *manifest.Manifest2822Entry) (*dockerfile.Metadata, error) {
5041
return r.archDockerfileMetadata(arch, entry)
5142
}
5243

53-
var dockerfileMetadataCache = map[string]*dockerfileMetadata{}
44+
var dockerfileMetadataCache = map[string]*dockerfile.Metadata{}
5445

55-
func (r Repo) archDockerfileMetadata(arch string, entry *manifest.Manifest2822Entry) (*dockerfileMetadata, error) {
46+
func (r Repo) archDockerfileMetadata(arch string, entry *manifest.Manifest2822Entry) (*dockerfile.Metadata, error) {
5647
if builder := entry.ArchBuilder(arch); builder == "oci-import" {
57-
return &dockerfileMetadata{
48+
return &dockerfile.Metadata{
5849
StageFroms: []string{
5950
"scratch",
6051
},
@@ -79,12 +70,12 @@ func (r Repo) archDockerfileMetadata(arch string, entry *manifest.Manifest2822En
7970
return meta, nil
8071
}
8172

82-
dockerfile, err := gitShow(commit, dockerfileFile)
73+
df, err := gitShow(commit, dockerfileFile)
8374
if err != nil {
8475
return nil, cli.NewMultiError(fmt.Errorf(`failed "git show" for %q from commit %q`, dockerfileFile, commit), err)
8576
}
8677

87-
meta, err := parseDockerfileMetadata(dockerfile)
78+
meta, err := dockerfile.Parse(df)
8879
if err != nil {
8980
return nil, cli.NewMultiError(fmt.Errorf(`failed parsing Dockerfile metadata for %q from commit %q`, dockerfileFile, commit), err)
9081
}
@@ -93,102 +84,6 @@ func (r Repo) archDockerfileMetadata(arch string, entry *manifest.Manifest2822En
9384
return meta, nil
9485
}
9586

96-
func parseDockerfileMetadata(dockerfile string) (*dockerfileMetadata, error) {
97-
meta := &dockerfileMetadata{
98-
// panic: assignment to entry in nil map
99-
StageNameFroms: map[string]string{},
100-
// (nil slices work fine)
101-
}
102-
103-
scanner := bufio.NewScanner(strings.NewReader(dockerfile))
104-
for scanner.Scan() {
105-
line := strings.TrimSpace(scanner.Text())
106-
107-
if line == "" {
108-
// ignore blank lines
109-
continue
110-
}
111-
112-
if line[0] == '#' {
113-
// TODO handle "escape" parser directive
114-
// TODO handle "syntax" parser directive -- explode appropriately (since custom syntax invalidates our Dockerfile parsing)
115-
// ignore comments
116-
continue
117-
}
118-
119-
// handle line continuations
120-
// (TODO see note above regarding "escape" parser directive)
121-
for line[len(line)-1] == '\\' && scanner.Scan() {
122-
nextLine := strings.TrimSpace(scanner.Text())
123-
if nextLine == "" || nextLine[0] == '#' {
124-
// ignore blank lines and comments
125-
continue
126-
}
127-
line = line[0:len(line)-1] + nextLine
128-
}
129-
130-
fields := strings.Fields(line)
131-
if len(fields) < 1 {
132-
// must be a much more complex empty line??
133-
continue
134-
}
135-
instruction := strings.ToUpper(fields[0])
136-
137-
// TODO balk at ARG / $ in from values
138-
139-
switch instruction {
140-
case "FROM":
141-
from := fields[1]
142-
143-
if stageFrom, ok := meta.StageNameFroms[from]; ok {
144-
// if this is a valid stage name, we should resolve it back to the original FROM value of that previous stage (we don't care about inter-stage dependencies for the purposes of either tag dependency calculation or tag building -- just how many there are and what external things they require)
145-
from = stageFrom
146-
}
147-
148-
// make sure to add ":latest" if it's implied
149-
from = latestizeRepoTag(from)
150-
151-
meta.StageFroms = append(meta.StageFroms, from)
152-
meta.Froms = append(meta.Froms, from)
153-
154-
if len(fields) == 4 && strings.ToUpper(fields[2]) == "AS" {
155-
stageName := fields[3]
156-
meta.StageNames = append(meta.StageNames, stageName)
157-
meta.StageNameFroms[stageName] = from
158-
}
159-
case "COPY":
160-
for _, arg := range fields[1:] {
161-
if !strings.HasPrefix(arg, "--") {
162-
// doesn't appear to be a "flag"; time to bail!
163-
break
164-
}
165-
if !strings.HasPrefix(arg, "--from=") {
166-
// ignore any flags we're not interested in
167-
continue
168-
}
169-
from := arg[len("--from="):]
170-
171-
if stageFrom, ok := meta.StageNameFroms[from]; ok {
172-
// see note above regarding stage names in FROM
173-
from = stageFrom
174-
} else if stageNumber, err := strconv.Atoi(from); err == nil && stageNumber < len(meta.StageFroms) {
175-
// must be a stage number, we should resolve it too
176-
from = meta.StageFroms[stageNumber]
177-
}
178-
179-
// make sure to add ":latest" if it's implied
180-
from = latestizeRepoTag(from)
181-
182-
meta.Froms = append(meta.Froms, from)
183-
}
184-
}
185-
}
186-
if err := scanner.Err(); err != nil {
187-
return nil, err
188-
}
189-
return meta, nil
190-
}
191-
19287
func (r Repo) DockerCacheName(entry *manifest.Manifest2822Entry) (string, error) {
19388
cacheHash, err := r.dockerCacheHash(entry)
19489
if err != nil {

cmd/bashbrew/repo.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"path"
77
"path/filepath"
88
"sort"
9-
"strings"
109

1110
"github.com/docker-library/bashbrew/manifest"
1211
)
@@ -39,13 +38,6 @@ func repos(all bool, args ...string) ([]string, error) {
3938
return ret, nil
4039
}
4140

42-
func latestizeRepoTag(repoTag string) string {
43-
if repoTag != "scratch" && strings.IndexRune(repoTag, ':') < 0 {
44-
return repoTag + ":latest"
45-
}
46-
return repoTag
47-
}
48-
4941
type Repo struct {
5042
RepoName string
5143
TagName string

pkg/dockerfile/parse.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package dockerfile
2+
3+
import (
4+
"bufio"
5+
"io"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
type Metadata struct {
11+
StageFroms []string // every image "FROM" instruction value (or the parent stage's FROM value in the case of a named stage)
12+
StageNames []string // the name of any named stage (in order)
13+
StageNameFroms map[string]string // map of stage names to FROM values (or the parent stage's FROM value in the case of a named stage), useful for resolving stage names to FROM values
14+
15+
Froms []string // every "FROM" or "COPY --from=xxx" value (minus named and/or numbered stages in the case of "--from=")
16+
}
17+
18+
func Parse(dockerfile string) (*Metadata, error) {
19+
return ParseReader(strings.NewReader(dockerfile))
20+
}
21+
22+
func ParseReader(dockerfile io.Reader) (*Metadata, error) {
23+
meta := &Metadata{
24+
// panic: assignment to entry in nil map
25+
StageNameFroms: map[string]string{},
26+
// (nil slices work fine)
27+
}
28+
29+
scanner := bufio.NewScanner(dockerfile)
30+
for scanner.Scan() {
31+
line := strings.TrimSpace(scanner.Text())
32+
33+
if line == "" {
34+
// ignore blank lines
35+
continue
36+
}
37+
38+
if line[0] == '#' {
39+
// TODO handle "escape" parser directive
40+
// TODO handle "syntax" parser directive -- explode appropriately (since custom syntax invalidates our Dockerfile parsing)
41+
// ignore comments
42+
continue
43+
}
44+
45+
// handle line continuations
46+
// (TODO see note above regarding "escape" parser directive)
47+
for line[len(line)-1] == '\\' && scanner.Scan() {
48+
nextLine := strings.TrimSpace(scanner.Text())
49+
if nextLine == "" || nextLine[0] == '#' {
50+
// ignore blank lines and comments
51+
continue
52+
}
53+
line = line[0:len(line)-1] + nextLine
54+
}
55+
56+
fields := strings.Fields(line)
57+
if len(fields) < 1 {
58+
// must be a much more complex empty line??
59+
continue
60+
}
61+
instruction := strings.ToUpper(fields[0])
62+
63+
// TODO balk at ARG / $ in from values
64+
65+
switch instruction {
66+
case "FROM":
67+
from := fields[1]
68+
69+
if stageFrom, ok := meta.StageNameFroms[from]; ok {
70+
// if this is a valid stage name, we should resolve it back to the original FROM value of that previous stage (we don't care about inter-stage dependencies for the purposes of either tag dependency calculation or tag building -- just how many there are and what external things they require)
71+
from = stageFrom
72+
}
73+
74+
// make sure to add ":latest" if it's implied
75+
from = latestizeRepoTag(from)
76+
77+
meta.StageFroms = append(meta.StageFroms, from)
78+
meta.Froms = append(meta.Froms, from)
79+
80+
if len(fields) == 4 && strings.ToUpper(fields[2]) == "AS" {
81+
stageName := fields[3]
82+
meta.StageNames = append(meta.StageNames, stageName)
83+
meta.StageNameFroms[stageName] = from
84+
}
85+
86+
case "COPY":
87+
for _, arg := range fields[1:] {
88+
if !strings.HasPrefix(arg, "--") {
89+
// doesn't appear to be a "flag"; time to bail!
90+
break
91+
}
92+
if !strings.HasPrefix(arg, "--from=") {
93+
// ignore any flags we're not interested in
94+
continue
95+
}
96+
from := arg[len("--from="):]
97+
98+
if stageFrom, ok := meta.StageNameFroms[from]; ok {
99+
// see note above regarding stage names in FROM
100+
from = stageFrom
101+
} else if stageNumber, err := strconv.Atoi(from); err == nil && stageNumber < len(meta.StageFroms) {
102+
// must be a stage number, we should resolve it too
103+
from = meta.StageFroms[stageNumber]
104+
}
105+
106+
// make sure to add ":latest" if it's implied
107+
from = latestizeRepoTag(from)
108+
109+
meta.Froms = append(meta.Froms, from)
110+
}
111+
}
112+
}
113+
if err := scanner.Err(); err != nil {
114+
return nil, err
115+
}
116+
return meta, nil
117+
}
118+
119+
func latestizeRepoTag(repoTag string) string {
120+
if repoTag != "scratch" && strings.IndexRune(repoTag, ':') < 0 {
121+
return repoTag + ":latest"
122+
}
123+
return repoTag
124+
}

0 commit comments

Comments
 (0)