|
@@ -10,81 +10,169 @@ import (
|
|
|
"os"
|
|
|
"os/exec"
|
|
|
"path"
|
|
|
+ "strconv"
|
|
|
"strings"
|
|
|
|
|
|
"github.com/codegangsta/cli"
|
|
|
"github.com/docker-library/go-dockerlibrary/manifest"
|
|
|
)
|
|
|
|
|
|
-var dockerFromCache = map[string]string{}
|
|
|
+type DockerfileMetadata struct {
|
|
|
+ StageFroms []string // every image "FROM" instruction value (or the parent stage's FROM value in the case of a named stage)
|
|
|
+ StageNames []string // the name of any named stage (in order)
|
|
|
+ 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
|
|
|
+
|
|
|
+ Froms []string // every "FROM" or "COPY --from=xxx" value (minus named and/or numbered stages in the case of "--from=")
|
|
|
+}
|
|
|
|
|
|
func (r Repo) DockerFrom(entry *manifest.Manifest2822Entry) (string, error) {
|
|
|
return r.ArchDockerFrom(arch, entry)
|
|
|
}
|
|
|
|
|
|
func (r Repo) ArchDockerFrom(arch string, entry *manifest.Manifest2822Entry) (string, error) {
|
|
|
- commit, err := r.fetchGitRepo(arch, entry)
|
|
|
+ dockerfileMeta, err := r.ArchDockerfileMetadata(arch, entry)
|
|
|
if err != nil {
|
|
|
return "", err
|
|
|
}
|
|
|
|
|
|
+ return dockerfileMeta.StageFroms[len(dockerfileMeta.StageFroms)-1], nil
|
|
|
+}
|
|
|
+
|
|
|
+func (r Repo) DockerfileMetadata(entry *manifest.Manifest2822Entry) (*DockerfileMetadata, error) {
|
|
|
+ return r.ArchDockerfileMetadata(arch, entry)
|
|
|
+}
|
|
|
+
|
|
|
+var dockerfileMetadataCache = map[string]*DockerfileMetadata{}
|
|
|
+
|
|
|
+func (r Repo) ArchDockerfileMetadata(arch string, entry *manifest.Manifest2822Entry) (*DockerfileMetadata, error) {
|
|
|
+ commit, err := r.fetchGitRepo(arch, entry)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
dockerfileFile := path.Join(entry.ArchDirectory(arch), entry.ArchFile(arch))
|
|
|
|
|
|
cacheKey := strings.Join([]string{
|
|
|
commit,
|
|
|
dockerfileFile,
|
|
|
}, "\n")
|
|
|
- if from, ok := dockerFromCache[cacheKey]; ok {
|
|
|
- return from, nil
|
|
|
+ if meta, ok := dockerfileMetadataCache[cacheKey]; ok {
|
|
|
+ return meta, nil
|
|
|
}
|
|
|
|
|
|
dockerfile, err := gitShow(commit, dockerfileFile)
|
|
|
if err != nil {
|
|
|
- return "", err
|
|
|
+ return nil, err
|
|
|
}
|
|
|
defer dockerfile.Close()
|
|
|
|
|
|
- from, err := dockerfileFrom(dockerfile)
|
|
|
+ meta, err := parseDockerfileMetadata(dockerfile)
|
|
|
if err != nil {
|
|
|
- return "", err
|
|
|
+ return nil, err
|
|
|
}
|
|
|
|
|
|
if err := dockerfile.Close(); err != nil {
|
|
|
- return "", err
|
|
|
+ return nil, err
|
|
|
}
|
|
|
|
|
|
- // make sure to add ":latest" if it's implied
|
|
|
- from = latestizeRepoTag(from)
|
|
|
-
|
|
|
- dockerFromCache[cacheKey] = from
|
|
|
- return from, nil
|
|
|
+ dockerfileMetadataCache[cacheKey] = meta
|
|
|
+ return meta, nil
|
|
|
}
|
|
|
|
|
|
-// TODO determine multi-stage-builds impact here (once official images are willing/able to support them; post-17.06 at the earliest)
|
|
|
-func dockerfileFrom(dockerfile io.Reader) (string, error) {
|
|
|
+func parseDockerfileMetadata(dockerfile io.Reader) (*DockerfileMetadata, error) {
|
|
|
+ meta := &DockerfileMetadata{
|
|
|
+ // panic: assignment to entry in nil map
|
|
|
+ StageNameFroms: map[string]string{},
|
|
|
+ // (nil slices work fine)
|
|
|
+ }
|
|
|
+
|
|
|
scanner := bufio.NewScanner(dockerfile)
|
|
|
for scanner.Scan() {
|
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
+
|
|
|
if line == "" {
|
|
|
// ignore blank lines
|
|
|
continue
|
|
|
}
|
|
|
+
|
|
|
if line[0] == '#' {
|
|
|
+ // TODO handle "escape" parser directive
|
|
|
+ // TODO handle "syntax" parser directive -- explode appropriately (since custom syntax invalidates our Dockerfile parsing)
|
|
|
// ignore comments
|
|
|
continue
|
|
|
}
|
|
|
+
|
|
|
+ // handle line continuations
|
|
|
+ // (TODO see note above regarding "escape" parser directive)
|
|
|
+ for line[len(line)-1] == '\\' && scanner.Scan() {
|
|
|
+ nextLine := strings.TrimSpace(scanner.Text())
|
|
|
+ if nextLine == "" || nextLine[0] == '#' {
|
|
|
+ // ignore blank lines and comments
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ line = line[0:len(line)-1]+nextLine
|
|
|
+ }
|
|
|
+
|
|
|
fields := strings.Fields(line)
|
|
|
if len(fields) < 1 {
|
|
|
+ // must be a much more complex empty line??
|
|
|
continue
|
|
|
}
|
|
|
- if strings.ToUpper(fields[0]) == "FROM" {
|
|
|
- return fields[1], nil
|
|
|
+ instruction := strings.ToUpper(fields[0])
|
|
|
+
|
|
|
+ // TODO balk at ARG / $ in from values
|
|
|
+
|
|
|
+ switch instruction {
|
|
|
+ case "FROM":
|
|
|
+ from := fields[1]
|
|
|
+
|
|
|
+ if stageFrom, ok := meta.StageNameFroms[from]; ok {
|
|
|
+ // 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)
|
|
|
+ from = stageFrom
|
|
|
+ }
|
|
|
+
|
|
|
+ // make sure to add ":latest" if it's implied
|
|
|
+ from = latestizeRepoTag(from)
|
|
|
+
|
|
|
+ meta.StageFroms = append(meta.StageFroms, from)
|
|
|
+ meta.Froms = append(meta.Froms, from)
|
|
|
+
|
|
|
+ if len(fields) == 4 && strings.ToUpper(fields[2]) == "AS" {
|
|
|
+ stageName := fields[3]
|
|
|
+ meta.StageNames = append(meta.StageNames, stageName)
|
|
|
+ meta.StageNameFroms[stageName] = from
|
|
|
+ }
|
|
|
+ case "ADD", "COPY":
|
|
|
+ for _, arg := range fields[1:] {
|
|
|
+ if !strings.HasPrefix(arg, "--") {
|
|
|
+ // doesn't appear to be a "flag"; time to bail!
|
|
|
+ break
|
|
|
+ }
|
|
|
+ if !strings.HasPrefix(arg, "--from=") {
|
|
|
+ // ignore any flags we're not interested in
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ from := arg[len("--from="):]
|
|
|
+
|
|
|
+ if stageFrom, ok := meta.StageNameFroms[from]; ok {
|
|
|
+ // see note above regarding stage names in FROM
|
|
|
+ from = stageFrom
|
|
|
+ } else if stageNumber, err := strconv.Atoi(from); err == nil && stageNumber < len(meta.StageFroms) {
|
|
|
+ // must be a stage number, we should resolve it too
|
|
|
+ from = meta.StageFroms[stageNumber]
|
|
|
+ }
|
|
|
+
|
|
|
+ // make sure to add ":latest" if it's implied
|
|
|
+ from = latestizeRepoTag(from)
|
|
|
+
|
|
|
+ meta.Froms = append(meta.Froms, from)
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
- return "", err
|
|
|
+ return nil, err
|
|
|
}
|
|
|
- return "", nil
|
|
|
+ return meta, nil
|
|
|
}
|
|
|
|
|
|
func (r Repo) DockerCacheName(entry *manifest.Manifest2822Entry) (string, error) {
|
|
@@ -121,26 +209,29 @@ var dockerFromIdCache = map[string]string{
|
|
|
}
|
|
|
|
|
|
func (r Repo) dockerBuildUniqueBits(entry *manifest.Manifest2822Entry) ([]string, error) {
|
|
|
- from, err := r.DockerFrom(entry)
|
|
|
+ uniqueBits := []string{
|
|
|
+ entry.ArchGitRepo(arch),
|
|
|
+ entry.ArchGitCommit(arch),
|
|
|
+ entry.ArchDirectory(arch),
|
|
|
+ entry.ArchFile(arch),
|
|
|
+ }
|
|
|
+ meta, err := r.DockerfileMetadata(entry)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
- fromId, ok := dockerFromIdCache[from]
|
|
|
- if !ok {
|
|
|
- var err error
|
|
|
- fromId, err = dockerInspect("{{.Id}}", from)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
+ for _, from := range meta.Froms {
|
|
|
+ fromId, ok := dockerFromIdCache[from]
|
|
|
+ if !ok {
|
|
|
+ var err error
|
|
|
+ fromId, err = dockerInspect("{{.Id}}", from)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ dockerFromIdCache[from] = fromId
|
|
|
}
|
|
|
- dockerFromIdCache[from] = fromId
|
|
|
+ uniqueBits = append(uniqueBits, fromId)
|
|
|
}
|
|
|
- return []string{
|
|
|
- entry.ArchGitRepo(arch),
|
|
|
- entry.ArchGitCommit(arch),
|
|
|
- entry.ArchDirectory(arch),
|
|
|
- entry.ArchFile(arch),
|
|
|
- fromId,
|
|
|
- }, nil
|
|
|
+ return uniqueBits, nil
|
|
|
}
|
|
|
|
|
|
func dockerBuild(tag string, file string, context io.Reader) error {
|