Skip to content

Inherit ARGs across stages that are based other stages #304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package imagebuilder

import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"

Expand Down Expand Up @@ -288,6 +290,55 @@ type Stage struct {
}

func NewStages(node *parser.Node, b *Builder) (Stages, error) {
getStageFrom := func(stageIndex int, root *parser.Node) (from string, as string, err error) {
for _, child := range root.Children {
if !strings.EqualFold(child.Value, command.From) {
continue
}
if child.Next == nil {
return "", "", errors.New("FROM requires an argument")
}
if child.Next.Value == "" {
return "", "", errors.New("FROM requires a non-empty argument")
}
from = child.Next.Value
if name, ok := extractNameFromNode(child); ok {
as = name
}
return from, as, nil
}
return "", "", fmt.Errorf("stage %d requires a FROM instruction (%q)", stageIndex+1, root.Original)
}
argInstructionsInStages := make(map[string][]string)
setStageInheritedArgs := func(s *Stage) error {
from, as, err := getStageFrom(s.Position, s.Node)
if err != nil {
return err
}
inheritedArgs := argInstructionsInStages[from]
thisStageArgs := slices.Clone(inheritedArgs)
for _, child := range s.Node.Children {
if !strings.EqualFold(child.Value, command.Arg) {
continue
}
if child.Next == nil {
return errors.New("ARG requires an argument")
}
if child.Next.Value == "" {
return errors.New("ARG requires a non-empty argument")
}
next := child.Next
for next != nil {
thisStageArgs = append(thisStageArgs, next.Value)
next = next.Next
}
}
if as != "" {
argInstructionsInStages[as] = thisStageArgs
}
argInstructionsInStages[strconv.Itoa(s.Position)] = thisStageArgs
return arg(s.Builder, inheritedArgs, nil, nil, "", nil)
}
var stages Stages
var headingArgs []string
if err := b.extractHeadingArgsFromNode(node); err != nil {
Expand All @@ -297,8 +348,8 @@ func NewStages(node *parser.Node, b *Builder) (Stages, error) {
headingArgs = append(headingArgs, k)
}
for i, root := range SplitBy(node, command.From) {
name, _ := extractNameFromNode(root.Children[0])
if len(name) == 0 {
name, hasName := extractNameFromNode(root.Children[0])
if !hasName {
name = strconv.Itoa(i)
}
filteredUserArgs := make(map[string]string)
Expand All @@ -317,12 +368,16 @@ func NewStages(node *parser.Node, b *Builder) (Stages, error) {
if err != nil {
return nil, err
}
stages = append(stages, Stage{
stage := Stage{
Position: i,
Name: processedName,
Builder: b.builderForStage(headingArgs),
Node: root,
})
}
if err := setStageInheritedArgs(&stage); err != nil {
return nil, err
}
stages = append(stages, stage)
}
return stages, nil
}
Expand Down
29 changes: 27 additions & 2 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,13 +523,14 @@ func TestMultiStageArgScope(t *testing.T) {
args := map[string]string{
"SECRET": "secretthings",
"BAR": "notsecretthings",
"UNUSED": "notrightawayanyway",
}
stages, err := NewStages(n, NewBuilder(args))
if err != nil {
t.Fatal(err)
}
if len(stages) != 2 {
t.Fatalf("expected 2 stages, got %d", len(stages))
if len(stages) != 3 {
t.Fatalf("expected 3 stages, got %d", len(stages))
}

for _, stage := range stages {
Expand Down Expand Up @@ -562,6 +563,30 @@ func TestMultiStageArgScope(t *testing.T) {
if !builderHasArgument(stages[1].Builder, "BAR=notsecretthings") {
t.Fatalf("expected BAR=notsecretthings to be present in second stage arguments list: %v", secondStageArguments)
}

thirdStageArguments := stages[2].Builder.Arguments()
inheritedInThirdStage := false
unusedInThirdStage := false
for _, arg := range thirdStageArguments {
if match, err := regexp.MatchString(`INHERITED=.*`, arg); err == nil && match {
inheritedInThirdStage = true
continue
} else if err != nil {
t.Fatal(err)
}
if match, err := regexp.MatchString(`UNUSED=.*`, arg); err == nil && match {
unusedInThirdStage = true
continue
} else if err != nil {
t.Fatal(err)
}
}
if !inheritedInThirdStage {
t.Fatalf("expected INHERITED to be present in third stage")
}
if !unusedInThirdStage {
t.Fatalf("expected UNUSED to be present in third stage")
}
}

func TestRun(t *testing.T) {
Expand Down
55 changes: 41 additions & 14 deletions dockerclient/conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,7 @@ func equivalentImages(t *testing.T, c *docker.Client, a, b string, testFilesyste

// for mutation commands, check the layer diff
if testFilesystem {
differs, onlyA, onlyB, err := compareImageFS(c, a, b)
differs, contents, onlyA, onlyB, err := compareImageFS(c, a, b)
if err != nil {
t.Errorf("can't calculate FS differences %q: %v", a, err)
return false
Expand All @@ -1143,7 +1143,11 @@ func equivalentImages(t *testing.T, c *docker.Client, a, b string, testFilesyste
delete(differs, k)
continue
}
t.Errorf("%s and %s differ at %s:\n%#v\n%#v", a, b, k, v[0].Header, v[1].Header)
if content, ok := contents[k]; ok {
t.Errorf("%s and %s differ at %s:\n%#v\ncontents: %q\n%#v\ncontents: %q", a, b, k, v[0].Header, string(content[0]), v[1].Header, string(content[1]))
} else {
t.Errorf("%s and %s differ at %s:\n%#v\n%#v", a, b, k, v[0].Header, v[1].Header)
}
}
for k, v := range onlyA {
if ignoreFuncs(ignoreFns).Ignore(v.Header, nil) {
Expand Down Expand Up @@ -1222,16 +1226,17 @@ func ignoreDockerfileSize(dockerfile string) ignoreFunc {
// compareImageFS exports the file systems of two images and returns a map
// of files that differ in any way (modification time excluded), only exist in
// image A, or only existing in image B.
func compareImageFS(c *docker.Client, a, b string) (differ map[string][]tarHeader, onlyA, onlyB map[string]tarHeader, err error) {
fsA, err := imageFSMetadata(c, a)
func compareImageFS(c *docker.Client, a, b string) (differ map[string][]tarHeader, differentContent map[string][2][]byte, onlyA, onlyB map[string]tarHeader, err error) {
fsA, contentA, err := imageFSMetadata(c, a)
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
fsB, err := imageFSMetadata(c, b)
fsB, contentB, err := imageFSMetadata(c, b)
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, nil, err
}
differ = make(map[string][]tarHeader)
differentContent = make(map[string][2][]byte)
onlyA = make(map[string]tarHeader)
onlyB = fsB
for k, v1 := range fsA {
Expand All @@ -1246,9 +1251,13 @@ func compareImageFS(c *docker.Client, a, b string) (differ map[string][]tarHeade
v2.ModTime = time.Time{}
if !reflect.DeepEqual(v1, v2) {
differ[k] = []tarHeader{v1, v2}
c1, c2 := contentA[v1.Name], contentB[v2.Name]
if len(c1) > 0 || len(c2) > 0 {
differentContent[v1.Name] = [2][]byte{c1, c2}
}
}
}
return differ, onlyA, onlyB, nil
return differ, differentContent, onlyA, onlyB, nil
}

type tarHeader struct {
Expand All @@ -1264,21 +1273,36 @@ func (h tarHeader) String() string {
}

// imageFSMetadata creates a container and reads the filesystem metadata out of the archive.
func imageFSMetadata(c *docker.Client, name string) (map[string]tarHeader, error) {
func imageFSMetadata(c *docker.Client, name string) (map[string]tarHeader, map[string][]byte, error) {
container, err := c.CreateContainer(docker.CreateContainerOptions{Name: name + "-export", Config: &docker.Config{Image: name}})
if err != nil {
return nil, err
return nil, nil, err
}
defer c.RemoveContainer(docker.RemoveContainerOptions{ID: container.ID, RemoveVolumes: true, Force: true})

ch := make(chan struct{})
result := make(map[string]tarHeader)
tarHeaders := make(map[string]tarHeader)
tarContents := make(map[string][]byte)
r, w := io.Pipe()
go func() {
defer close(ch)
out := tar.NewReader(r)
for {
h, err := out.Next()
if h != nil {
tarHeaders[h.Name] = tarHeader{h}
if h.Size < 512 { // arbitrary size limit
contents, err := io.ReadAll(out)
if err != nil {
w.CloseWithError(err)
break
}
// only ascii
if bytes.IndexFunc(contents, func(r rune) bool { return r > 128 }) == -1 {
tarContents[h.Name] = contents
}
}
}
if err != nil {
if err == io.EOF {
w.Close()
Expand All @@ -1287,14 +1311,17 @@ func imageFSMetadata(c *docker.Client, name string) (map[string]tarHeader, error
}
break
}
result[h.Name] = tarHeader{h}
if h == nil {
w.Close()
break
}
}
}()
if err := c.ExportContainer(docker.ExportContainerOptions{ID: container.ID, OutputStream: w}); err != nil {
return nil, err
return nil, nil, err
}
<-ch
return result, nil
return tarHeaders, tarContents, nil
}

type hardlinkChecker struct {
Expand Down
5 changes: 5 additions & 0 deletions dockerclient/testdata/multistage/Dockerfile.arg-scope
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
FROM alpine
ARG SECRET
ARG UNUSED
ARG INHERITED=set
RUN echo "$SECRET"

FROM alpine
ARG FOO=test
ARG BAR=bartest
RUN echo "$FOO:$BAR"
RUN echo "$SECRET"

FROM 0
RUN echo "$SECRET"