This is a work base to introduce more features like build time
dockerfile optimisations, dependency analysis and parallel build, as
well as a first step to go from a dispatch-inline process to a
frontend+backend process.
Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
| 1 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,183 +0,0 @@ |
| 1 |
-package dockerfile |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "fmt" |
|
| 5 |
- "strings" |
|
| 6 |
-) |
|
| 7 |
- |
|
| 8 |
-// FlagType is the type of the build flag |
|
| 9 |
-type FlagType int |
|
| 10 |
- |
|
| 11 |
-const ( |
|
| 12 |
- boolType FlagType = iota |
|
| 13 |
- stringType |
|
| 14 |
-) |
|
| 15 |
- |
|
| 16 |
-// BFlags contains all flags information for the builder |
|
| 17 |
-type BFlags struct {
|
|
| 18 |
- Args []string // actual flags/args from cmd line |
|
| 19 |
- flags map[string]*Flag |
|
| 20 |
- used map[string]*Flag |
|
| 21 |
- Err error |
|
| 22 |
-} |
|
| 23 |
- |
|
| 24 |
-// Flag contains all information for a flag |
|
| 25 |
-type Flag struct {
|
|
| 26 |
- bf *BFlags |
|
| 27 |
- name string |
|
| 28 |
- flagType FlagType |
|
| 29 |
- Value string |
|
| 30 |
-} |
|
| 31 |
- |
|
| 32 |
-// NewBFlags returns the new BFlags struct |
|
| 33 |
-func NewBFlags() *BFlags {
|
|
| 34 |
- return &BFlags{
|
|
| 35 |
- flags: make(map[string]*Flag), |
|
| 36 |
- used: make(map[string]*Flag), |
|
| 37 |
- } |
|
| 38 |
-} |
|
| 39 |
- |
|
| 40 |
-// NewBFlagsWithArgs returns the new BFlags struct with Args set to args |
|
| 41 |
-func NewBFlagsWithArgs(args []string) *BFlags {
|
|
| 42 |
- flags := NewBFlags() |
|
| 43 |
- flags.Args = args |
|
| 44 |
- return flags |
|
| 45 |
-} |
|
| 46 |
- |
|
| 47 |
-// AddBool adds a bool flag to BFlags |
|
| 48 |
-// Note, any error will be generated when Parse() is called (see Parse). |
|
| 49 |
-func (bf *BFlags) AddBool(name string, def bool) *Flag {
|
|
| 50 |
- flag := bf.addFlag(name, boolType) |
|
| 51 |
- if flag == nil {
|
|
| 52 |
- return nil |
|
| 53 |
- } |
|
| 54 |
- if def {
|
|
| 55 |
- flag.Value = "true" |
|
| 56 |
- } else {
|
|
| 57 |
- flag.Value = "false" |
|
| 58 |
- } |
|
| 59 |
- return flag |
|
| 60 |
-} |
|
| 61 |
- |
|
| 62 |
-// AddString adds a string flag to BFlags |
|
| 63 |
-// Note, any error will be generated when Parse() is called (see Parse). |
|
| 64 |
-func (bf *BFlags) AddString(name string, def string) *Flag {
|
|
| 65 |
- flag := bf.addFlag(name, stringType) |
|
| 66 |
- if flag == nil {
|
|
| 67 |
- return nil |
|
| 68 |
- } |
|
| 69 |
- flag.Value = def |
|
| 70 |
- return flag |
|
| 71 |
-} |
|
| 72 |
- |
|
| 73 |
-// addFlag is a generic func used by the other AddXXX() func |
|
| 74 |
-// to add a new flag to the BFlags struct. |
|
| 75 |
-// Note, any error will be generated when Parse() is called (see Parse). |
|
| 76 |
-func (bf *BFlags) addFlag(name string, flagType FlagType) *Flag {
|
|
| 77 |
- if _, ok := bf.flags[name]; ok {
|
|
| 78 |
- bf.Err = fmt.Errorf("Duplicate flag defined: %s", name)
|
|
| 79 |
- return nil |
|
| 80 |
- } |
|
| 81 |
- |
|
| 82 |
- newFlag := &Flag{
|
|
| 83 |
- bf: bf, |
|
| 84 |
- name: name, |
|
| 85 |
- flagType: flagType, |
|
| 86 |
- } |
|
| 87 |
- bf.flags[name] = newFlag |
|
| 88 |
- |
|
| 89 |
- return newFlag |
|
| 90 |
-} |
|
| 91 |
- |
|
| 92 |
-// IsUsed checks if the flag is used |
|
| 93 |
-func (fl *Flag) IsUsed() bool {
|
|
| 94 |
- if _, ok := fl.bf.used[fl.name]; ok {
|
|
| 95 |
- return true |
|
| 96 |
- } |
|
| 97 |
- return false |
|
| 98 |
-} |
|
| 99 |
- |
|
| 100 |
-// IsTrue checks if a bool flag is true |
|
| 101 |
-func (fl *Flag) IsTrue() bool {
|
|
| 102 |
- if fl.flagType != boolType {
|
|
| 103 |
- // Should never get here |
|
| 104 |
- panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name))
|
|
| 105 |
- } |
|
| 106 |
- return fl.Value == "true" |
|
| 107 |
-} |
|
| 108 |
- |
|
| 109 |
-// Parse parses and checks if the BFlags is valid. |
|
| 110 |
-// Any error noticed during the AddXXX() funcs will be generated/returned |
|
| 111 |
-// here. We do this because an error during AddXXX() is more like a |
|
| 112 |
-// compile time error so it doesn't matter too much when we stop our |
|
| 113 |
-// processing as long as we do stop it, so this allows the code |
|
| 114 |
-// around AddXXX() to be just: |
|
| 115 |
-// defFlag := AddString("description", "")
|
|
| 116 |
-// w/o needing to add an if-statement around each one. |
|
| 117 |
-func (bf *BFlags) Parse() error {
|
|
| 118 |
- // If there was an error while defining the possible flags |
|
| 119 |
- // go ahead and bubble it back up here since we didn't do it |
|
| 120 |
- // earlier in the processing |
|
| 121 |
- if bf.Err != nil {
|
|
| 122 |
- return fmt.Errorf("Error setting up flags: %s", bf.Err)
|
|
| 123 |
- } |
|
| 124 |
- |
|
| 125 |
- for _, arg := range bf.Args {
|
|
| 126 |
- if !strings.HasPrefix(arg, "--") {
|
|
| 127 |
- return fmt.Errorf("Arg should start with -- : %s", arg)
|
|
| 128 |
- } |
|
| 129 |
- |
|
| 130 |
- if arg == "--" {
|
|
| 131 |
- return nil |
|
| 132 |
- } |
|
| 133 |
- |
|
| 134 |
- arg = arg[2:] |
|
| 135 |
- value := "" |
|
| 136 |
- |
|
| 137 |
- index := strings.Index(arg, "=") |
|
| 138 |
- if index >= 0 {
|
|
| 139 |
- value = arg[index+1:] |
|
| 140 |
- arg = arg[:index] |
|
| 141 |
- } |
|
| 142 |
- |
|
| 143 |
- flag, ok := bf.flags[arg] |
|
| 144 |
- if !ok {
|
|
| 145 |
- return fmt.Errorf("Unknown flag: %s", arg)
|
|
| 146 |
- } |
|
| 147 |
- |
|
| 148 |
- if _, ok = bf.used[arg]; ok {
|
|
| 149 |
- return fmt.Errorf("Duplicate flag specified: %s", arg)
|
|
| 150 |
- } |
|
| 151 |
- |
|
| 152 |
- bf.used[arg] = flag |
|
| 153 |
- |
|
| 154 |
- switch flag.flagType {
|
|
| 155 |
- case boolType: |
|
| 156 |
- // value == "" is only ok if no "=" was specified |
|
| 157 |
- if index >= 0 && value == "" {
|
|
| 158 |
- return fmt.Errorf("Missing a value on flag: %s", arg)
|
|
| 159 |
- } |
|
| 160 |
- |
|
| 161 |
- lower := strings.ToLower(value) |
|
| 162 |
- if lower == "" {
|
|
| 163 |
- flag.Value = "true" |
|
| 164 |
- } else if lower == "true" || lower == "false" {
|
|
| 165 |
- flag.Value = lower |
|
| 166 |
- } else {
|
|
| 167 |
- return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value)
|
|
| 168 |
- } |
|
| 169 |
- |
|
| 170 |
- case stringType: |
|
| 171 |
- if index < 0 {
|
|
| 172 |
- return fmt.Errorf("Missing a value on flag: %s", arg)
|
|
| 173 |
- } |
|
| 174 |
- flag.Value = value |
|
| 175 |
- |
|
| 176 |
- default: |
|
| 177 |
- panic("No idea what kind of flag we have! Should never get here!")
|
|
| 178 |
- } |
|
| 179 |
- |
|
| 180 |
- } |
|
| 181 |
- |
|
| 182 |
- return nil |
|
| 183 |
-} |
| 184 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,187 +0,0 @@ |
| 1 |
-package dockerfile |
|
| 2 |
- |
|
| 3 |
-import ( |
|
| 4 |
- "testing" |
|
| 5 |
-) |
|
| 6 |
- |
|
| 7 |
-func TestBuilderFlags(t *testing.T) {
|
|
| 8 |
- var expected string |
|
| 9 |
- var err error |
|
| 10 |
- |
|
| 11 |
- // --- |
|
| 12 |
- |
|
| 13 |
- bf := NewBFlags() |
|
| 14 |
- bf.Args = []string{}
|
|
| 15 |
- if err := bf.Parse(); err != nil {
|
|
| 16 |
- t.Fatalf("Test1 of %q was supposed to work: %s", bf.Args, err)
|
|
| 17 |
- } |
|
| 18 |
- |
|
| 19 |
- // --- |
|
| 20 |
- |
|
| 21 |
- bf = NewBFlags() |
|
| 22 |
- bf.Args = []string{"--"}
|
|
| 23 |
- if err := bf.Parse(); err != nil {
|
|
| 24 |
- t.Fatalf("Test2 of %q was supposed to work: %s", bf.Args, err)
|
|
| 25 |
- } |
|
| 26 |
- |
|
| 27 |
- // --- |
|
| 28 |
- |
|
| 29 |
- bf = NewBFlags() |
|
| 30 |
- flStr1 := bf.AddString("str1", "")
|
|
| 31 |
- flBool1 := bf.AddBool("bool1", false)
|
|
| 32 |
- bf.Args = []string{}
|
|
| 33 |
- if err = bf.Parse(); err != nil {
|
|
| 34 |
- t.Fatalf("Test3 of %q was supposed to work: %s", bf.Args, err)
|
|
| 35 |
- } |
|
| 36 |
- |
|
| 37 |
- if flStr1.IsUsed() {
|
|
| 38 |
- t.Fatal("Test3 - str1 was not used!")
|
|
| 39 |
- } |
|
| 40 |
- if flBool1.IsUsed() {
|
|
| 41 |
- t.Fatal("Test3 - bool1 was not used!")
|
|
| 42 |
- } |
|
| 43 |
- |
|
| 44 |
- // --- |
|
| 45 |
- |
|
| 46 |
- bf = NewBFlags() |
|
| 47 |
- flStr1 = bf.AddString("str1", "HI")
|
|
| 48 |
- flBool1 = bf.AddBool("bool1", false)
|
|
| 49 |
- bf.Args = []string{}
|
|
| 50 |
- |
|
| 51 |
- if err = bf.Parse(); err != nil {
|
|
| 52 |
- t.Fatalf("Test4 of %q was supposed to work: %s", bf.Args, err)
|
|
| 53 |
- } |
|
| 54 |
- |
|
| 55 |
- if flStr1.Value != "HI" {
|
|
| 56 |
- t.Fatal("Str1 was supposed to default to: HI")
|
|
| 57 |
- } |
|
| 58 |
- if flBool1.IsTrue() {
|
|
| 59 |
- t.Fatal("Bool1 was supposed to default to: false")
|
|
| 60 |
- } |
|
| 61 |
- if flStr1.IsUsed() {
|
|
| 62 |
- t.Fatal("Str1 was not used!")
|
|
| 63 |
- } |
|
| 64 |
- if flBool1.IsUsed() {
|
|
| 65 |
- t.Fatal("Bool1 was not used!")
|
|
| 66 |
- } |
|
| 67 |
- |
|
| 68 |
- // --- |
|
| 69 |
- |
|
| 70 |
- bf = NewBFlags() |
|
| 71 |
- flStr1 = bf.AddString("str1", "HI")
|
|
| 72 |
- bf.Args = []string{"--str1"}
|
|
| 73 |
- |
|
| 74 |
- if err = bf.Parse(); err == nil {
|
|
| 75 |
- t.Fatalf("Test %q was supposed to fail", bf.Args)
|
|
| 76 |
- } |
|
| 77 |
- |
|
| 78 |
- // --- |
|
| 79 |
- |
|
| 80 |
- bf = NewBFlags() |
|
| 81 |
- flStr1 = bf.AddString("str1", "HI")
|
|
| 82 |
- bf.Args = []string{"--str1="}
|
|
| 83 |
- |
|
| 84 |
- if err = bf.Parse(); err != nil {
|
|
| 85 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 86 |
- } |
|
| 87 |
- |
|
| 88 |
- expected = "" |
|
| 89 |
- if flStr1.Value != expected {
|
|
| 90 |
- t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected)
|
|
| 91 |
- } |
|
| 92 |
- |
|
| 93 |
- // --- |
|
| 94 |
- |
|
| 95 |
- bf = NewBFlags() |
|
| 96 |
- flStr1 = bf.AddString("str1", "HI")
|
|
| 97 |
- bf.Args = []string{"--str1=BYE"}
|
|
| 98 |
- |
|
| 99 |
- if err = bf.Parse(); err != nil {
|
|
| 100 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 101 |
- } |
|
| 102 |
- |
|
| 103 |
- expected = "BYE" |
|
| 104 |
- if flStr1.Value != expected {
|
|
| 105 |
- t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected)
|
|
| 106 |
- } |
|
| 107 |
- |
|
| 108 |
- // --- |
|
| 109 |
- |
|
| 110 |
- bf = NewBFlags() |
|
| 111 |
- flBool1 = bf.AddBool("bool1", false)
|
|
| 112 |
- bf.Args = []string{"--bool1"}
|
|
| 113 |
- |
|
| 114 |
- if err = bf.Parse(); err != nil {
|
|
| 115 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 116 |
- } |
|
| 117 |
- |
|
| 118 |
- if !flBool1.IsTrue() {
|
|
| 119 |
- t.Fatal("Test-b1 Bool1 was supposed to be true")
|
|
| 120 |
- } |
|
| 121 |
- |
|
| 122 |
- // --- |
|
| 123 |
- |
|
| 124 |
- bf = NewBFlags() |
|
| 125 |
- flBool1 = bf.AddBool("bool1", false)
|
|
| 126 |
- bf.Args = []string{"--bool1=true"}
|
|
| 127 |
- |
|
| 128 |
- if err = bf.Parse(); err != nil {
|
|
| 129 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 130 |
- } |
|
| 131 |
- |
|
| 132 |
- if !flBool1.IsTrue() {
|
|
| 133 |
- t.Fatal("Test-b2 Bool1 was supposed to be true")
|
|
| 134 |
- } |
|
| 135 |
- |
|
| 136 |
- // --- |
|
| 137 |
- |
|
| 138 |
- bf = NewBFlags() |
|
| 139 |
- flBool1 = bf.AddBool("bool1", false)
|
|
| 140 |
- bf.Args = []string{"--bool1=false"}
|
|
| 141 |
- |
|
| 142 |
- if err = bf.Parse(); err != nil {
|
|
| 143 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 144 |
- } |
|
| 145 |
- |
|
| 146 |
- if flBool1.IsTrue() {
|
|
| 147 |
- t.Fatal("Test-b3 Bool1 was supposed to be false")
|
|
| 148 |
- } |
|
| 149 |
- |
|
| 150 |
- // --- |
|
| 151 |
- |
|
| 152 |
- bf = NewBFlags() |
|
| 153 |
- flBool1 = bf.AddBool("bool1", false)
|
|
| 154 |
- bf.Args = []string{"--bool1=false1"}
|
|
| 155 |
- |
|
| 156 |
- if err = bf.Parse(); err == nil {
|
|
| 157 |
- t.Fatalf("Test %q was supposed to fail", bf.Args)
|
|
| 158 |
- } |
|
| 159 |
- |
|
| 160 |
- // --- |
|
| 161 |
- |
|
| 162 |
- bf = NewBFlags() |
|
| 163 |
- flBool1 = bf.AddBool("bool1", false)
|
|
| 164 |
- bf.Args = []string{"--bool2"}
|
|
| 165 |
- |
|
| 166 |
- if err = bf.Parse(); err == nil {
|
|
| 167 |
- t.Fatalf("Test %q was supposed to fail", bf.Args)
|
|
| 168 |
- } |
|
| 169 |
- |
|
| 170 |
- // --- |
|
| 171 |
- |
|
| 172 |
- bf = NewBFlags() |
|
| 173 |
- flStr1 = bf.AddString("str1", "HI")
|
|
| 174 |
- flBool1 = bf.AddBool("bool1", false)
|
|
| 175 |
- bf.Args = []string{"--bool1", "--str1=BYE"}
|
|
| 176 |
- |
|
| 177 |
- if err = bf.Parse(); err != nil {
|
|
| 178 |
- t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 179 |
- } |
|
| 180 |
- |
|
| 181 |
- if flStr1.Value != "BYE" {
|
|
| 182 |
- t.Fatalf("Test %s, str1 should be BYE", bf.Args)
|
|
| 183 |
- } |
|
| 184 |
- if !flBool1.IsTrue() {
|
|
| 185 |
- t.Fatalf("Test %s, bool1 should be true", bf.Args)
|
|
| 186 |
- } |
|
| 187 |
-} |
| ... | ... |
@@ -42,6 +42,26 @@ func newBuildArgs(argsFromOptions map[string]*string) *buildArgs {
|
| 42 | 42 |
} |
| 43 | 43 |
} |
| 44 | 44 |
|
| 45 |
+func (b *buildArgs) Clone() *buildArgs {
|
|
| 46 |
+ result := newBuildArgs(b.argsFromOptions) |
|
| 47 |
+ for k, v := range b.allowedBuildArgs {
|
|
| 48 |
+ result.allowedBuildArgs[k] = v |
|
| 49 |
+ } |
|
| 50 |
+ for k, v := range b.allowedMetaArgs {
|
|
| 51 |
+ result.allowedMetaArgs[k] = v |
|
| 52 |
+ } |
|
| 53 |
+ for k := range b.referencedArgs {
|
|
| 54 |
+ result.referencedArgs[k] = struct{}{}
|
|
| 55 |
+ } |
|
| 56 |
+ return result |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+func (b *buildArgs) MergeReferencedArgs(other *buildArgs) {
|
|
| 60 |
+ for k := range other.referencedArgs {
|
|
| 61 |
+ b.referencedArgs[k] = struct{}{}
|
|
| 62 |
+ } |
|
| 63 |
+} |
|
| 64 |
+ |
|
| 45 | 65 |
// WarnOnUnusedBuildArgs checks if there are any leftover build-args that were |
| 46 | 66 |
// passed but not consumed during build. Print a warning, if there are any. |
| 47 | 67 |
func (b *buildArgs) WarnOnUnusedBuildArgs(out io.Writer) {
|
| ... | ... |
@@ -13,7 +13,7 @@ import ( |
| 13 | 13 |
"github.com/docker/docker/api/types/backend" |
| 14 | 14 |
"github.com/docker/docker/api/types/container" |
| 15 | 15 |
"github.com/docker/docker/builder" |
| 16 |
- "github.com/docker/docker/builder/dockerfile/command" |
|
| 16 |
+ "github.com/docker/docker/builder/dockerfile/instructions" |
|
| 17 | 17 |
"github.com/docker/docker/builder/dockerfile/parser" |
| 18 | 18 |
"github.com/docker/docker/builder/fscache" |
| 19 | 19 |
"github.com/docker/docker/builder/remotecontext" |
| ... | ... |
@@ -41,6 +41,10 @@ var validCommitCommands = map[string]bool{
|
| 41 | 41 |
"workdir": true, |
| 42 | 42 |
} |
| 43 | 43 |
|
| 44 |
+const ( |
|
| 45 |
+ stepFormat = "Step %d/%d : %v" |
|
| 46 |
+) |
|
| 47 |
+ |
|
| 44 | 48 |
// SessionGetter is object used to get access to a session by uuid |
| 45 | 49 |
type SessionGetter interface {
|
| 46 | 50 |
Get(ctx context.Context, uuid string) (session.Caller, error) |
| ... | ... |
@@ -176,9 +180,7 @@ type Builder struct {
|
| 176 | 176 |
clientCtx context.Context |
| 177 | 177 |
|
| 178 | 178 |
idMappings *idtools.IDMappings |
| 179 |
- buildStages *buildStages |
|
| 180 | 179 |
disableCommit bool |
| 181 |
- buildArgs *buildArgs |
|
| 182 | 180 |
imageSources *imageSources |
| 183 | 181 |
pathCache pathCache |
| 184 | 182 |
containerManager *containerManager |
| ... | ... |
@@ -218,8 +220,6 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
|
| 218 | 218 |
Output: options.ProgressWriter.Output, |
| 219 | 219 |
docker: options.Backend, |
| 220 | 220 |
idMappings: options.IDMappings, |
| 221 |
- buildArgs: newBuildArgs(config.BuildArgs), |
|
| 222 |
- buildStages: newBuildStages(), |
|
| 223 | 221 |
imageSources: newImageSources(clientCtx, options), |
| 224 | 222 |
pathCache: options.PathCache, |
| 225 | 223 |
imageProber: newImageProber(options.Backend, config.CacheFrom, options.Platform, config.NoCache), |
| ... | ... |
@@ -237,24 +237,27 @@ func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*buil |
| 237 | 237 |
|
| 238 | 238 |
addNodesForLabelOption(dockerfile.AST, b.options.Labels) |
| 239 | 239 |
|
| 240 |
- if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
|
|
| 241 |
- buildsFailed.WithValues(metricsDockerfileSyntaxError).Inc() |
|
| 240 |
+ stages, metaArgs, err := instructions.Parse(dockerfile.AST) |
|
| 241 |
+ if err != nil {
|
|
| 242 |
+ if instructions.IsUnknownInstruction(err) {
|
|
| 243 |
+ buildsFailed.WithValues(metricsUnknownInstructionError).Inc() |
|
| 244 |
+ } |
|
| 242 | 245 |
return nil, validationError{err}
|
| 243 | 246 |
} |
| 247 |
+ if b.options.Target != "" {
|
|
| 248 |
+ targetIx, found := instructions.HasStage(stages, b.options.Target) |
|
| 249 |
+ if !found {
|
|
| 250 |
+ buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc() |
|
| 251 |
+ return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
|
|
| 252 |
+ } |
|
| 253 |
+ stages = stages[:targetIx+1] |
|
| 254 |
+ } |
|
| 244 | 255 |
|
| 245 |
- dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile, source) |
|
| 256 |
+ dockerfile.PrintWarnings(b.Stderr) |
|
| 257 |
+ dispatchState, err := b.dispatchDockerfileWithCancellation(stages, metaArgs, dockerfile.EscapeToken, source) |
|
| 246 | 258 |
if err != nil {
|
| 247 | 259 |
return nil, err |
| 248 | 260 |
} |
| 249 |
- |
|
| 250 |
- if b.options.Target != "" && !dispatchState.isCurrentStage(b.options.Target) {
|
|
| 251 |
- buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc() |
|
| 252 |
- return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
|
|
| 253 |
- } |
|
| 254 |
- |
|
| 255 |
- dockerfile.PrintWarnings(b.Stderr) |
|
| 256 |
- b.buildArgs.WarnOnUnusedBuildArgs(b.Stderr) |
|
| 257 |
- |
|
| 258 | 261 |
if dispatchState.imageID == "" {
|
| 259 | 262 |
buildsFailed.WithValues(metricsDockerfileEmptyError).Inc() |
| 260 | 263 |
return nil, errors.New("No image was generated. Is your Dockerfile empty?")
|
| ... | ... |
@@ -269,61 +272,91 @@ func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error |
| 269 | 269 |
return aux.Emit(types.BuildResult{ID: state.imageID})
|
| 270 | 270 |
} |
| 271 | 271 |
|
| 272 |
-func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result, source builder.Source) (*dispatchState, error) {
|
|
| 273 |
- shlex := NewShellLex(dockerfile.EscapeToken) |
|
| 274 |
- state := newDispatchState() |
|
| 275 |
- total := len(dockerfile.AST.Children) |
|
| 276 |
- var err error |
|
| 277 |
- for i, n := range dockerfile.AST.Children {
|
|
| 278 |
- select {
|
|
| 279 |
- case <-b.clientCtx.Done(): |
|
| 280 |
- logrus.Debug("Builder: build cancelled!")
|
|
| 281 |
- fmt.Fprint(b.Stdout, "Build cancelled") |
|
| 282 |
- buildsFailed.WithValues(metricsBuildCanceled).Inc() |
|
| 283 |
- return nil, errors.New("Build cancelled")
|
|
| 284 |
- default: |
|
| 285 |
- // Not cancelled yet, keep going... |
|
| 286 |
- } |
|
| 272 |
+func processMetaArg(meta instructions.ArgCommand, shlex *ShellLex, args *buildArgs) error {
|
|
| 273 |
+ // ShellLex currently only support the concatenated string format |
|
| 274 |
+ envs := convertMapToEnvList(args.GetAllAllowed()) |
|
| 275 |
+ if err := meta.Expand(func(word string) (string, error) {
|
|
| 276 |
+ return shlex.ProcessWord(word, envs) |
|
| 277 |
+ }); err != nil {
|
|
| 278 |
+ return err |
|
| 279 |
+ } |
|
| 280 |
+ args.AddArg(meta.Key, meta.Value) |
|
| 281 |
+ args.AddMetaArg(meta.Key, meta.Value) |
|
| 282 |
+ return nil |
|
| 283 |
+} |
|
| 287 | 284 |
|
| 288 |
- // If this is a FROM and we have a previous image then |
|
| 289 |
- // emit an aux message for that image since it is the |
|
| 290 |
- // end of the previous stage |
|
| 291 |
- if n.Value == command.From {
|
|
| 292 |
- if err := emitImageID(b.Aux, state); err != nil {
|
|
| 293 |
- return nil, err |
|
| 294 |
- } |
|
| 285 |
+func printCommand(out io.Writer, currentCommandIndex int, totalCommands int, cmd interface{}) int {
|
|
| 286 |
+ fmt.Fprintf(out, stepFormat, currentCommandIndex, totalCommands, cmd) |
|
| 287 |
+ fmt.Fprintln(out) |
|
| 288 |
+ return currentCommandIndex + 1 |
|
| 289 |
+} |
|
| 290 |
+ |
|
| 291 |
+func (b *Builder) dispatchDockerfileWithCancellation(parseResult []instructions.Stage, metaArgs []instructions.ArgCommand, escapeToken rune, source builder.Source) (*dispatchState, error) {
|
|
| 292 |
+ dispatchRequest := dispatchRequest{}
|
|
| 293 |
+ buildArgs := newBuildArgs(b.options.BuildArgs) |
|
| 294 |
+ totalCommands := len(metaArgs) + len(parseResult) |
|
| 295 |
+ currentCommandIndex := 1 |
|
| 296 |
+ for _, stage := range parseResult {
|
|
| 297 |
+ totalCommands += len(stage.Commands) |
|
| 298 |
+ } |
|
| 299 |
+ shlex := NewShellLex(escapeToken) |
|
| 300 |
+ for _, meta := range metaArgs {
|
|
| 301 |
+ currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, &meta) |
|
| 302 |
+ |
|
| 303 |
+ err := processMetaArg(meta, shlex, buildArgs) |
|
| 304 |
+ if err != nil {
|
|
| 305 |
+ return nil, err |
|
| 295 | 306 |
} |
| 307 |
+ } |
|
| 308 |
+ |
|
| 309 |
+ stagesResults := newStagesBuildResults() |
|
| 296 | 310 |
|
| 297 |
- if n.Value == command.From && state.isCurrentStage(b.options.Target) {
|
|
| 298 |
- break |
|
| 311 |
+ for _, stage := range parseResult {
|
|
| 312 |
+ if err := stagesResults.checkStageNameAvailable(stage.Name); err != nil {
|
|
| 313 |
+ return nil, err |
|
| 299 | 314 |
} |
| 315 |
+ dispatchRequest = newDispatchRequest(b, escapeToken, source, buildArgs, stagesResults) |
|
| 300 | 316 |
|
| 301 |
- opts := dispatchOptions{
|
|
| 302 |
- state: state, |
|
| 303 |
- stepMsg: formatStep(i, total), |
|
| 304 |
- node: n, |
|
| 305 |
- shlex: shlex, |
|
| 306 |
- source: source, |
|
| 317 |
+ currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, stage.SourceCode) |
|
| 318 |
+ if err := initializeStage(dispatchRequest, &stage); err != nil {
|
|
| 319 |
+ return nil, err |
|
| 307 | 320 |
} |
| 308 |
- if state, err = b.dispatch(opts); err != nil {
|
|
| 309 |
- if b.options.ForceRemove {
|
|
| 310 |
- b.containerManager.RemoveAll(b.Stdout) |
|
| 321 |
+ dispatchRequest.state.updateRunConfig() |
|
| 322 |
+ fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID)) |
|
| 323 |
+ for _, cmd := range stage.Commands {
|
|
| 324 |
+ select {
|
|
| 325 |
+ case <-b.clientCtx.Done(): |
|
| 326 |
+ logrus.Debug("Builder: build cancelled!")
|
|
| 327 |
+ fmt.Fprint(b.Stdout, "Build cancelled\n") |
|
| 328 |
+ buildsFailed.WithValues(metricsBuildCanceled).Inc() |
|
| 329 |
+ return nil, errors.New("Build cancelled")
|
|
| 330 |
+ default: |
|
| 331 |
+ // Not cancelled yet, keep going... |
|
| 311 | 332 |
} |
| 333 |
+ |
|
| 334 |
+ currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, cmd) |
|
| 335 |
+ |
|
| 336 |
+ if err := dispatch(dispatchRequest, cmd); err != nil {
|
|
| 337 |
+ return nil, err |
|
| 338 |
+ } |
|
| 339 |
+ |
|
| 340 |
+ dispatchRequest.state.updateRunConfig() |
|
| 341 |
+ fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID)) |
|
| 342 |
+ |
|
| 343 |
+ } |
|
| 344 |
+ if err := emitImageID(b.Aux, dispatchRequest.state); err != nil {
|
|
| 312 | 345 |
return nil, err |
| 313 | 346 |
} |
| 314 |
- |
|
| 315 |
- fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID)) |
|
| 316 |
- if b.options.Remove {
|
|
| 317 |
- b.containerManager.RemoveAll(b.Stdout) |
|
| 347 |
+ buildArgs.MergeReferencedArgs(dispatchRequest.state.buildArgs) |
|
| 348 |
+ if err := commitStage(dispatchRequest.state, stagesResults); err != nil {
|
|
| 349 |
+ return nil, err |
|
| 318 | 350 |
} |
| 319 | 351 |
} |
| 320 |
- |
|
| 321 |
- // Emit a final aux message for the final image |
|
| 322 |
- if err := emitImageID(b.Aux, state); err != nil {
|
|
| 323 |
- return nil, err |
|
| 352 |
+ if b.options.Remove {
|
|
| 353 |
+ b.containerManager.RemoveAll(b.Stdout) |
|
| 324 | 354 |
} |
| 325 |
- |
|
| 326 |
- return state, nil |
|
| 355 |
+ buildArgs.WarnOnUnusedBuildArgs(b.Stdout) |
|
| 356 |
+ return dispatchRequest.state, nil |
|
| 327 | 357 |
} |
| 328 | 358 |
|
| 329 | 359 |
func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) {
|
| ... | ... |
@@ -380,39 +413,33 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con |
| 380 | 380 |
b.Stderr = ioutil.Discard |
| 381 | 381 |
b.disableCommit = true |
| 382 | 382 |
|
| 383 |
- if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
|
|
| 384 |
- return nil, validationError{err}
|
|
| 383 |
+ commands := []instructions.Command{}
|
|
| 384 |
+ for _, n := range dockerfile.AST.Children {
|
|
| 385 |
+ cmd, err := instructions.ParseCommand(n) |
|
| 386 |
+ if err != nil {
|
|
| 387 |
+ return nil, validationError{err}
|
|
| 388 |
+ } |
|
| 389 |
+ commands = append(commands, cmd) |
|
| 385 | 390 |
} |
| 386 |
- dispatchState := newDispatchState() |
|
| 387 |
- dispatchState.runConfig = config |
|
| 388 |
- return dispatchFromDockerfile(b, dockerfile, dispatchState, nil) |
|
| 389 |
-} |
|
| 390 | 391 |
|
| 391 |
-func checkDispatchDockerfile(dockerfile *parser.Node) error {
|
|
| 392 |
- for _, n := range dockerfile.Children {
|
|
| 393 |
- if err := checkDispatch(n); err != nil {
|
|
| 394 |
- return errors.Wrapf(err, "Dockerfile parse error line %d", n.StartLine) |
|
| 392 |
+ dispatchRequest := newDispatchRequest(b, dockerfile.EscapeToken, nil, newBuildArgs(b.options.BuildArgs), newStagesBuildResults()) |
|
| 393 |
+ dispatchRequest.state.runConfig = config |
|
| 394 |
+ dispatchRequest.state.imageID = config.Image |
|
| 395 |
+ for _, cmd := range commands {
|
|
| 396 |
+ err := dispatch(dispatchRequest, cmd) |
|
| 397 |
+ if err != nil {
|
|
| 398 |
+ return nil, validationError{err}
|
|
| 395 | 399 |
} |
| 400 |
+ dispatchRequest.state.updateRunConfig() |
|
| 396 | 401 |
} |
| 397 |
- return nil |
|
| 402 |
+ |
|
| 403 |
+ return dispatchRequest.state.runConfig, nil |
|
| 398 | 404 |
} |
| 399 | 405 |
|
| 400 |
-func dispatchFromDockerfile(b *Builder, result *parser.Result, dispatchState *dispatchState, source builder.Source) (*container.Config, error) {
|
|
| 401 |
- shlex := NewShellLex(result.EscapeToken) |
|
| 402 |
- ast := result.AST |
|
| 403 |
- total := len(ast.Children) |
|
| 404 |
- |
|
| 405 |
- for i, n := range ast.Children {
|
|
| 406 |
- opts := dispatchOptions{
|
|
| 407 |
- state: dispatchState, |
|
| 408 |
- stepMsg: formatStep(i, total), |
|
| 409 |
- node: n, |
|
| 410 |
- shlex: shlex, |
|
| 411 |
- source: source, |
|
| 412 |
- } |
|
| 413 |
- if _, err := b.dispatch(opts); err != nil {
|
|
| 414 |
- return nil, err |
|
| 415 |
- } |
|
| 406 |
+func convertMapToEnvList(m map[string]string) []string {
|
|
| 407 |
+ result := []string{}
|
|
| 408 |
+ for k, v := range m {
|
|
| 409 |
+ result = append(result, k+"="+v) |
|
| 416 | 410 |
} |
| 417 |
- return dispatchState.runConfig, nil |
|
| 411 |
+ return result |
|
| 418 | 412 |
} |
| ... | ... |
@@ -10,17 +10,15 @@ package dockerfile |
| 10 | 10 |
import ( |
| 11 | 11 |
"bytes" |
| 12 | 12 |
"fmt" |
| 13 |
- "regexp" |
|
| 14 | 13 |
"runtime" |
| 15 | 14 |
"sort" |
| 16 |
- "strconv" |
|
| 17 | 15 |
"strings" |
| 18 |
- "time" |
|
| 19 | 16 |
|
| 20 | 17 |
"github.com/docker/docker/api" |
| 21 | 18 |
"github.com/docker/docker/api/types/container" |
| 22 | 19 |
"github.com/docker/docker/api/types/strslice" |
| 23 | 20 |
"github.com/docker/docker/builder" |
| 21 |
+ "github.com/docker/docker/builder/dockerfile/instructions" |
|
| 24 | 22 |
"github.com/docker/docker/builder/dockerfile/parser" |
| 25 | 23 |
"github.com/docker/docker/image" |
| 26 | 24 |
"github.com/docker/docker/pkg/jsonmessage" |
| ... | ... |
@@ -36,32 +34,14 @@ import ( |
| 36 | 36 |
// Sets the environment variable foo to bar, also makes interpolation |
| 37 | 37 |
// in the dockerfile available from the next statement on via ${foo}.
|
| 38 | 38 |
// |
| 39 |
-func env(req dispatchRequest) error {
|
|
| 40 |
- if len(req.args) == 0 {
|
|
| 41 |
- return errAtLeastOneArgument("ENV")
|
|
| 42 |
- } |
|
| 43 |
- |
|
| 44 |
- if len(req.args)%2 != 0 {
|
|
| 45 |
- // should never get here, but just in case |
|
| 46 |
- return errTooManyArguments("ENV")
|
|
| 47 |
- } |
|
| 48 |
- |
|
| 49 |
- if err := req.flags.Parse(); err != nil {
|
|
| 50 |
- return err |
|
| 51 |
- } |
|
| 52 |
- |
|
| 53 |
- runConfig := req.state.runConfig |
|
| 39 |
+func dispatchEnv(d dispatchRequest, c *instructions.EnvCommand) error {
|
|
| 40 |
+ runConfig := d.state.runConfig |
|
| 54 | 41 |
commitMessage := bytes.NewBufferString("ENV")
|
| 42 |
+ for _, e := range c.Env {
|
|
| 43 |
+ name := e.Key |
|
| 44 |
+ newVar := e.String() |
|
| 55 | 45 |
|
| 56 |
- for j := 0; j < len(req.args); j += 2 {
|
|
| 57 |
- if len(req.args[j]) == 0 {
|
|
| 58 |
- return errBlankCommandNames("ENV")
|
|
| 59 |
- } |
|
| 60 |
- name := req.args[j] |
|
| 61 |
- value := req.args[j+1] |
|
| 62 |
- newVar := name + "=" + value |
|
| 63 | 46 |
commitMessage.WriteString(" " + newVar)
|
| 64 |
- |
|
| 65 | 47 |
gotOne := false |
| 66 | 48 |
for i, envVar := range runConfig.Env {
|
| 67 | 49 |
envParts := strings.SplitN(envVar, "=", 2) |
| ... | ... |
@@ -76,64 +56,32 @@ func env(req dispatchRequest) error {
|
| 76 | 76 |
runConfig.Env = append(runConfig.Env, newVar) |
| 77 | 77 |
} |
| 78 | 78 |
} |
| 79 |
- |
|
| 80 |
- return req.builder.commit(req.state, commitMessage.String()) |
|
| 79 |
+ return d.builder.commit(d.state, commitMessage.String()) |
|
| 81 | 80 |
} |
| 82 | 81 |
|
| 83 | 82 |
// MAINTAINER some text <maybe@an.email.address> |
| 84 | 83 |
// |
| 85 | 84 |
// Sets the maintainer metadata. |
| 86 |
-func maintainer(req dispatchRequest) error {
|
|
| 87 |
- if len(req.args) != 1 {
|
|
| 88 |
- return errExactlyOneArgument("MAINTAINER")
|
|
| 89 |
- } |
|
| 85 |
+func dispatchMaintainer(d dispatchRequest, c *instructions.MaintainerCommand) error {
|
|
| 90 | 86 |
|
| 91 |
- if err := req.flags.Parse(); err != nil {
|
|
| 92 |
- return err |
|
| 93 |
- } |
|
| 94 |
- |
|
| 95 |
- maintainer := req.args[0] |
|
| 96 |
- req.state.maintainer = maintainer |
|
| 97 |
- return req.builder.commit(req.state, "MAINTAINER "+maintainer) |
|
| 87 |
+ d.state.maintainer = c.Maintainer |
|
| 88 |
+ return d.builder.commit(d.state, "MAINTAINER "+c.Maintainer) |
|
| 98 | 89 |
} |
| 99 | 90 |
|
| 100 | 91 |
// LABEL some json data describing the image |
| 101 | 92 |
// |
| 102 | 93 |
// Sets the Label variable foo to bar, |
| 103 | 94 |
// |
| 104 |
-func label(req dispatchRequest) error {
|
|
| 105 |
- if len(req.args) == 0 {
|
|
| 106 |
- return errAtLeastOneArgument("LABEL")
|
|
| 107 |
- } |
|
| 108 |
- if len(req.args)%2 != 0 {
|
|
| 109 |
- // should never get here, but just in case |
|
| 110 |
- return errTooManyArguments("LABEL")
|
|
| 111 |
- } |
|
| 112 |
- |
|
| 113 |
- if err := req.flags.Parse(); err != nil {
|
|
| 114 |
- return err |
|
| 95 |
+func dispatchLabel(d dispatchRequest, c *instructions.LabelCommand) error {
|
|
| 96 |
+ if d.state.runConfig.Labels == nil {
|
|
| 97 |
+ d.state.runConfig.Labels = make(map[string]string) |
|
| 115 | 98 |
} |
| 116 |
- |
|
| 117 | 99 |
commitStr := "LABEL" |
| 118 |
- runConfig := req.state.runConfig |
|
| 119 |
- |
|
| 120 |
- if runConfig.Labels == nil {
|
|
| 121 |
- runConfig.Labels = map[string]string{}
|
|
| 122 |
- } |
|
| 123 |
- |
|
| 124 |
- for j := 0; j < len(req.args); j++ {
|
|
| 125 |
- name := req.args[j] |
|
| 126 |
- if name == "" {
|
|
| 127 |
- return errBlankCommandNames("LABEL")
|
|
| 128 |
- } |
|
| 129 |
- |
|
| 130 |
- value := req.args[j+1] |
|
| 131 |
- commitStr += " " + name + "=" + value |
|
| 132 |
- |
|
| 133 |
- runConfig.Labels[name] = value |
|
| 134 |
- j++ |
|
| 100 |
+ for _, v := range c.Labels {
|
|
| 101 |
+ d.state.runConfig.Labels[v.Key] = v.Value |
|
| 102 |
+ commitStr += " " + v.String() |
|
| 135 | 103 |
} |
| 136 |
- return req.builder.commit(req.state, commitStr) |
|
| 104 |
+ return d.builder.commit(d.state, commitStr) |
|
| 137 | 105 |
} |
| 138 | 106 |
|
| 139 | 107 |
// ADD foo /path |
| ... | ... |
@@ -141,257 +89,172 @@ func label(req dispatchRequest) error {
|
| 141 | 141 |
// Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling |
| 142 | 142 |
// exist here. If you do not wish to have this automatic handling, use COPY. |
| 143 | 143 |
// |
| 144 |
-func add(req dispatchRequest) error {
|
|
| 145 |
- if len(req.args) < 2 {
|
|
| 146 |
- return errAtLeastTwoArguments("ADD")
|
|
| 147 |
- } |
|
| 148 |
- |
|
| 149 |
- flChown := req.flags.AddString("chown", "")
|
|
| 150 |
- if err := req.flags.Parse(); err != nil {
|
|
| 151 |
- return err |
|
| 152 |
- } |
|
| 153 |
- |
|
| 154 |
- downloader := newRemoteSourceDownloader(req.builder.Output, req.builder.Stdout) |
|
| 155 |
- copier := copierFromDispatchRequest(req, downloader, nil) |
|
| 144 |
+func dispatchAdd(d dispatchRequest, c *instructions.AddCommand) error {
|
|
| 145 |
+ downloader := newRemoteSourceDownloader(d.builder.Output, d.builder.Stdout) |
|
| 146 |
+ copier := copierFromDispatchRequest(d, downloader, nil) |
|
| 156 | 147 |
defer copier.Cleanup() |
| 157 |
- copyInstruction, err := copier.createCopyInstruction(req.args, "ADD") |
|
| 148 |
+ |
|
| 149 |
+ copyInstruction, err := copier.createCopyInstruction(c.SourcesAndDest, "ADD") |
|
| 158 | 150 |
if err != nil {
|
| 159 | 151 |
return err |
| 160 | 152 |
} |
| 161 |
- copyInstruction.chownStr = flChown.Value |
|
| 153 |
+ copyInstruction.chownStr = c.Chown |
|
| 162 | 154 |
copyInstruction.allowLocalDecompression = true |
| 163 | 155 |
|
| 164 |
- return req.builder.performCopy(req.state, copyInstruction) |
|
| 156 |
+ return d.builder.performCopy(d.state, copyInstruction) |
|
| 165 | 157 |
} |
| 166 | 158 |
|
| 167 | 159 |
// COPY foo /path |
| 168 | 160 |
// |
| 169 | 161 |
// Same as 'ADD' but without the tar and remote url handling. |
| 170 | 162 |
// |
| 171 |
-func dispatchCopy(req dispatchRequest) error {
|
|
| 172 |
- if len(req.args) < 2 {
|
|
| 173 |
- return errAtLeastTwoArguments("COPY")
|
|
| 174 |
- } |
|
| 175 |
- |
|
| 176 |
- flFrom := req.flags.AddString("from", "")
|
|
| 177 |
- flChown := req.flags.AddString("chown", "")
|
|
| 178 |
- if err := req.flags.Parse(); err != nil {
|
|
| 179 |
- return err |
|
| 180 |
- } |
|
| 181 |
- |
|
| 182 |
- im, err := req.builder.getImageMount(flFrom) |
|
| 183 |
- if err != nil {
|
|
| 184 |
- return errors.Wrapf(err, "invalid from flag value %s", flFrom.Value) |
|
| 163 |
+func dispatchCopy(d dispatchRequest, c *instructions.CopyCommand) error {
|
|
| 164 |
+ var im *imageMount |
|
| 165 |
+ var err error |
|
| 166 |
+ if c.From != "" {
|
|
| 167 |
+ im, err = d.getImageMount(c.From) |
|
| 168 |
+ if err != nil {
|
|
| 169 |
+ return errors.Wrapf(err, "invalid from flag value %s", c.From) |
|
| 170 |
+ } |
|
| 185 | 171 |
} |
| 186 |
- |
|
| 187 |
- copier := copierFromDispatchRequest(req, errOnSourceDownload, im) |
|
| 172 |
+ copier := copierFromDispatchRequest(d, errOnSourceDownload, im) |
|
| 188 | 173 |
defer copier.Cleanup() |
| 189 |
- copyInstruction, err := copier.createCopyInstruction(req.args, "COPY") |
|
| 174 |
+ copyInstruction, err := copier.createCopyInstruction(c.SourcesAndDest, "COPY") |
|
| 190 | 175 |
if err != nil {
|
| 191 | 176 |
return err |
| 192 | 177 |
} |
| 193 |
- copyInstruction.chownStr = flChown.Value |
|
| 178 |
+ copyInstruction.chownStr = c.Chown |
|
| 194 | 179 |
|
| 195 |
- return req.builder.performCopy(req.state, copyInstruction) |
|
| 180 |
+ return d.builder.performCopy(d.state, copyInstruction) |
|
| 196 | 181 |
} |
| 197 | 182 |
|
| 198 |
-func (b *Builder) getImageMount(fromFlag *Flag) (*imageMount, error) {
|
|
| 199 |
- if !fromFlag.IsUsed() {
|
|
| 183 |
+func (d *dispatchRequest) getImageMount(imageRefOrID string) (*imageMount, error) {
|
|
| 184 |
+ if imageRefOrID == "" {
|
|
| 200 | 185 |
// TODO: this could return the source in the default case as well? |
| 201 | 186 |
return nil, nil |
| 202 | 187 |
} |
| 203 | 188 |
|
| 204 | 189 |
var localOnly bool |
| 205 |
- imageRefOrID := fromFlag.Value |
|
| 206 |
- stage, err := b.buildStages.get(fromFlag.Value) |
|
| 190 |
+ stage, err := d.stages.get(imageRefOrID) |
|
| 207 | 191 |
if err != nil {
|
| 208 | 192 |
return nil, err |
| 209 | 193 |
} |
| 210 | 194 |
if stage != nil {
|
| 211 |
- imageRefOrID = stage.ImageID() |
|
| 195 |
+ imageRefOrID = stage.Image |
|
| 212 | 196 |
localOnly = true |
| 213 | 197 |
} |
| 214 |
- return b.imageSources.Get(imageRefOrID, localOnly) |
|
| 198 |
+ return d.builder.imageSources.Get(imageRefOrID, localOnly) |
|
| 215 | 199 |
} |
| 216 | 200 |
|
| 217 | 201 |
// FROM imagename[:tag | @digest] [AS build-stage-name] |
| 218 | 202 |
// |
| 219 |
-func from(req dispatchRequest) error {
|
|
| 220 |
- stageName, err := parseBuildStageName(req.args) |
|
| 221 |
- if err != nil {
|
|
| 222 |
- return err |
|
| 223 |
- } |
|
| 224 |
- |
|
| 225 |
- if err := req.flags.Parse(); err != nil {
|
|
| 226 |
- return err |
|
| 227 |
- } |
|
| 228 |
- |
|
| 229 |
- req.builder.imageProber.Reset() |
|
| 230 |
- image, err := req.builder.getFromImage(req.shlex, req.args[0]) |
|
| 203 |
+func initializeStage(d dispatchRequest, cmd *instructions.Stage) error {
|
|
| 204 |
+ d.builder.imageProber.Reset() |
|
| 205 |
+ image, err := d.getFromImage(d.shlex, cmd.BaseName) |
|
| 231 | 206 |
if err != nil {
|
| 232 | 207 |
return err |
| 233 | 208 |
} |
| 234 |
- if err := req.builder.buildStages.add(stageName, image); err != nil {
|
|
| 235 |
- return err |
|
| 236 |
- } |
|
| 237 |
- req.state.beginStage(stageName, image) |
|
| 238 |
- req.builder.buildArgs.ResetAllowed() |
|
| 239 |
- if image.ImageID() == "" {
|
|
| 240 |
- // Typically this means they used "FROM scratch" |
|
| 241 |
- return nil |
|
| 209 |
+ state := d.state |
|
| 210 |
+ state.beginStage(cmd.Name, image) |
|
| 211 |
+ if len(state.runConfig.OnBuild) > 0 {
|
|
| 212 |
+ triggers := state.runConfig.OnBuild |
|
| 213 |
+ state.runConfig.OnBuild = nil |
|
| 214 |
+ return dispatchTriggeredOnBuild(d, triggers) |
|
| 242 | 215 |
} |
| 243 |
- |
|
| 244 |
- return processOnBuild(req) |
|
| 216 |
+ return nil |
|
| 245 | 217 |
} |
| 246 | 218 |
|
| 247 |
-func parseBuildStageName(args []string) (string, error) {
|
|
| 248 |
- stageName := "" |
|
| 249 |
- switch {
|
|
| 250 |
- case len(args) == 3 && strings.EqualFold(args[1], "as"): |
|
| 251 |
- stageName = strings.ToLower(args[2]) |
|
| 252 |
- if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", stageName); !ok {
|
|
| 253 |
- return "", errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", stageName)
|
|
| 219 |
+func dispatchTriggeredOnBuild(d dispatchRequest, triggers []string) error {
|
|
| 220 |
+ fmt.Fprintf(d.builder.Stdout, "# Executing %d build trigger", len(triggers)) |
|
| 221 |
+ if len(triggers) > 1 {
|
|
| 222 |
+ fmt.Fprint(d.builder.Stdout, "s") |
|
| 223 |
+ } |
|
| 224 |
+ fmt.Fprintln(d.builder.Stdout) |
|
| 225 |
+ for _, trigger := range triggers {
|
|
| 226 |
+ d.state.updateRunConfig() |
|
| 227 |
+ ast, err := parser.Parse(strings.NewReader(trigger)) |
|
| 228 |
+ if err != nil {
|
|
| 229 |
+ return err |
|
| 230 |
+ } |
|
| 231 |
+ if len(ast.AST.Children) != 1 {
|
|
| 232 |
+ return errors.New("onbuild trigger should be a single expression")
|
|
| 233 |
+ } |
|
| 234 |
+ cmd, err := instructions.ParseCommand(ast.AST.Children[0]) |
|
| 235 |
+ if err != nil {
|
|
| 236 |
+ if instructions.IsUnknownInstruction(err) {
|
|
| 237 |
+ buildsFailed.WithValues(metricsUnknownInstructionError).Inc() |
|
| 238 |
+ } |
|
| 239 |
+ return err |
|
| 240 |
+ } |
|
| 241 |
+ err = dispatch(d, cmd) |
|
| 242 |
+ if err != nil {
|
|
| 243 |
+ return err |
|
| 254 | 244 |
} |
| 255 |
- case len(args) != 1: |
|
| 256 |
- return "", errors.New("FROM requires either one or three arguments")
|
|
| 257 | 245 |
} |
| 258 |
- |
|
| 259 |
- return stageName, nil |
|
| 246 |
+ return nil |
|
| 260 | 247 |
} |
| 261 | 248 |
|
| 262 |
-// scratchImage is used as a token for the empty base image. |
|
| 249 |
+// scratchImage is used as a token for the empty base image. It uses buildStage |
|
| 250 |
+// as a convenient implementation of builder.Image, but is not actually a |
|
| 251 |
+// buildStage. |
|
| 263 | 252 |
var scratchImage builder.Image = &image.Image{}
|
| 264 | 253 |
|
| 265 |
-func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, error) {
|
|
| 254 |
+func (d *dispatchRequest) getExpandedImageName(shlex *ShellLex, name string) (string, error) {
|
|
| 266 | 255 |
substitutionArgs := []string{}
|
| 267 |
- for key, value := range b.buildArgs.GetAllMeta() {
|
|
| 256 |
+ for key, value := range d.state.buildArgs.GetAllMeta() {
|
|
| 268 | 257 |
substitutionArgs = append(substitutionArgs, key+"="+value) |
| 269 | 258 |
} |
| 270 | 259 |
|
| 271 | 260 |
name, err := shlex.ProcessWord(name, substitutionArgs) |
| 272 | 261 |
if err != nil {
|
| 273 |
- return nil, err |
|
| 262 |
+ return "", err |
|
| 274 | 263 |
} |
| 275 |
- |
|
| 264 |
+ return name, nil |
|
| 265 |
+} |
|
| 266 |
+func (d *dispatchRequest) getImageOrStage(name string) (builder.Image, error) {
|
|
| 276 | 267 |
var localOnly bool |
| 277 |
- if stage, ok := b.buildStages.getByName(name); ok {
|
|
| 278 |
- name = stage.ImageID() |
|
| 268 |
+ if im, ok := d.stages.getByName(name); ok {
|
|
| 269 |
+ name = im.Image |
|
| 279 | 270 |
localOnly = true |
| 280 | 271 |
} |
| 281 | 272 |
|
| 282 | 273 |
// Windows cannot support a container with no base image unless it is LCOW. |
| 283 | 274 |
if name == api.NoBaseImageSpecifier {
|
| 284 | 275 |
if runtime.GOOS == "windows" {
|
| 285 |
- if b.platform == "windows" || (b.platform != "windows" && !system.LCOWSupported()) {
|
|
| 276 |
+ if d.builder.platform == "windows" || (d.builder.platform != "windows" && !system.LCOWSupported()) {
|
|
| 286 | 277 |
return nil, errors.New("Windows does not support FROM scratch")
|
| 287 | 278 |
} |
| 288 | 279 |
} |
| 289 | 280 |
return scratchImage, nil |
| 290 | 281 |
} |
| 291 |
- imageMount, err := b.imageSources.Get(name, localOnly) |
|
| 282 |
+ imageMount, err := d.builder.imageSources.Get(name, localOnly) |
|
| 292 | 283 |
if err != nil {
|
| 293 | 284 |
return nil, err |
| 294 | 285 |
} |
| 295 | 286 |
return imageMount.Image(), nil |
| 296 | 287 |
} |
| 297 |
- |
|
| 298 |
-func processOnBuild(req dispatchRequest) error {
|
|
| 299 |
- dispatchState := req.state |
|
| 300 |
- // Process ONBUILD triggers if they exist |
|
| 301 |
- if nTriggers := len(dispatchState.runConfig.OnBuild); nTriggers != 0 {
|
|
| 302 |
- word := "trigger" |
|
| 303 |
- if nTriggers > 1 {
|
|
| 304 |
- word = "triggers" |
|
| 305 |
- } |
|
| 306 |
- fmt.Fprintf(req.builder.Stderr, "# Executing %d build %s...\n", nTriggers, word) |
|
| 307 |
- } |
|
| 308 |
- |
|
| 309 |
- // Copy the ONBUILD triggers, and remove them from the config, since the config will be committed. |
|
| 310 |
- onBuildTriggers := dispatchState.runConfig.OnBuild |
|
| 311 |
- dispatchState.runConfig.OnBuild = []string{}
|
|
| 312 |
- |
|
| 313 |
- // Reset stdin settings as all build actions run without stdin |
|
| 314 |
- dispatchState.runConfig.OpenStdin = false |
|
| 315 |
- dispatchState.runConfig.StdinOnce = false |
|
| 316 |
- |
|
| 317 |
- // parse the ONBUILD triggers by invoking the parser |
|
| 318 |
- for _, step := range onBuildTriggers {
|
|
| 319 |
- dockerfile, err := parser.Parse(strings.NewReader(step)) |
|
| 320 |
- if err != nil {
|
|
| 321 |
- return err |
|
| 322 |
- } |
|
| 323 |
- |
|
| 324 |
- for _, n := range dockerfile.AST.Children {
|
|
| 325 |
- if err := checkDispatch(n); err != nil {
|
|
| 326 |
- return err |
|
| 327 |
- } |
|
| 328 |
- |
|
| 329 |
- upperCasedCmd := strings.ToUpper(n.Value) |
|
| 330 |
- switch upperCasedCmd {
|
|
| 331 |
- case "ONBUILD": |
|
| 332 |
- return errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
|
|
| 333 |
- case "MAINTAINER", "FROM": |
|
| 334 |
- return errors.Errorf("%s isn't allowed as an ONBUILD trigger", upperCasedCmd)
|
|
| 335 |
- } |
|
| 336 |
- } |
|
| 337 |
- |
|
| 338 |
- if _, err := dispatchFromDockerfile(req.builder, dockerfile, dispatchState, req.source); err != nil {
|
|
| 339 |
- return err |
|
| 340 |
- } |
|
| 288 |
+func (d *dispatchRequest) getFromImage(shlex *ShellLex, name string) (builder.Image, error) {
|
|
| 289 |
+ name, err := d.getExpandedImageName(shlex, name) |
|
| 290 |
+ if err != nil {
|
|
| 291 |
+ return nil, err |
|
| 341 | 292 |
} |
| 342 |
- return nil |
|
| 293 |
+ return d.getImageOrStage(name) |
|
| 343 | 294 |
} |
| 344 | 295 |
|
| 345 |
-// ONBUILD RUN echo yo |
|
| 346 |
-// |
|
| 347 |
-// ONBUILD triggers run when the image is used in a FROM statement. |
|
| 348 |
-// |
|
| 349 |
-// ONBUILD handling has a lot of special-case functionality, the heading in |
|
| 350 |
-// evaluator.go and comments around dispatch() in the same file explain the |
|
| 351 |
-// special cases. search for 'OnBuild' in internals.go for additional special |
|
| 352 |
-// cases. |
|
| 353 |
-// |
|
| 354 |
-func onbuild(req dispatchRequest) error {
|
|
| 355 |
- if len(req.args) == 0 {
|
|
| 356 |
- return errAtLeastOneArgument("ONBUILD")
|
|
| 357 |
- } |
|
| 358 |
- |
|
| 359 |
- if err := req.flags.Parse(); err != nil {
|
|
| 360 |
- return err |
|
| 361 |
- } |
|
| 362 |
- |
|
| 363 |
- triggerInstruction := strings.ToUpper(strings.TrimSpace(req.args[0])) |
|
| 364 |
- switch triggerInstruction {
|
|
| 365 |
- case "ONBUILD": |
|
| 366 |
- return errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
|
|
| 367 |
- case "MAINTAINER", "FROM": |
|
| 368 |
- return fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
|
|
| 369 |
- } |
|
| 296 |
+func dispatchOnbuild(d dispatchRequest, c *instructions.OnbuildCommand) error {
|
|
| 370 | 297 |
|
| 371 |
- runConfig := req.state.runConfig |
|
| 372 |
- original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "") |
|
| 373 |
- runConfig.OnBuild = append(runConfig.OnBuild, original) |
|
| 374 |
- return req.builder.commit(req.state, "ONBUILD "+original) |
|
| 298 |
+ d.state.runConfig.OnBuild = append(d.state.runConfig.OnBuild, c.Expression) |
|
| 299 |
+ return d.builder.commit(d.state, "ONBUILD "+c.Expression) |
|
| 375 | 300 |
} |
| 376 | 301 |
|
| 377 | 302 |
// WORKDIR /tmp |
| 378 | 303 |
// |
| 379 | 304 |
// Set the working directory for future RUN/CMD/etc statements. |
| 380 | 305 |
// |
| 381 |
-func workdir(req dispatchRequest) error {
|
|
| 382 |
- if len(req.args) != 1 {
|
|
| 383 |
- return errExactlyOneArgument("WORKDIR")
|
|
| 384 |
- } |
|
| 385 |
- |
|
| 386 |
- err := req.flags.Parse() |
|
| 387 |
- if err != nil {
|
|
| 388 |
- return err |
|
| 389 |
- } |
|
| 390 |
- |
|
| 391 |
- runConfig := req.state.runConfig |
|
| 392 |
- // This is from the Dockerfile and will not necessarily be in platform |
|
| 393 |
- // specific semantics, hence ensure it is converted. |
|
| 394 |
- runConfig.WorkingDir, err = normalizeWorkdir(req.builder.platform, runConfig.WorkingDir, req.args[0]) |
|
| 306 |
+func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error {
|
|
| 307 |
+ runConfig := d.state.runConfig |
|
| 308 |
+ var err error |
|
| 309 |
+ runConfig.WorkingDir, err = normalizeWorkdir(d.builder.platform, runConfig.WorkingDir, c.Path) |
|
| 395 | 310 |
if err != nil {
|
| 396 | 311 |
return err |
| 397 | 312 |
} |
| ... | ... |
@@ -400,23 +263,31 @@ func workdir(req dispatchRequest) error {
|
| 400 | 400 |
// This avoids having an unnecessary expensive mount/unmount calls |
| 401 | 401 |
// (on Windows in particular) during each container create. |
| 402 | 402 |
// Prior to 1.13, the mkdir was deferred and not executed at this step. |
| 403 |
- if req.builder.disableCommit {
|
|
| 403 |
+ if d.builder.disableCommit {
|
|
| 404 | 404 |
// Don't call back into the daemon if we're going through docker commit --change "WORKDIR /foo". |
| 405 | 405 |
// We've already updated the runConfig and that's enough. |
| 406 | 406 |
return nil |
| 407 | 407 |
} |
| 408 | 408 |
|
| 409 | 409 |
comment := "WORKDIR " + runConfig.WorkingDir |
| 410 |
- runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, req.builder.platform)) |
|
| 411 |
- containerID, err := req.builder.probeAndCreate(req.state, runConfigWithCommentCmd) |
|
| 410 |
+ runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, d.builder.platform)) |
|
| 411 |
+ containerID, err := d.builder.probeAndCreate(d.state, runConfigWithCommentCmd) |
|
| 412 | 412 |
if err != nil || containerID == "" {
|
| 413 | 413 |
return err |
| 414 | 414 |
} |
| 415 |
- if err := req.builder.docker.ContainerCreateWorkdir(containerID); err != nil {
|
|
| 415 |
+ if err := d.builder.docker.ContainerCreateWorkdir(containerID); err != nil {
|
|
| 416 | 416 |
return err |
| 417 | 417 |
} |
| 418 | 418 |
|
| 419 |
- return req.builder.commitContainer(req.state, containerID, runConfigWithCommentCmd) |
|
| 419 |
+ return d.builder.commitContainer(d.state, containerID, runConfigWithCommentCmd) |
|
| 420 |
+} |
|
| 421 |
+ |
|
| 422 |
+func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container.Config, platform string) []string {
|
|
| 423 |
+ result := cmd.CmdLine |
|
| 424 |
+ if cmd.PrependShell && result != nil {
|
|
| 425 |
+ result = append(getShell(runConfig, platform), result...) |
|
| 426 |
+ } |
|
| 427 |
+ return result |
|
| 420 | 428 |
} |
| 421 | 429 |
|
| 422 | 430 |
// RUN some command yo |
| ... | ... |
@@ -429,32 +300,21 @@ func workdir(req dispatchRequest) error {
|
| 429 | 429 |
// RUN echo hi # cmd /S /C echo hi (Windows) |
| 430 | 430 |
// RUN [ "echo", "hi" ] # echo hi |
| 431 | 431 |
// |
| 432 |
-func run(req dispatchRequest) error {
|
|
| 433 |
- if !req.state.hasFromImage() {
|
|
| 434 |
- return errors.New("Please provide a source image with `from` prior to run")
|
|
| 435 |
- } |
|
| 432 |
+func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error {
|
|
| 436 | 433 |
|
| 437 |
- if err := req.flags.Parse(); err != nil {
|
|
| 438 |
- return err |
|
| 439 |
- } |
|
| 440 |
- |
|
| 441 |
- stateRunConfig := req.state.runConfig |
|
| 442 |
- args := handleJSONArgs(req.args, req.attributes) |
|
| 443 |
- if !req.attributes["json"] {
|
|
| 444 |
- args = append(getShell(stateRunConfig, req.builder.platform), args...) |
|
| 445 |
- } |
|
| 446 |
- cmdFromArgs := strslice.StrSlice(args) |
|
| 447 |
- buildArgs := req.builder.buildArgs.FilterAllowed(stateRunConfig.Env) |
|
| 434 |
+ stateRunConfig := d.state.runConfig |
|
| 435 |
+ cmdFromArgs := resolveCmdLine(c.ShellDependantCmdLine, stateRunConfig, d.builder.platform) |
|
| 436 |
+ buildArgs := d.state.buildArgs.FilterAllowed(stateRunConfig.Env) |
|
| 448 | 437 |
|
| 449 | 438 |
saveCmd := cmdFromArgs |
| 450 | 439 |
if len(buildArgs) > 0 {
|
| 451 |
- saveCmd = prependEnvOnCmd(req.builder.buildArgs, buildArgs, cmdFromArgs) |
|
| 440 |
+ saveCmd = prependEnvOnCmd(d.state.buildArgs, buildArgs, cmdFromArgs) |
|
| 452 | 441 |
} |
| 453 | 442 |
|
| 454 | 443 |
runConfigForCacheProbe := copyRunConfig(stateRunConfig, |
| 455 | 444 |
withCmd(saveCmd), |
| 456 | 445 |
withEntrypointOverride(saveCmd, nil)) |
| 457 |
- hit, err := req.builder.probeCache(req.state, runConfigForCacheProbe) |
|
| 446 |
+ hit, err := d.builder.probeCache(d.state, runConfigForCacheProbe) |
|
| 458 | 447 |
if err != nil || hit {
|
| 459 | 448 |
return err |
| 460 | 449 |
} |
| ... | ... |
@@ -468,11 +328,11 @@ func run(req dispatchRequest) error {
|
| 468 | 468 |
runConfig.ArgsEscaped = true |
| 469 | 469 |
|
| 470 | 470 |
logrus.Debugf("[BUILDER] Command to be executed: %v", runConfig.Cmd)
|
| 471 |
- cID, err := req.builder.create(runConfig) |
|
| 471 |
+ cID, err := d.builder.create(runConfig) |
|
| 472 | 472 |
if err != nil {
|
| 473 | 473 |
return err |
| 474 | 474 |
} |
| 475 |
- if err := req.builder.containerManager.Run(req.builder.clientCtx, cID, req.builder.Stdout, req.builder.Stderr); err != nil {
|
|
| 475 |
+ if err := d.builder.containerManager.Run(d.builder.clientCtx, cID, d.builder.Stdout, d.builder.Stderr); err != nil {
|
|
| 476 | 476 |
if err, ok := err.(*statusCodeError); ok {
|
| 477 | 477 |
// TODO: change error type, because jsonmessage.JSONError assumes HTTP |
| 478 | 478 |
return &jsonmessage.JSONError{
|
| ... | ... |
@@ -485,7 +345,7 @@ func run(req dispatchRequest) error {
|
| 485 | 485 |
return err |
| 486 | 486 |
} |
| 487 | 487 |
|
| 488 |
- return req.builder.commitContainer(req.state, cID, runConfigForCacheProbe) |
|
| 488 |
+ return d.builder.commitContainer(d.state, cID, runConfigForCacheProbe) |
|
| 489 | 489 |
} |
| 490 | 490 |
|
| 491 | 491 |
// Derive the command to use for probeCache() and to commit in this container. |
| ... | ... |
@@ -518,139 +378,39 @@ func prependEnvOnCmd(buildArgs *buildArgs, buildArgVars []string, cmd strslice.S |
| 518 | 518 |
// Set the default command to run in the container (which may be empty). |
| 519 | 519 |
// Argument handling is the same as RUN. |
| 520 | 520 |
// |
| 521 |
-func cmd(req dispatchRequest) error {
|
|
| 522 |
- if err := req.flags.Parse(); err != nil {
|
|
| 523 |
- return err |
|
| 524 |
- } |
|
| 525 |
- |
|
| 526 |
- runConfig := req.state.runConfig |
|
| 527 |
- cmdSlice := handleJSONArgs(req.args, req.attributes) |
|
| 528 |
- if !req.attributes["json"] {
|
|
| 529 |
- cmdSlice = append(getShell(runConfig, req.builder.platform), cmdSlice...) |
|
| 530 |
- } |
|
| 531 |
- |
|
| 532 |
- runConfig.Cmd = strslice.StrSlice(cmdSlice) |
|
| 521 |
+func dispatchCmd(d dispatchRequest, c *instructions.CmdCommand) error {
|
|
| 522 |
+ runConfig := d.state.runConfig |
|
| 523 |
+ cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.builder.platform) |
|
| 524 |
+ runConfig.Cmd = cmd |
|
| 533 | 525 |
// set config as already being escaped, this prevents double escaping on windows |
| 534 | 526 |
runConfig.ArgsEscaped = true |
| 535 | 527 |
|
| 536 |
- if err := req.builder.commit(req.state, fmt.Sprintf("CMD %q", cmdSlice)); err != nil {
|
|
| 528 |
+ if err := d.builder.commit(d.state, fmt.Sprintf("CMD %q", cmd)); err != nil {
|
|
| 537 | 529 |
return err |
| 538 | 530 |
} |
| 539 | 531 |
|
| 540 |
- if len(req.args) != 0 {
|
|
| 541 |
- req.state.cmdSet = true |
|
| 532 |
+ if len(c.ShellDependantCmdLine.CmdLine) != 0 {
|
|
| 533 |
+ d.state.cmdSet = true |
|
| 542 | 534 |
} |
| 543 | 535 |
|
| 544 | 536 |
return nil |
| 545 | 537 |
} |
| 546 | 538 |
|
| 547 |
-// parseOptInterval(flag) is the duration of flag.Value, or 0 if |
|
| 548 |
-// empty. An error is reported if the value is given and less than minimum duration. |
|
| 549 |
-func parseOptInterval(f *Flag) (time.Duration, error) {
|
|
| 550 |
- s := f.Value |
|
| 551 |
- if s == "" {
|
|
| 552 |
- return 0, nil |
|
| 553 |
- } |
|
| 554 |
- d, err := time.ParseDuration(s) |
|
| 555 |
- if err != nil {
|
|
| 556 |
- return 0, err |
|
| 557 |
- } |
|
| 558 |
- if d < container.MinimumDuration {
|
|
| 559 |
- return 0, fmt.Errorf("Interval %#v cannot be less than %s", f.name, container.MinimumDuration)
|
|
| 560 |
- } |
|
| 561 |
- return d, nil |
|
| 562 |
-} |
|
| 563 |
- |
|
| 564 | 539 |
// HEALTHCHECK foo |
| 565 | 540 |
// |
| 566 | 541 |
// Set the default healthcheck command to run in the container (which may be empty). |
| 567 | 542 |
// Argument handling is the same as RUN. |
| 568 | 543 |
// |
| 569 |
-func healthcheck(req dispatchRequest) error {
|
|
| 570 |
- if len(req.args) == 0 {
|
|
| 571 |
- return errAtLeastOneArgument("HEALTHCHECK")
|
|
| 572 |
- } |
|
| 573 |
- runConfig := req.state.runConfig |
|
| 574 |
- typ := strings.ToUpper(req.args[0]) |
|
| 575 |
- args := req.args[1:] |
|
| 576 |
- if typ == "NONE" {
|
|
| 577 |
- if len(args) != 0 {
|
|
| 578 |
- return errors.New("HEALTHCHECK NONE takes no arguments")
|
|
| 579 |
- } |
|
| 580 |
- test := strslice.StrSlice{typ}
|
|
| 581 |
- runConfig.Healthcheck = &container.HealthConfig{
|
|
| 582 |
- Test: test, |
|
| 583 |
- } |
|
| 584 |
- } else {
|
|
| 585 |
- if runConfig.Healthcheck != nil {
|
|
| 586 |
- oldCmd := runConfig.Healthcheck.Test |
|
| 587 |
- if len(oldCmd) > 0 && oldCmd[0] != "NONE" {
|
|
| 588 |
- fmt.Fprintf(req.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd) |
|
| 589 |
- } |
|
| 590 |
- } |
|
| 591 |
- |
|
| 592 |
- healthcheck := container.HealthConfig{}
|
|
| 593 |
- |
|
| 594 |
- flInterval := req.flags.AddString("interval", "")
|
|
| 595 |
- flTimeout := req.flags.AddString("timeout", "")
|
|
| 596 |
- flStartPeriod := req.flags.AddString("start-period", "")
|
|
| 597 |
- flRetries := req.flags.AddString("retries", "")
|
|
| 598 |
- |
|
| 599 |
- if err := req.flags.Parse(); err != nil {
|
|
| 600 |
- return err |
|
| 601 |
- } |
|
| 602 |
- |
|
| 603 |
- switch typ {
|
|
| 604 |
- case "CMD": |
|
| 605 |
- cmdSlice := handleJSONArgs(args, req.attributes) |
|
| 606 |
- if len(cmdSlice) == 0 {
|
|
| 607 |
- return errors.New("Missing command after HEALTHCHECK CMD")
|
|
| 608 |
- } |
|
| 609 |
- |
|
| 610 |
- if !req.attributes["json"] {
|
|
| 611 |
- typ = "CMD-SHELL" |
|
| 612 |
- } |
|
| 613 |
- |
|
| 614 |
- healthcheck.Test = strslice.StrSlice(append([]string{typ}, cmdSlice...))
|
|
| 615 |
- default: |
|
| 616 |
- return fmt.Errorf("Unknown type %#v in HEALTHCHECK (try CMD)", typ)
|
|
| 617 |
- } |
|
| 618 |
- |
|
| 619 |
- interval, err := parseOptInterval(flInterval) |
|
| 620 |
- if err != nil {
|
|
| 621 |
- return err |
|
| 622 |
- } |
|
| 623 |
- healthcheck.Interval = interval |
|
| 624 |
- |
|
| 625 |
- timeout, err := parseOptInterval(flTimeout) |
|
| 626 |
- if err != nil {
|
|
| 627 |
- return err |
|
| 628 |
- } |
|
| 629 |
- healthcheck.Timeout = timeout |
|
| 630 |
- |
|
| 631 |
- startPeriod, err := parseOptInterval(flStartPeriod) |
|
| 632 |
- if err != nil {
|
|
| 633 |
- return err |
|
| 634 |
- } |
|
| 635 |
- healthcheck.StartPeriod = startPeriod |
|
| 636 |
- |
|
| 637 |
- if flRetries.Value != "" {
|
|
| 638 |
- retries, err := strconv.ParseInt(flRetries.Value, 10, 32) |
|
| 639 |
- if err != nil {
|
|
| 640 |
- return err |
|
| 641 |
- } |
|
| 642 |
- if retries < 1 {
|
|
| 643 |
- return fmt.Errorf("--retries must be at least 1 (not %d)", retries)
|
|
| 644 |
- } |
|
| 645 |
- healthcheck.Retries = int(retries) |
|
| 646 |
- } else {
|
|
| 647 |
- healthcheck.Retries = 0 |
|
| 544 |
+func dispatchHealthcheck(d dispatchRequest, c *instructions.HealthCheckCommand) error {
|
|
| 545 |
+ runConfig := d.state.runConfig |
|
| 546 |
+ if runConfig.Healthcheck != nil {
|
|
| 547 |
+ oldCmd := runConfig.Healthcheck.Test |
|
| 548 |
+ if len(oldCmd) > 0 && oldCmd[0] != "NONE" {
|
|
| 549 |
+ fmt.Fprintf(d.builder.Stdout, "Note: overriding previous HEALTHCHECK: %v\n", oldCmd) |
|
| 648 | 550 |
} |
| 649 |
- |
|
| 650 |
- runConfig.Healthcheck = &healthcheck |
|
| 651 | 551 |
} |
| 652 |
- |
|
| 653 |
- return req.builder.commit(req.state, fmt.Sprintf("HEALTHCHECK %q", runConfig.Healthcheck))
|
|
| 552 |
+ runConfig.Healthcheck = c.Health |
|
| 553 |
+ return d.builder.commit(d.state, fmt.Sprintf("HEALTHCHECK %q", runConfig.Healthcheck))
|
|
| 654 | 554 |
} |
| 655 | 555 |
|
| 656 | 556 |
// ENTRYPOINT /usr/sbin/nginx |
| ... | ... |
@@ -661,33 +421,15 @@ func healthcheck(req dispatchRequest) error {
|
| 661 | 661 |
// Handles command processing similar to CMD and RUN, only req.runConfig.Entrypoint |
| 662 | 662 |
// is initialized at newBuilder time instead of through argument parsing. |
| 663 | 663 |
// |
| 664 |
-func entrypoint(req dispatchRequest) error {
|
|
| 665 |
- if err := req.flags.Parse(); err != nil {
|
|
| 666 |
- return err |
|
| 667 |
- } |
|
| 668 |
- |
|
| 669 |
- runConfig := req.state.runConfig |
|
| 670 |
- parsed := handleJSONArgs(req.args, req.attributes) |
|
| 671 |
- |
|
| 672 |
- switch {
|
|
| 673 |
- case req.attributes["json"]: |
|
| 674 |
- // ENTRYPOINT ["echo", "hi"] |
|
| 675 |
- runConfig.Entrypoint = strslice.StrSlice(parsed) |
|
| 676 |
- case len(parsed) == 0: |
|
| 677 |
- // ENTRYPOINT [] |
|
| 678 |
- runConfig.Entrypoint = nil |
|
| 679 |
- default: |
|
| 680 |
- // ENTRYPOINT echo hi |
|
| 681 |
- runConfig.Entrypoint = strslice.StrSlice(append(getShell(runConfig, req.builder.platform), parsed[0])) |
|
| 682 |
- } |
|
| 683 |
- |
|
| 684 |
- // when setting the entrypoint if a CMD was not explicitly set then |
|
| 685 |
- // set the command to nil |
|
| 686 |
- if !req.state.cmdSet {
|
|
| 664 |
+func dispatchEntrypoint(d dispatchRequest, c *instructions.EntrypointCommand) error {
|
|
| 665 |
+ runConfig := d.state.runConfig |
|
| 666 |
+ cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.builder.platform) |
|
| 667 |
+ runConfig.Entrypoint = cmd |
|
| 668 |
+ if !d.state.cmdSet {
|
|
| 687 | 669 |
runConfig.Cmd = nil |
| 688 | 670 |
} |
| 689 | 671 |
|
| 690 |
- return req.builder.commit(req.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint))
|
|
| 672 |
+ return d.builder.commit(d.state, fmt.Sprintf("ENTRYPOINT %q", runConfig.Entrypoint))
|
|
| 691 | 673 |
} |
| 692 | 674 |
|
| 693 | 675 |
// EXPOSE 6667/tcp 7000/tcp |
| ... | ... |
@@ -695,41 +437,33 @@ func entrypoint(req dispatchRequest) error {
|
| 695 | 695 |
// Expose ports for links and port mappings. This all ends up in |
| 696 | 696 |
// req.runConfig.ExposedPorts for runconfig. |
| 697 | 697 |
// |
| 698 |
-func expose(req dispatchRequest) error {
|
|
| 699 |
- portsTab := req.args |
|
| 700 |
- |
|
| 701 |
- if len(req.args) == 0 {
|
|
| 702 |
- return errAtLeastOneArgument("EXPOSE")
|
|
| 698 |
+func dispatchExpose(d dispatchRequest, c *instructions.ExposeCommand, envs []string) error {
|
|
| 699 |
+ // custom multi word expansion |
|
| 700 |
+ // expose $FOO with FOO="80 443" is expanded as EXPOSE [80,443]. This is the only command supporting word to words expansion |
|
| 701 |
+ // so the word processing has been de-generalized |
|
| 702 |
+ ports := []string{}
|
|
| 703 |
+ for _, p := range c.Ports {
|
|
| 704 |
+ ps, err := d.shlex.ProcessWords(p, envs) |
|
| 705 |
+ if err != nil {
|
|
| 706 |
+ return err |
|
| 707 |
+ } |
|
| 708 |
+ ports = append(ports, ps...) |
|
| 703 | 709 |
} |
| 710 |
+ c.Ports = ports |
|
| 704 | 711 |
|
| 705 |
- if err := req.flags.Parse(); err != nil {
|
|
| 712 |
+ ps, _, err := nat.ParsePortSpecs(ports) |
|
| 713 |
+ if err != nil {
|
|
| 706 | 714 |
return err |
| 707 | 715 |
} |
| 708 | 716 |
|
| 709 |
- runConfig := req.state.runConfig |
|
| 710 |
- if runConfig.ExposedPorts == nil {
|
|
| 711 |
- runConfig.ExposedPorts = make(nat.PortSet) |
|
| 717 |
+ if d.state.runConfig.ExposedPorts == nil {
|
|
| 718 |
+ d.state.runConfig.ExposedPorts = make(nat.PortSet) |
|
| 712 | 719 |
} |
| 713 |
- |
|
| 714 |
- ports, _, err := nat.ParsePortSpecs(portsTab) |
|
| 715 |
- if err != nil {
|
|
| 716 |
- return err |
|
| 720 |
+ for p := range ps {
|
|
| 721 |
+ d.state.runConfig.ExposedPorts[p] = struct{}{}
|
|
| 717 | 722 |
} |
| 718 | 723 |
|
| 719 |
- // instead of using ports directly, we build a list of ports and sort it so |
|
| 720 |
- // the order is consistent. This prevents cache burst where map ordering |
|
| 721 |
- // changes between builds |
|
| 722 |
- portList := make([]string, len(ports)) |
|
| 723 |
- var i int |
|
| 724 |
- for port := range ports {
|
|
| 725 |
- if _, exists := runConfig.ExposedPorts[port]; !exists {
|
|
| 726 |
- runConfig.ExposedPorts[port] = struct{}{}
|
|
| 727 |
- } |
|
| 728 |
- portList[i] = string(port) |
|
| 729 |
- i++ |
|
| 730 |
- } |
|
| 731 |
- sort.Strings(portList) |
|
| 732 |
- return req.builder.commit(req.state, "EXPOSE "+strings.Join(portList, " ")) |
|
| 724 |
+ return d.builder.commit(d.state, "EXPOSE "+strings.Join(c.Ports, " ")) |
|
| 733 | 725 |
} |
| 734 | 726 |
|
| 735 | 727 |
// USER foo |
| ... | ... |
@@ -737,62 +471,39 @@ func expose(req dispatchRequest) error {
|
| 737 | 737 |
// Set the user to 'foo' for future commands and when running the |
| 738 | 738 |
// ENTRYPOINT/CMD at container run time. |
| 739 | 739 |
// |
| 740 |
-func user(req dispatchRequest) error {
|
|
| 741 |
- if len(req.args) != 1 {
|
|
| 742 |
- return errExactlyOneArgument("USER")
|
|
| 743 |
- } |
|
| 744 |
- |
|
| 745 |
- if err := req.flags.Parse(); err != nil {
|
|
| 746 |
- return err |
|
| 747 |
- } |
|
| 748 |
- |
|
| 749 |
- req.state.runConfig.User = req.args[0] |
|
| 750 |
- return req.builder.commit(req.state, fmt.Sprintf("USER %v", req.args))
|
|
| 740 |
+func dispatchUser(d dispatchRequest, c *instructions.UserCommand) error {
|
|
| 741 |
+ d.state.runConfig.User = c.User |
|
| 742 |
+ return d.builder.commit(d.state, fmt.Sprintf("USER %v", c.User))
|
|
| 751 | 743 |
} |
| 752 | 744 |
|
| 753 | 745 |
// VOLUME /foo |
| 754 | 746 |
// |
| 755 | 747 |
// Expose the volume /foo for use. Will also accept the JSON array form. |
| 756 | 748 |
// |
| 757 |
-func volume(req dispatchRequest) error {
|
|
| 758 |
- if len(req.args) == 0 {
|
|
| 759 |
- return errAtLeastOneArgument("VOLUME")
|
|
| 760 |
- } |
|
| 761 |
- |
|
| 762 |
- if err := req.flags.Parse(); err != nil {
|
|
| 763 |
- return err |
|
| 749 |
+func dispatchVolume(d dispatchRequest, c *instructions.VolumeCommand) error {
|
|
| 750 |
+ if d.state.runConfig.Volumes == nil {
|
|
| 751 |
+ d.state.runConfig.Volumes = map[string]struct{}{}
|
|
| 764 | 752 |
} |
| 765 |
- |
|
| 766 |
- runConfig := req.state.runConfig |
|
| 767 |
- if runConfig.Volumes == nil {
|
|
| 768 |
- runConfig.Volumes = map[string]struct{}{}
|
|
| 769 |
- } |
|
| 770 |
- for _, v := range req.args {
|
|
| 771 |
- v = strings.TrimSpace(v) |
|
| 753 |
+ for _, v := range c.Volumes {
|
|
| 772 | 754 |
if v == "" {
|
| 773 | 755 |
return errors.New("VOLUME specified can not be an empty string")
|
| 774 | 756 |
} |
| 775 |
- runConfig.Volumes[v] = struct{}{}
|
|
| 757 |
+ d.state.runConfig.Volumes[v] = struct{}{}
|
|
| 776 | 758 |
} |
| 777 |
- return req.builder.commit(req.state, fmt.Sprintf("VOLUME %v", req.args))
|
|
| 759 |
+ return d.builder.commit(d.state, fmt.Sprintf("VOLUME %v", c.Volumes))
|
|
| 778 | 760 |
} |
| 779 | 761 |
|
| 780 | 762 |
// STOPSIGNAL signal |
| 781 | 763 |
// |
| 782 | 764 |
// Set the signal that will be used to kill the container. |
| 783 |
-func stopSignal(req dispatchRequest) error {
|
|
| 784 |
- if len(req.args) != 1 {
|
|
| 785 |
- return errExactlyOneArgument("STOPSIGNAL")
|
|
| 786 |
- } |
|
| 765 |
+func dispatchStopSignal(d dispatchRequest, c *instructions.StopSignalCommand) error {
|
|
| 787 | 766 |
|
| 788 |
- sig := req.args[0] |
|
| 789 |
- _, err := signal.ParseSignal(sig) |
|
| 767 |
+ _, err := signal.ParseSignal(c.Signal) |
|
| 790 | 768 |
if err != nil {
|
| 791 | 769 |
return validationError{err}
|
| 792 | 770 |
} |
| 793 |
- |
|
| 794 |
- req.state.runConfig.StopSignal = sig |
|
| 795 |
- return req.builder.commit(req.state, fmt.Sprintf("STOPSIGNAL %v", req.args))
|
|
| 771 |
+ d.state.runConfig.StopSignal = c.Signal |
|
| 772 |
+ return d.builder.commit(d.state, fmt.Sprintf("STOPSIGNAL %v", c.Signal))
|
|
| 796 | 773 |
} |
| 797 | 774 |
|
| 798 | 775 |
// ARG name[=value] |
| ... | ... |
@@ -800,89 +511,21 @@ func stopSignal(req dispatchRequest) error {
|
| 800 | 800 |
// Adds the variable foo to the trusted list of variables that can be passed |
| 801 | 801 |
// to builder using the --build-arg flag for expansion/substitution or passing to 'run'. |
| 802 | 802 |
// Dockerfile author may optionally set a default value of this variable. |
| 803 |
-func arg(req dispatchRequest) error {
|
|
| 804 |
- if len(req.args) != 1 {
|
|
| 805 |
- return errExactlyOneArgument("ARG")
|
|
| 806 |
- } |
|
| 807 |
- |
|
| 808 |
- var ( |
|
| 809 |
- name string |
|
| 810 |
- newValue string |
|
| 811 |
- hasDefault bool |
|
| 812 |
- ) |
|
| 813 |
- |
|
| 814 |
- arg := req.args[0] |
|
| 815 |
- // 'arg' can just be a name or name-value pair. Note that this is different |
|
| 816 |
- // from 'env' that handles the split of name and value at the parser level. |
|
| 817 |
- // The reason for doing it differently for 'arg' is that we support just |
|
| 818 |
- // defining an arg and not assign it a value (while 'env' always expects a |
|
| 819 |
- // name-value pair). If possible, it will be good to harmonize the two. |
|
| 820 |
- if strings.Contains(arg, "=") {
|
|
| 821 |
- parts := strings.SplitN(arg, "=", 2) |
|
| 822 |
- if len(parts[0]) == 0 {
|
|
| 823 |
- return errBlankCommandNames("ARG")
|
|
| 824 |
- } |
|
| 825 |
- |
|
| 826 |
- name = parts[0] |
|
| 827 |
- newValue = parts[1] |
|
| 828 |
- hasDefault = true |
|
| 829 |
- } else {
|
|
| 830 |
- name = arg |
|
| 831 |
- hasDefault = false |
|
| 832 |
- } |
|
| 803 |
+func dispatchArg(d dispatchRequest, c *instructions.ArgCommand) error {
|
|
| 833 | 804 |
|
| 834 |
- var value *string |
|
| 835 |
- if hasDefault {
|
|
| 836 |
- value = &newValue |
|
| 805 |
+ commitStr := "ARG " + c.Key |
|
| 806 |
+ if c.Value != nil {
|
|
| 807 |
+ commitStr += "=" + *c.Value |
|
| 837 | 808 |
} |
| 838 |
- req.builder.buildArgs.AddArg(name, value) |
|
| 839 | 809 |
|
| 840 |
- // Arg before FROM doesn't add a layer |
|
| 841 |
- if !req.state.hasFromImage() {
|
|
| 842 |
- req.builder.buildArgs.AddMetaArg(name, value) |
|
| 843 |
- return nil |
|
| 844 |
- } |
|
| 845 |
- return req.builder.commit(req.state, "ARG "+arg) |
|
| 810 |
+ d.state.buildArgs.AddArg(c.Key, c.Value) |
|
| 811 |
+ return d.builder.commit(d.state, commitStr) |
|
| 846 | 812 |
} |
| 847 | 813 |
|
| 848 | 814 |
// SHELL powershell -command |
| 849 | 815 |
// |
| 850 | 816 |
// Set the non-default shell to use. |
| 851 |
-func shell(req dispatchRequest) error {
|
|
| 852 |
- if err := req.flags.Parse(); err != nil {
|
|
| 853 |
- return err |
|
| 854 |
- } |
|
| 855 |
- shellSlice := handleJSONArgs(req.args, req.attributes) |
|
| 856 |
- switch {
|
|
| 857 |
- case len(shellSlice) == 0: |
|
| 858 |
- // SHELL [] |
|
| 859 |
- return errAtLeastOneArgument("SHELL")
|
|
| 860 |
- case req.attributes["json"]: |
|
| 861 |
- // SHELL ["powershell", "-command"] |
|
| 862 |
- req.state.runConfig.Shell = strslice.StrSlice(shellSlice) |
|
| 863 |
- default: |
|
| 864 |
- // SHELL powershell -command - not JSON |
|
| 865 |
- return errNotJSON("SHELL", req.original)
|
|
| 866 |
- } |
|
| 867 |
- return req.builder.commit(req.state, fmt.Sprintf("SHELL %v", shellSlice))
|
|
| 868 |
-} |
|
| 869 |
- |
|
| 870 |
-func errAtLeastOneArgument(command string) error {
|
|
| 871 |
- return fmt.Errorf("%s requires at least one argument", command)
|
|
| 872 |
-} |
|
| 873 |
- |
|
| 874 |
-func errExactlyOneArgument(command string) error {
|
|
| 875 |
- return fmt.Errorf("%s requires exactly one argument", command)
|
|
| 876 |
-} |
|
| 877 |
- |
|
| 878 |
-func errAtLeastTwoArguments(command string) error {
|
|
| 879 |
- return fmt.Errorf("%s requires at least two arguments", command)
|
|
| 880 |
-} |
|
| 881 |
- |
|
| 882 |
-func errBlankCommandNames(command string) error {
|
|
| 883 |
- return fmt.Errorf("%s names can not be blank", command)
|
|
| 884 |
-} |
|
| 885 |
- |
|
| 886 |
-func errTooManyArguments(command string) error {
|
|
| 887 |
- return fmt.Errorf("Bad input to %s, too many arguments", command)
|
|
| 817 |
+func dispatchShell(d dispatchRequest, c *instructions.ShellCommand) error {
|
|
| 818 |
+ d.state.runConfig.Shell = c.Shell |
|
| 819 |
+ return d.builder.commit(d.state, fmt.Sprintf("SHELL %v", d.state.runConfig.Shell))
|
|
| 888 | 820 |
} |
| ... | ... |
@@ -1,60 +1,29 @@ |
| 1 | 1 |
package dockerfile |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "fmt" |
|
| 5 |
- "runtime" |
|
| 6 |
- "testing" |
|
| 7 |
- |
|
| 8 | 4 |
"bytes" |
| 9 | 5 |
"context" |
| 6 |
+ "runtime" |
|
| 7 |
+ "testing" |
|
| 10 | 8 |
|
| 11 | 9 |
"github.com/docker/docker/api/types" |
| 12 | 10 |
"github.com/docker/docker/api/types/backend" |
| 13 | 11 |
"github.com/docker/docker/api/types/container" |
| 14 | 12 |
"github.com/docker/docker/api/types/strslice" |
| 15 | 13 |
"github.com/docker/docker/builder" |
| 16 |
- "github.com/docker/docker/builder/dockerfile/parser" |
|
| 17 |
- "github.com/docker/docker/internal/testutil" |
|
| 14 |
+ "github.com/docker/docker/builder/dockerfile/instructions" |
|
| 18 | 15 |
"github.com/docker/docker/pkg/system" |
| 19 | 16 |
"github.com/docker/go-connections/nat" |
| 20 | 17 |
"github.com/stretchr/testify/assert" |
| 21 | 18 |
"github.com/stretchr/testify/require" |
| 22 | 19 |
) |
| 23 | 20 |
|
| 24 |
-type commandWithFunction struct {
|
|
| 25 |
- name string |
|
| 26 |
- function func(args []string) error |
|
| 27 |
-} |
|
| 28 |
- |
|
| 29 |
-func withArgs(f dispatcher) func([]string) error {
|
|
| 30 |
- return func(args []string) error {
|
|
| 31 |
- return f(dispatchRequest{args: args})
|
|
| 32 |
- } |
|
| 33 |
-} |
|
| 34 |
- |
|
| 35 |
-func withBuilderAndArgs(builder *Builder, f dispatcher) func([]string) error {
|
|
| 36 |
- return func(args []string) error {
|
|
| 37 |
- return f(defaultDispatchReq(builder, args...)) |
|
| 38 |
- } |
|
| 39 |
-} |
|
| 40 |
- |
|
| 41 |
-func defaultDispatchReq(builder *Builder, args ...string) dispatchRequest {
|
|
| 42 |
- return dispatchRequest{
|
|
| 43 |
- builder: builder, |
|
| 44 |
- args: args, |
|
| 45 |
- flags: NewBFlags(), |
|
| 46 |
- shlex: NewShellLex(parser.DefaultEscapeToken), |
|
| 47 |
- state: &dispatchState{runConfig: &container.Config{}},
|
|
| 48 |
- } |
|
| 49 |
-} |
|
| 50 |
- |
|
| 51 | 21 |
func newBuilderWithMockBackend() *Builder {
|
| 52 | 22 |
mockBackend := &MockBackend{}
|
| 53 | 23 |
ctx := context.Background() |
| 54 | 24 |
b := &Builder{
|
| 55 | 25 |
options: &types.ImageBuildOptions{},
|
| 56 | 26 |
docker: mockBackend, |
| 57 |
- buildArgs: newBuildArgs(make(map[string]*string)), |
|
| 58 | 27 |
Stdout: new(bytes.Buffer), |
| 59 | 28 |
clientCtx: ctx, |
| 60 | 29 |
disableCommit: true, |
| ... | ... |
@@ -62,137 +31,84 @@ func newBuilderWithMockBackend() *Builder {
|
| 62 | 62 |
Options: &types.ImageBuildOptions{},
|
| 63 | 63 |
Backend: mockBackend, |
| 64 | 64 |
}), |
| 65 |
- buildStages: newBuildStages(), |
|
| 66 | 65 |
imageProber: newImageProber(mockBackend, nil, runtime.GOOS, false), |
| 67 | 66 |
containerManager: newContainerManager(mockBackend), |
| 68 | 67 |
} |
| 69 | 68 |
return b |
| 70 | 69 |
} |
| 71 | 70 |
|
| 72 |
-func TestCommandsExactlyOneArgument(t *testing.T) {
|
|
| 73 |
- commands := []commandWithFunction{
|
|
| 74 |
- {"MAINTAINER", withArgs(maintainer)},
|
|
| 75 |
- {"WORKDIR", withArgs(workdir)},
|
|
| 76 |
- {"USER", withArgs(user)},
|
|
| 77 |
- {"STOPSIGNAL", withArgs(stopSignal)},
|
|
| 78 |
- } |
|
| 79 |
- |
|
| 80 |
- for _, command := range commands {
|
|
| 81 |
- err := command.function([]string{})
|
|
| 82 |
- assert.EqualError(t, err, errExactlyOneArgument(command.name).Error()) |
|
| 83 |
- } |
|
| 84 |
-} |
|
| 85 |
- |
|
| 86 |
-func TestCommandsAtLeastOneArgument(t *testing.T) {
|
|
| 87 |
- commands := []commandWithFunction{
|
|
| 88 |
- {"ENV", withArgs(env)},
|
|
| 89 |
- {"LABEL", withArgs(label)},
|
|
| 90 |
- {"ONBUILD", withArgs(onbuild)},
|
|
| 91 |
- {"HEALTHCHECK", withArgs(healthcheck)},
|
|
| 92 |
- {"EXPOSE", withArgs(expose)},
|
|
| 93 |
- {"VOLUME", withArgs(volume)},
|
|
| 94 |
- } |
|
| 95 |
- |
|
| 96 |
- for _, command := range commands {
|
|
| 97 |
- err := command.function([]string{})
|
|
| 98 |
- assert.EqualError(t, err, errAtLeastOneArgument(command.name).Error()) |
|
| 99 |
- } |
|
| 100 |
-} |
|
| 101 |
- |
|
| 102 |
-func TestCommandsAtLeastTwoArguments(t *testing.T) {
|
|
| 103 |
- commands := []commandWithFunction{
|
|
| 104 |
- {"ADD", withArgs(add)},
|
|
| 105 |
- {"COPY", withArgs(dispatchCopy)}}
|
|
| 106 |
- |
|
| 107 |
- for _, command := range commands {
|
|
| 108 |
- err := command.function([]string{"arg1"})
|
|
| 109 |
- assert.EqualError(t, err, errAtLeastTwoArguments(command.name).Error()) |
|
| 110 |
- } |
|
| 111 |
-} |
|
| 112 |
- |
|
| 113 |
-func TestCommandsTooManyArguments(t *testing.T) {
|
|
| 114 |
- commands := []commandWithFunction{
|
|
| 115 |
- {"ENV", withArgs(env)},
|
|
| 116 |
- {"LABEL", withArgs(label)}}
|
|
| 117 |
- |
|
| 118 |
- for _, command := range commands {
|
|
| 119 |
- err := command.function([]string{"arg1", "arg2", "arg3"})
|
|
| 120 |
- assert.EqualError(t, err, errTooManyArguments(command.name).Error()) |
|
| 121 |
- } |
|
| 122 |
-} |
|
| 123 |
- |
|
| 124 |
-func TestCommandsBlankNames(t *testing.T) {
|
|
| 125 |
- builder := newBuilderWithMockBackend() |
|
| 126 |
- commands := []commandWithFunction{
|
|
| 127 |
- {"ENV", withBuilderAndArgs(builder, env)},
|
|
| 128 |
- {"LABEL", withBuilderAndArgs(builder, label)},
|
|
| 129 |
- } |
|
| 130 |
- |
|
| 131 |
- for _, command := range commands {
|
|
| 132 |
- err := command.function([]string{"", ""})
|
|
| 133 |
- assert.EqualError(t, err, errBlankCommandNames(command.name).Error()) |
|
| 134 |
- } |
|
| 135 |
-} |
|
| 136 |
- |
|
| 137 | 71 |
func TestEnv2Variables(t *testing.T) {
|
| 138 | 72 |
b := newBuilderWithMockBackend() |
| 139 |
- |
|
| 140 |
- args := []string{"var1", "val1", "var2", "val2"}
|
|
| 141 |
- req := defaultDispatchReq(b, args...) |
|
| 142 |
- err := env(req) |
|
| 73 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 74 |
+ envCommand := &instructions.EnvCommand{
|
|
| 75 |
+ Env: instructions.KeyValuePairs{
|
|
| 76 |
+ instructions.KeyValuePair{Key: "var1", Value: "val1"},
|
|
| 77 |
+ instructions.KeyValuePair{Key: "var2", Value: "val2"},
|
|
| 78 |
+ }, |
|
| 79 |
+ } |
|
| 80 |
+ err := dispatch(sb, envCommand) |
|
| 143 | 81 |
require.NoError(t, err) |
| 144 | 82 |
|
| 145 | 83 |
expected := []string{
|
| 146 |
- fmt.Sprintf("%s=%s", args[0], args[1]),
|
|
| 147 |
- fmt.Sprintf("%s=%s", args[2], args[3]),
|
|
| 84 |
+ "var1=val1", |
|
| 85 |
+ "var2=val2", |
|
| 148 | 86 |
} |
| 149 |
- assert.Equal(t, expected, req.state.runConfig.Env) |
|
| 87 |
+ assert.Equal(t, expected, sb.state.runConfig.Env) |
|
| 150 | 88 |
} |
| 151 | 89 |
|
| 152 | 90 |
func TestEnvValueWithExistingRunConfigEnv(t *testing.T) {
|
| 153 | 91 |
b := newBuilderWithMockBackend() |
| 154 |
- |
|
| 155 |
- args := []string{"var1", "val1"}
|
|
| 156 |
- req := defaultDispatchReq(b, args...) |
|
| 157 |
- req.state.runConfig.Env = []string{"var1=old", "var2=fromenv"}
|
|
| 158 |
- err := env(req) |
|
| 92 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 93 |
+ sb.state.runConfig.Env = []string{"var1=old", "var2=fromenv"}
|
|
| 94 |
+ envCommand := &instructions.EnvCommand{
|
|
| 95 |
+ Env: instructions.KeyValuePairs{
|
|
| 96 |
+ instructions.KeyValuePair{Key: "var1", Value: "val1"},
|
|
| 97 |
+ }, |
|
| 98 |
+ } |
|
| 99 |
+ err := dispatch(sb, envCommand) |
|
| 159 | 100 |
require.NoError(t, err) |
| 160 |
- |
|
| 161 | 101 |
expected := []string{
|
| 162 |
- fmt.Sprintf("%s=%s", args[0], args[1]),
|
|
| 102 |
+ "var1=val1", |
|
| 163 | 103 |
"var2=fromenv", |
| 164 | 104 |
} |
| 165 |
- assert.Equal(t, expected, req.state.runConfig.Env) |
|
| 105 |
+ assert.Equal(t, expected, sb.state.runConfig.Env) |
|
| 166 | 106 |
} |
| 167 | 107 |
|
| 168 | 108 |
func TestMaintainer(t *testing.T) {
|
| 169 | 109 |
maintainerEntry := "Some Maintainer <maintainer@example.com>" |
| 170 |
- |
|
| 171 | 110 |
b := newBuilderWithMockBackend() |
| 172 |
- req := defaultDispatchReq(b, maintainerEntry) |
|
| 173 |
- err := maintainer(req) |
|
| 111 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 112 |
+ cmd := &instructions.MaintainerCommand{Maintainer: maintainerEntry}
|
|
| 113 |
+ err := dispatch(sb, cmd) |
|
| 174 | 114 |
require.NoError(t, err) |
| 175 |
- assert.Equal(t, maintainerEntry, req.state.maintainer) |
|
| 115 |
+ assert.Equal(t, maintainerEntry, sb.state.maintainer) |
|
| 176 | 116 |
} |
| 177 | 117 |
|
| 178 | 118 |
func TestLabel(t *testing.T) {
|
| 179 | 119 |
labelName := "label" |
| 180 | 120 |
labelValue := "value" |
| 181 | 121 |
|
| 182 |
- labelEntry := []string{labelName, labelValue}
|
|
| 183 | 122 |
b := newBuilderWithMockBackend() |
| 184 |
- req := defaultDispatchReq(b, labelEntry...) |
|
| 185 |
- err := label(req) |
|
| 123 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 124 |
+ cmd := &instructions.LabelCommand{
|
|
| 125 |
+ Labels: instructions.KeyValuePairs{
|
|
| 126 |
+ instructions.KeyValuePair{Key: labelName, Value: labelValue},
|
|
| 127 |
+ }, |
|
| 128 |
+ } |
|
| 129 |
+ err := dispatch(sb, cmd) |
|
| 186 | 130 |
require.NoError(t, err) |
| 187 | 131 |
|
| 188 |
- require.Contains(t, req.state.runConfig.Labels, labelName) |
|
| 189 |
- assert.Equal(t, req.state.runConfig.Labels[labelName], labelValue) |
|
| 132 |
+ require.Contains(t, sb.state.runConfig.Labels, labelName) |
|
| 133 |
+ assert.Equal(t, sb.state.runConfig.Labels[labelName], labelValue) |
|
| 190 | 134 |
} |
| 191 | 135 |
|
| 192 | 136 |
func TestFromScratch(t *testing.T) {
|
| 193 | 137 |
b := newBuilderWithMockBackend() |
| 194 |
- req := defaultDispatchReq(b, "scratch") |
|
| 195 |
- err := from(req) |
|
| 138 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 139 |
+ cmd := &instructions.Stage{
|
|
| 140 |
+ BaseName: "scratch", |
|
| 141 |
+ } |
|
| 142 |
+ err := initializeStage(sb, cmd) |
|
| 196 | 143 |
|
| 197 | 144 |
if runtime.GOOS == "windows" && !system.LCOWSupported() {
|
| 198 | 145 |
assert.EqualError(t, err, "Windows does not support FROM scratch") |
| ... | ... |
@@ -200,14 +116,14 @@ func TestFromScratch(t *testing.T) {
|
| 200 | 200 |
} |
| 201 | 201 |
|
| 202 | 202 |
require.NoError(t, err) |
| 203 |
- assert.True(t, req.state.hasFromImage()) |
|
| 204 |
- assert.Equal(t, "", req.state.imageID) |
|
| 203 |
+ assert.True(t, sb.state.hasFromImage()) |
|
| 204 |
+ assert.Equal(t, "", sb.state.imageID) |
|
| 205 | 205 |
// Windows does not set the default path. TODO @jhowardmsft LCOW support. This will need revisiting as we get further into the implementation |
| 206 | 206 |
expected := "PATH=" + system.DefaultPathEnv(runtime.GOOS) |
| 207 | 207 |
if runtime.GOOS == "windows" {
|
| 208 | 208 |
expected = "" |
| 209 | 209 |
} |
| 210 |
- assert.Equal(t, []string{expected}, req.state.runConfig.Env)
|
|
| 210 |
+ assert.Equal(t, []string{expected}, sb.state.runConfig.Env)
|
|
| 211 | 211 |
} |
| 212 | 212 |
|
| 213 | 213 |
func TestFromWithArg(t *testing.T) {
|
| ... | ... |
@@ -219,16 +135,27 @@ func TestFromWithArg(t *testing.T) {
|
| 219 | 219 |
} |
| 220 | 220 |
b := newBuilderWithMockBackend() |
| 221 | 221 |
b.docker.(*MockBackend).getImageFunc = getImage |
| 222 |
+ args := newBuildArgs(make(map[string]*string)) |
|
| 222 | 223 |
|
| 223 |
- require.NoError(t, arg(defaultDispatchReq(b, "THETAG="+tag))) |
|
| 224 |
- req := defaultDispatchReq(b, "alpine${THETAG}")
|
|
| 225 |
- err := from(req) |
|
| 224 |
+ val := "sometag" |
|
| 225 |
+ metaArg := instructions.ArgCommand{
|
|
| 226 |
+ Key: "THETAG", |
|
| 227 |
+ Value: &val, |
|
| 228 |
+ } |
|
| 229 |
+ cmd := &instructions.Stage{
|
|
| 230 |
+ BaseName: "alpine:${THETAG}",
|
|
| 231 |
+ } |
|
| 232 |
+ err := processMetaArg(metaArg, NewShellLex('\\'), args)
|
|
| 226 | 233 |
|
| 234 |
+ sb := newDispatchRequest(b, '\\', nil, args, newStagesBuildResults()) |
|
| 227 | 235 |
require.NoError(t, err) |
| 228 |
- assert.Equal(t, expected, req.state.imageID) |
|
| 229 |
- assert.Equal(t, expected, req.state.baseImage.ImageID()) |
|
| 230 |
- assert.Len(t, b.buildArgs.GetAllAllowed(), 0) |
|
| 231 |
- assert.Len(t, b.buildArgs.GetAllMeta(), 1) |
|
| 236 |
+ err = initializeStage(sb, cmd) |
|
| 237 |
+ require.NoError(t, err) |
|
| 238 |
+ |
|
| 239 |
+ assert.Equal(t, expected, sb.state.imageID) |
|
| 240 |
+ assert.Equal(t, expected, sb.state.baseImage.ImageID()) |
|
| 241 |
+ assert.Len(t, sb.state.buildArgs.GetAllAllowed(), 0) |
|
| 242 |
+ assert.Len(t, sb.state.buildArgs.GetAllMeta(), 1) |
|
| 232 | 243 |
} |
| 233 | 244 |
|
| 234 | 245 |
func TestFromWithUndefinedArg(t *testing.T) {
|
| ... | ... |
@@ -240,74 +167,74 @@ func TestFromWithUndefinedArg(t *testing.T) {
|
| 240 | 240 |
} |
| 241 | 241 |
b := newBuilderWithMockBackend() |
| 242 | 242 |
b.docker.(*MockBackend).getImageFunc = getImage |
| 243 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 244 |
+ |
|
| 243 | 245 |
b.options.BuildArgs = map[string]*string{"THETAG": &tag}
|
| 244 | 246 |
|
| 245 |
- req := defaultDispatchReq(b, "alpine${THETAG}")
|
|
| 246 |
- err := from(req) |
|
| 247 |
+ cmd := &instructions.Stage{
|
|
| 248 |
+ BaseName: "alpine${THETAG}",
|
|
| 249 |
+ } |
|
| 250 |
+ err := initializeStage(sb, cmd) |
|
| 247 | 251 |
require.NoError(t, err) |
| 248 |
- assert.Equal(t, expected, req.state.imageID) |
|
| 252 |
+ assert.Equal(t, expected, sb.state.imageID) |
|
| 249 | 253 |
} |
| 250 | 254 |
|
| 251 |
-func TestFromMultiStageWithScratchNamedStage(t *testing.T) {
|
|
| 252 |
- if runtime.GOOS == "windows" {
|
|
| 253 |
- t.Skip("Windows does not support scratch")
|
|
| 254 |
- } |
|
| 255 |
+func TestFromMultiStageWithNamedStage(t *testing.T) {
|
|
| 255 | 256 |
b := newBuilderWithMockBackend() |
| 256 |
- req := defaultDispatchReq(b, "scratch", "AS", "base") |
|
| 257 |
- |
|
| 258 |
- require.NoError(t, from(req)) |
|
| 259 |
- assert.True(t, req.state.hasFromImage()) |
|
| 260 |
- |
|
| 261 |
- req.args = []string{"base"}
|
|
| 262 |
- require.NoError(t, from(req)) |
|
| 263 |
- assert.True(t, req.state.hasFromImage()) |
|
| 264 |
-} |
|
| 265 |
- |
|
| 266 |
-func TestOnbuildIllegalTriggers(t *testing.T) {
|
|
| 267 |
- triggers := []struct{ command, expectedError string }{
|
|
| 268 |
- {"ONBUILD", "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed"},
|
|
| 269 |
- {"MAINTAINER", "MAINTAINER isn't allowed as an ONBUILD trigger"},
|
|
| 270 |
- {"FROM", "FROM isn't allowed as an ONBUILD trigger"}}
|
|
| 271 |
- |
|
| 272 |
- for _, trigger := range triggers {
|
|
| 273 |
- b := newBuilderWithMockBackend() |
|
| 274 |
- |
|
| 275 |
- err := onbuild(defaultDispatchReq(b, trigger.command)) |
|
| 276 |
- testutil.ErrorContains(t, err, trigger.expectedError) |
|
| 277 |
- } |
|
| 257 |
+ firstFrom := &instructions.Stage{BaseName: "someimg", Name: "base"}
|
|
| 258 |
+ secondFrom := &instructions.Stage{BaseName: "base"}
|
|
| 259 |
+ previousResults := newStagesBuildResults() |
|
| 260 |
+ firstSB := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), previousResults) |
|
| 261 |
+ secondSB := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), previousResults) |
|
| 262 |
+ err := initializeStage(firstSB, firstFrom) |
|
| 263 |
+ require.NoError(t, err) |
|
| 264 |
+ assert.True(t, firstSB.state.hasFromImage()) |
|
| 265 |
+ previousResults.indexed["base"] = firstSB.state.runConfig |
|
| 266 |
+ previousResults.flat = append(previousResults.flat, firstSB.state.runConfig) |
|
| 267 |
+ err = initializeStage(secondSB, secondFrom) |
|
| 268 |
+ require.NoError(t, err) |
|
| 269 |
+ assert.True(t, secondSB.state.hasFromImage()) |
|
| 278 | 270 |
} |
| 279 | 271 |
|
| 280 | 272 |
func TestOnbuild(t *testing.T) {
|
| 281 | 273 |
b := newBuilderWithMockBackend() |
| 282 |
- |
|
| 283 |
- req := defaultDispatchReq(b, "ADD", ".", "/app/src") |
|
| 284 |
- req.original = "ONBUILD ADD . /app/src" |
|
| 285 |
- req.state.runConfig = &container.Config{}
|
|
| 286 |
- |
|
| 287 |
- err := onbuild(req) |
|
| 274 |
+ sb := newDispatchRequest(b, '\\', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 275 |
+ cmd := &instructions.OnbuildCommand{
|
|
| 276 |
+ Expression: "ADD . /app/src", |
|
| 277 |
+ } |
|
| 278 |
+ err := dispatch(sb, cmd) |
|
| 288 | 279 |
require.NoError(t, err) |
| 289 |
- assert.Equal(t, "ADD . /app/src", req.state.runConfig.OnBuild[0]) |
|
| 280 |
+ assert.Equal(t, "ADD . /app/src", sb.state.runConfig.OnBuild[0]) |
|
| 290 | 281 |
} |
| 291 | 282 |
|
| 292 | 283 |
func TestWorkdir(t *testing.T) {
|
| 293 | 284 |
b := newBuilderWithMockBackend() |
| 285 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 294 | 286 |
workingDir := "/app" |
| 295 | 287 |
if runtime.GOOS == "windows" {
|
| 296 |
- workingDir = "C:\app" |
|
| 288 |
+ workingDir = "C:\\app" |
|
| 289 |
+ } |
|
| 290 |
+ cmd := &instructions.WorkdirCommand{
|
|
| 291 |
+ Path: workingDir, |
|
| 297 | 292 |
} |
| 298 | 293 |
|
| 299 |
- req := defaultDispatchReq(b, workingDir) |
|
| 300 |
- err := workdir(req) |
|
| 294 |
+ err := dispatch(sb, cmd) |
|
| 301 | 295 |
require.NoError(t, err) |
| 302 |
- assert.Equal(t, workingDir, req.state.runConfig.WorkingDir) |
|
| 296 |
+ assert.Equal(t, workingDir, sb.state.runConfig.WorkingDir) |
|
| 303 | 297 |
} |
| 304 | 298 |
|
| 305 | 299 |
func TestCmd(t *testing.T) {
|
| 306 | 300 |
b := newBuilderWithMockBackend() |
| 301 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 307 | 302 |
command := "./executable" |
| 308 | 303 |
|
| 309 |
- req := defaultDispatchReq(b, command) |
|
| 310 |
- err := cmd(req) |
|
| 304 |
+ cmd := &instructions.CmdCommand{
|
|
| 305 |
+ ShellDependantCmdLine: instructions.ShellDependantCmdLine{
|
|
| 306 |
+ CmdLine: strslice.StrSlice{command},
|
|
| 307 |
+ PrependShell: true, |
|
| 308 |
+ }, |
|
| 309 |
+ } |
|
| 310 |
+ err := dispatch(sb, cmd) |
|
| 311 | 311 |
require.NoError(t, err) |
| 312 | 312 |
|
| 313 | 313 |
var expectedCommand strslice.StrSlice |
| ... | ... |
@@ -317,42 +244,56 @@ func TestCmd(t *testing.T) {
|
| 317 | 317 |
expectedCommand = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", command))
|
| 318 | 318 |
} |
| 319 | 319 |
|
| 320 |
- assert.Equal(t, expectedCommand, req.state.runConfig.Cmd) |
|
| 321 |
- assert.True(t, req.state.cmdSet) |
|
| 320 |
+ assert.Equal(t, expectedCommand, sb.state.runConfig.Cmd) |
|
| 321 |
+ assert.True(t, sb.state.cmdSet) |
|
| 322 | 322 |
} |
| 323 | 323 |
|
| 324 | 324 |
func TestHealthcheckNone(t *testing.T) {
|
| 325 | 325 |
b := newBuilderWithMockBackend() |
| 326 |
- |
|
| 327 |
- req := defaultDispatchReq(b, "NONE") |
|
| 328 |
- err := healthcheck(req) |
|
| 326 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 327 |
+ cmd := &instructions.HealthCheckCommand{
|
|
| 328 |
+ Health: &container.HealthConfig{
|
|
| 329 |
+ Test: []string{"NONE"},
|
|
| 330 |
+ }, |
|
| 331 |
+ } |
|
| 332 |
+ err := dispatch(sb, cmd) |
|
| 329 | 333 |
require.NoError(t, err) |
| 330 | 334 |
|
| 331 |
- require.NotNil(t, req.state.runConfig.Healthcheck) |
|
| 332 |
- assert.Equal(t, []string{"NONE"}, req.state.runConfig.Healthcheck.Test)
|
|
| 335 |
+ require.NotNil(t, sb.state.runConfig.Healthcheck) |
|
| 336 |
+ assert.Equal(t, []string{"NONE"}, sb.state.runConfig.Healthcheck.Test)
|
|
| 333 | 337 |
} |
| 334 | 338 |
|
| 335 | 339 |
func TestHealthcheckCmd(t *testing.T) {
|
| 336 |
- b := newBuilderWithMockBackend() |
|
| 337 | 340 |
|
| 338 |
- args := []string{"CMD", "curl", "-f", "http://localhost/", "||", "exit", "1"}
|
|
| 339 |
- req := defaultDispatchReq(b, args...) |
|
| 340 |
- err := healthcheck(req) |
|
| 341 |
+ b := newBuilderWithMockBackend() |
|
| 342 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 343 |
+ expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"}
|
|
| 344 |
+ cmd := &instructions.HealthCheckCommand{
|
|
| 345 |
+ Health: &container.HealthConfig{
|
|
| 346 |
+ Test: expectedTest, |
|
| 347 |
+ }, |
|
| 348 |
+ } |
|
| 349 |
+ err := dispatch(sb, cmd) |
|
| 341 | 350 |
require.NoError(t, err) |
| 342 | 351 |
|
| 343 |
- require.NotNil(t, req.state.runConfig.Healthcheck) |
|
| 344 |
- expectedTest := []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"}
|
|
| 345 |
- assert.Equal(t, expectedTest, req.state.runConfig.Healthcheck.Test) |
|
| 352 |
+ require.NotNil(t, sb.state.runConfig.Healthcheck) |
|
| 353 |
+ assert.Equal(t, expectedTest, sb.state.runConfig.Healthcheck.Test) |
|
| 346 | 354 |
} |
| 347 | 355 |
|
| 348 | 356 |
func TestEntrypoint(t *testing.T) {
|
| 349 | 357 |
b := newBuilderWithMockBackend() |
| 358 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 350 | 359 |
entrypointCmd := "/usr/sbin/nginx" |
| 351 | 360 |
|
| 352 |
- req := defaultDispatchReq(b, entrypointCmd) |
|
| 353 |
- err := entrypoint(req) |
|
| 361 |
+ cmd := &instructions.EntrypointCommand{
|
|
| 362 |
+ ShellDependantCmdLine: instructions.ShellDependantCmdLine{
|
|
| 363 |
+ CmdLine: strslice.StrSlice{entrypointCmd},
|
|
| 364 |
+ PrependShell: true, |
|
| 365 |
+ }, |
|
| 366 |
+ } |
|
| 367 |
+ err := dispatch(sb, cmd) |
|
| 354 | 368 |
require.NoError(t, err) |
| 355 |
- require.NotNil(t, req.state.runConfig.Entrypoint) |
|
| 369 |
+ require.NotNil(t, sb.state.runConfig.Entrypoint) |
|
| 356 | 370 |
|
| 357 | 371 |
var expectedEntrypoint strslice.StrSlice |
| 358 | 372 |
if runtime.GOOS == "windows" {
|
| ... | ... |
@@ -360,99 +301,99 @@ func TestEntrypoint(t *testing.T) {
|
| 360 | 360 |
} else {
|
| 361 | 361 |
expectedEntrypoint = strslice.StrSlice(append([]string{"/bin/sh"}, "-c", entrypointCmd))
|
| 362 | 362 |
} |
| 363 |
- assert.Equal(t, expectedEntrypoint, req.state.runConfig.Entrypoint) |
|
| 363 |
+ assert.Equal(t, expectedEntrypoint, sb.state.runConfig.Entrypoint) |
|
| 364 | 364 |
} |
| 365 | 365 |
|
| 366 | 366 |
func TestExpose(t *testing.T) {
|
| 367 | 367 |
b := newBuilderWithMockBackend() |
| 368 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 368 | 369 |
|
| 369 | 370 |
exposedPort := "80" |
| 370 |
- req := defaultDispatchReq(b, exposedPort) |
|
| 371 |
- err := expose(req) |
|
| 371 |
+ cmd := &instructions.ExposeCommand{
|
|
| 372 |
+ Ports: []string{exposedPort},
|
|
| 373 |
+ } |
|
| 374 |
+ err := dispatch(sb, cmd) |
|
| 372 | 375 |
require.NoError(t, err) |
| 373 | 376 |
|
| 374 |
- require.NotNil(t, req.state.runConfig.ExposedPorts) |
|
| 375 |
- require.Len(t, req.state.runConfig.ExposedPorts, 1) |
|
| 377 |
+ require.NotNil(t, sb.state.runConfig.ExposedPorts) |
|
| 378 |
+ require.Len(t, sb.state.runConfig.ExposedPorts, 1) |
|
| 376 | 379 |
|
| 377 | 380 |
portsMapping, err := nat.ParsePortSpec(exposedPort) |
| 378 | 381 |
require.NoError(t, err) |
| 379 |
- assert.Contains(t, req.state.runConfig.ExposedPorts, portsMapping[0].Port) |
|
| 382 |
+ assert.Contains(t, sb.state.runConfig.ExposedPorts, portsMapping[0].Port) |
|
| 380 | 383 |
} |
| 381 | 384 |
|
| 382 | 385 |
func TestUser(t *testing.T) {
|
| 383 | 386 |
b := newBuilderWithMockBackend() |
| 384 |
- userCommand := "foo" |
|
| 387 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 385 | 388 |
|
| 386 |
- req := defaultDispatchReq(b, userCommand) |
|
| 387 |
- err := user(req) |
|
| 389 |
+ cmd := &instructions.UserCommand{
|
|
| 390 |
+ User: "test", |
|
| 391 |
+ } |
|
| 392 |
+ err := dispatch(sb, cmd) |
|
| 388 | 393 |
require.NoError(t, err) |
| 389 |
- assert.Equal(t, userCommand, req.state.runConfig.User) |
|
| 394 |
+ assert.Equal(t, "test", sb.state.runConfig.User) |
|
| 390 | 395 |
} |
| 391 | 396 |
|
| 392 | 397 |
func TestVolume(t *testing.T) {
|
| 393 | 398 |
b := newBuilderWithMockBackend() |
| 399 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 394 | 400 |
|
| 395 | 401 |
exposedVolume := "/foo" |
| 396 | 402 |
|
| 397 |
- req := defaultDispatchReq(b, exposedVolume) |
|
| 398 |
- err := volume(req) |
|
| 403 |
+ cmd := &instructions.VolumeCommand{
|
|
| 404 |
+ Volumes: []string{exposedVolume},
|
|
| 405 |
+ } |
|
| 406 |
+ err := dispatch(sb, cmd) |
|
| 399 | 407 |
require.NoError(t, err) |
| 400 |
- |
|
| 401 |
- require.NotNil(t, req.state.runConfig.Volumes) |
|
| 402 |
- assert.Len(t, req.state.runConfig.Volumes, 1) |
|
| 403 |
- assert.Contains(t, req.state.runConfig.Volumes, exposedVolume) |
|
| 408 |
+ require.NotNil(t, sb.state.runConfig.Volumes) |
|
| 409 |
+ assert.Len(t, sb.state.runConfig.Volumes, 1) |
|
| 410 |
+ assert.Contains(t, sb.state.runConfig.Volumes, exposedVolume) |
|
| 404 | 411 |
} |
| 405 | 412 |
|
| 406 | 413 |
func TestStopSignal(t *testing.T) {
|
| 414 |
+ if runtime.GOOS == "windows" {
|
|
| 415 |
+ t.Skip("Windows does not support stopsignal")
|
|
| 416 |
+ return |
|
| 417 |
+ } |
|
| 407 | 418 |
b := newBuilderWithMockBackend() |
| 419 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 408 | 420 |
signal := "SIGKILL" |
| 409 | 421 |
|
| 410 |
- req := defaultDispatchReq(b, signal) |
|
| 411 |
- err := stopSignal(req) |
|
| 422 |
+ cmd := &instructions.StopSignalCommand{
|
|
| 423 |
+ Signal: signal, |
|
| 424 |
+ } |
|
| 425 |
+ err := dispatch(sb, cmd) |
|
| 412 | 426 |
require.NoError(t, err) |
| 413 |
- assert.Equal(t, signal, req.state.runConfig.StopSignal) |
|
| 427 |
+ assert.Equal(t, signal, sb.state.runConfig.StopSignal) |
|
| 414 | 428 |
} |
| 415 | 429 |
|
| 416 | 430 |
func TestArg(t *testing.T) {
|
| 417 | 431 |
b := newBuilderWithMockBackend() |
| 432 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 418 | 433 |
|
| 419 | 434 |
argName := "foo" |
| 420 | 435 |
argVal := "bar" |
| 421 |
- argDef := fmt.Sprintf("%s=%s", argName, argVal)
|
|
| 422 |
- |
|
| 423 |
- err := arg(defaultDispatchReq(b, argDef)) |
|
| 436 |
+ cmd := &instructions.ArgCommand{Key: argName, Value: &argVal}
|
|
| 437 |
+ err := dispatch(sb, cmd) |
|
| 424 | 438 |
require.NoError(t, err) |
| 425 | 439 |
|
| 426 | 440 |
expected := map[string]string{argName: argVal}
|
| 427 |
- assert.Equal(t, expected, b.buildArgs.GetAllAllowed()) |
|
| 441 |
+ assert.Equal(t, expected, sb.state.buildArgs.GetAllAllowed()) |
|
| 428 | 442 |
} |
| 429 | 443 |
|
| 430 | 444 |
func TestShell(t *testing.T) {
|
| 431 | 445 |
b := newBuilderWithMockBackend() |
| 446 |
+ sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 432 | 447 |
|
| 433 | 448 |
shellCmd := "powershell" |
| 434 |
- req := defaultDispatchReq(b, shellCmd) |
|
| 435 |
- req.attributes = map[string]bool{"json": true}
|
|
| 449 |
+ cmd := &instructions.ShellCommand{Shell: strslice.StrSlice{shellCmd}}
|
|
| 436 | 450 |
|
| 437 |
- err := shell(req) |
|
| 451 |
+ err := dispatch(sb, cmd) |
|
| 438 | 452 |
require.NoError(t, err) |
| 439 | 453 |
|
| 440 | 454 |
expectedShell := strslice.StrSlice([]string{shellCmd})
|
| 441 |
- assert.Equal(t, expectedShell, req.state.runConfig.Shell) |
|
| 442 |
-} |
|
| 443 |
- |
|
| 444 |
-func TestParseOptInterval(t *testing.T) {
|
|
| 445 |
- flInterval := &Flag{
|
|
| 446 |
- name: "interval", |
|
| 447 |
- flagType: stringType, |
|
| 448 |
- Value: "50ns", |
|
| 449 |
- } |
|
| 450 |
- _, err := parseOptInterval(flInterval) |
|
| 451 |
- testutil.ErrorContains(t, err, "cannot be less than 1ms") |
|
| 452 |
- |
|
| 453 |
- flInterval.Value = "1ms" |
|
| 454 |
- _, err = parseOptInterval(flInterval) |
|
| 455 |
- require.NoError(t, err) |
|
| 455 |
+ assert.Equal(t, expectedShell, sb.state.runConfig.Shell) |
|
| 456 | 456 |
} |
| 457 | 457 |
|
| 458 | 458 |
func TestPrependEnvOnCmd(t *testing.T) {
|
| ... | ... |
@@ -469,8 +410,10 @@ func TestPrependEnvOnCmd(t *testing.T) {
|
| 469 | 469 |
|
| 470 | 470 |
func TestRunWithBuildArgs(t *testing.T) {
|
| 471 | 471 |
b := newBuilderWithMockBackend() |
| 472 |
- b.buildArgs.argsFromOptions["HTTP_PROXY"] = strPtr("FOO")
|
|
| 472 |
+ args := newBuildArgs(make(map[string]*string)) |
|
| 473 |
+ args.argsFromOptions["HTTP_PROXY"] = strPtr("FOO")
|
|
| 473 | 474 |
b.disableCommit = false |
| 475 |
+ sb := newDispatchRequest(b, '`', nil, args, newStagesBuildResults()) |
|
| 474 | 476 |
|
| 475 | 477 |
runConfig := &container.Config{}
|
| 476 | 478 |
origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"})
|
| ... | ... |
@@ -512,14 +455,18 @@ func TestRunWithBuildArgs(t *testing.T) {
|
| 512 | 512 |
assert.Equal(t, strslice.StrSlice(nil), cfg.Config.Entrypoint) |
| 513 | 513 |
return "", nil |
| 514 | 514 |
} |
| 515 |
- |
|
| 516 |
- req := defaultDispatchReq(b, "abcdef") |
|
| 517 |
- require.NoError(t, from(req)) |
|
| 518 |
- b.buildArgs.AddArg("one", strPtr("two"))
|
|
| 519 |
- |
|
| 520 |
- req.args = []string{"echo foo"}
|
|
| 521 |
- require.NoError(t, run(req)) |
|
| 515 |
+ from := &instructions.Stage{BaseName: "abcdef"}
|
|
| 516 |
+ err := initializeStage(sb, from) |
|
| 517 |
+ require.NoError(t, err) |
|
| 518 |
+ sb.state.buildArgs.AddArg("one", strPtr("two"))
|
|
| 519 |
+ run := &instructions.RunCommand{
|
|
| 520 |
+ ShellDependantCmdLine: instructions.ShellDependantCmdLine{
|
|
| 521 |
+ CmdLine: strslice.StrSlice{"echo foo"},
|
|
| 522 |
+ PrependShell: true, |
|
| 523 |
+ }, |
|
| 524 |
+ } |
|
| 525 |
+ require.NoError(t, dispatch(sb, run)) |
|
| 522 | 526 |
|
| 523 | 527 |
// Check that runConfig.Cmd has not been modified by run |
| 524 |
- assert.Equal(t, origCmd, req.state.runConfig.Cmd) |
|
| 528 |
+ assert.Equal(t, origCmd, sb.state.runConfig.Cmd) |
|
| 525 | 529 |
} |
| ... | ... |
@@ -4,7 +4,6 @@ package dockerfile |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 | 6 |
"errors" |
| 7 |
- "fmt" |
|
| 8 | 7 |
"os" |
| 9 | 8 |
"path/filepath" |
| 10 | 9 |
) |
| ... | ... |
@@ -23,10 +22,6 @@ func normalizeWorkdir(_ string, current string, requested string) (string, error |
| 23 | 23 |
return requested, nil |
| 24 | 24 |
} |
| 25 | 25 |
|
| 26 |
-func errNotJSON(command, _ string) error {
|
|
| 27 |
- return fmt.Errorf("%s requires the arguments to be in JSON form", command)
|
|
| 28 |
-} |
|
| 29 |
- |
|
| 30 | 26 |
// equalEnvKeys compare two strings and returns true if they are equal. On |
| 31 | 27 |
// Windows this comparison is case insensitive. |
| 32 | 28 |
func equalEnvKeys(from, to string) bool {
|
| ... | ... |
@@ -94,25 +94,6 @@ func normalizeWorkdirWindows(current string, requested string) (string, error) {
|
| 94 | 94 |
return (strings.ToUpper(string(requested[0])) + requested[1:]), nil |
| 95 | 95 |
} |
| 96 | 96 |
|
| 97 |
-func errNotJSON(command, original string) error {
|
|
| 98 |
- // For Windows users, give a hint if it looks like it might contain |
|
| 99 |
- // a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"], |
|
| 100 |
- // as JSON must be escaped. Unfortunate... |
|
| 101 |
- // |
|
| 102 |
- // Specifically looking for quote-driveletter-colon-backslash, there's no |
|
| 103 |
- // double backslash and a [] pair. No, this is not perfect, but it doesn't |
|
| 104 |
- // have to be. It's simply a hint to make life a little easier. |
|
| 105 |
- extra := "" |
|
| 106 |
- original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1))) |
|
| 107 |
- if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 && |
|
| 108 |
- !strings.Contains(original, `\\`) && |
|
| 109 |
- strings.Contains(original, "[") && |
|
| 110 |
- strings.Contains(original, "]") {
|
|
| 111 |
- extra = fmt.Sprintf(`. It looks like '%s' includes a file path without an escaped back-slash. JSON requires back-slashes to be escaped such as ["c:\\path\\to\\file.exe", "/parameter"]`, original) |
|
| 112 |
- } |
|
| 113 |
- return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra)
|
|
| 114 |
-} |
|
| 115 |
- |
|
| 116 | 97 |
// equalEnvKeys compare two strings and returns true if they are equal. On |
| 117 | 98 |
// Windows this comparison is case insensitive. |
| 118 | 99 |
func equalEnvKeys(from, to string) bool {
|
| ... | ... |
@@ -20,183 +20,178 @@ |
| 20 | 20 |
package dockerfile |
| 21 | 21 |
|
| 22 | 22 |
import ( |
| 23 |
- "bytes" |
|
| 24 |
- "fmt" |
|
| 23 |
+ "reflect" |
|
| 25 | 24 |
"runtime" |
| 25 |
+ "strconv" |
|
| 26 | 26 |
"strings" |
| 27 | 27 |
|
| 28 | 28 |
"github.com/docker/docker/api/types/container" |
| 29 | 29 |
"github.com/docker/docker/builder" |
| 30 |
- "github.com/docker/docker/builder/dockerfile/command" |
|
| 31 |
- "github.com/docker/docker/builder/dockerfile/parser" |
|
| 30 |
+ "github.com/docker/docker/builder/dockerfile/instructions" |
|
| 32 | 31 |
"github.com/docker/docker/pkg/system" |
| 33 | 32 |
"github.com/docker/docker/runconfig/opts" |
| 34 | 33 |
"github.com/pkg/errors" |
| 35 | 34 |
) |
| 36 | 35 |
|
| 37 |
-// Environment variable interpolation will happen on these statements only. |
|
| 38 |
-var replaceEnvAllowed = map[string]bool{
|
|
| 39 |
- command.Env: true, |
|
| 40 |
- command.Label: true, |
|
| 41 |
- command.Add: true, |
|
| 42 |
- command.Copy: true, |
|
| 43 |
- command.Workdir: true, |
|
| 44 |
- command.Expose: true, |
|
| 45 |
- command.Volume: true, |
|
| 46 |
- command.User: true, |
|
| 47 |
- command.StopSignal: true, |
|
| 48 |
- command.Arg: true, |
|
| 49 |
-} |
|
| 36 |
+func dispatch(d dispatchRequest, cmd instructions.Command) error {
|
|
| 37 |
+ if c, ok := cmd.(instructions.PlatformSpecific); ok {
|
|
| 38 |
+ err := c.CheckPlatform(d.builder.platform) |
|
| 39 |
+ if err != nil {
|
|
| 40 |
+ return validationError{err}
|
|
| 41 |
+ } |
|
| 42 |
+ } |
|
| 43 |
+ runConfigEnv := d.state.runConfig.Env |
|
| 44 |
+ envs := append(runConfigEnv, d.state.buildArgs.FilterAllowed(runConfigEnv)...) |
|
| 50 | 45 |
|
| 51 |
-// Certain commands are allowed to have their args split into more |
|
| 52 |
-// words after env var replacements. Meaning: |
|
| 53 |
-// ENV foo="123 456" |
|
| 54 |
-// EXPOSE $foo |
|
| 55 |
-// should result in the same thing as: |
|
| 56 |
-// EXPOSE 123 456 |
|
| 57 |
-// and not treat "123 456" as a single word. |
|
| 58 |
-// Note that: EXPOSE "$foo" and EXPOSE $foo are not the same thing. |
|
| 59 |
-// Quotes will cause it to still be treated as single word. |
|
| 60 |
-var allowWordExpansion = map[string]bool{
|
|
| 61 |
- command.Expose: true, |
|
| 62 |
-} |
|
| 46 |
+ if ex, ok := cmd.(instructions.SupportsSingleWordExpansion); ok {
|
|
| 47 |
+ err := ex.Expand(func(word string) (string, error) {
|
|
| 48 |
+ return d.shlex.ProcessWord(word, envs) |
|
| 49 |
+ }) |
|
| 50 |
+ if err != nil {
|
|
| 51 |
+ return validationError{err}
|
|
| 52 |
+ } |
|
| 53 |
+ } |
|
| 63 | 54 |
|
| 64 |
-type dispatchRequest struct {
|
|
| 65 |
- builder *Builder // TODO: replace this with a smaller interface |
|
| 66 |
- args []string |
|
| 67 |
- attributes map[string]bool |
|
| 68 |
- flags *BFlags |
|
| 69 |
- original string |
|
| 70 |
- shlex *ShellLex |
|
| 71 |
- state *dispatchState |
|
| 72 |
- source builder.Source |
|
| 55 |
+ if d.builder.options.ForceRemove {
|
|
| 56 |
+ defer d.builder.containerManager.RemoveAll(d.builder.Stdout) |
|
| 57 |
+ } |
|
| 58 |
+ |
|
| 59 |
+ switch c := cmd.(type) {
|
|
| 60 |
+ case *instructions.EnvCommand: |
|
| 61 |
+ return dispatchEnv(d, c) |
|
| 62 |
+ case *instructions.MaintainerCommand: |
|
| 63 |
+ return dispatchMaintainer(d, c) |
|
| 64 |
+ case *instructions.LabelCommand: |
|
| 65 |
+ return dispatchLabel(d, c) |
|
| 66 |
+ case *instructions.AddCommand: |
|
| 67 |
+ return dispatchAdd(d, c) |
|
| 68 |
+ case *instructions.CopyCommand: |
|
| 69 |
+ return dispatchCopy(d, c) |
|
| 70 |
+ case *instructions.OnbuildCommand: |
|
| 71 |
+ return dispatchOnbuild(d, c) |
|
| 72 |
+ case *instructions.WorkdirCommand: |
|
| 73 |
+ return dispatchWorkdir(d, c) |
|
| 74 |
+ case *instructions.RunCommand: |
|
| 75 |
+ return dispatchRun(d, c) |
|
| 76 |
+ case *instructions.CmdCommand: |
|
| 77 |
+ return dispatchCmd(d, c) |
|
| 78 |
+ case *instructions.HealthCheckCommand: |
|
| 79 |
+ return dispatchHealthcheck(d, c) |
|
| 80 |
+ case *instructions.EntrypointCommand: |
|
| 81 |
+ return dispatchEntrypoint(d, c) |
|
| 82 |
+ case *instructions.ExposeCommand: |
|
| 83 |
+ return dispatchExpose(d, c, envs) |
|
| 84 |
+ case *instructions.UserCommand: |
|
| 85 |
+ return dispatchUser(d, c) |
|
| 86 |
+ case *instructions.VolumeCommand: |
|
| 87 |
+ return dispatchVolume(d, c) |
|
| 88 |
+ case *instructions.StopSignalCommand: |
|
| 89 |
+ return dispatchStopSignal(d, c) |
|
| 90 |
+ case *instructions.ArgCommand: |
|
| 91 |
+ return dispatchArg(d, c) |
|
| 92 |
+ case *instructions.ShellCommand: |
|
| 93 |
+ return dispatchShell(d, c) |
|
| 94 |
+ } |
|
| 95 |
+ return errors.Errorf("unsupported command type: %v", reflect.TypeOf(cmd))
|
|
| 73 | 96 |
} |
| 74 | 97 |
|
| 75 |
-func newDispatchRequestFromOptions(options dispatchOptions, builder *Builder, args []string) dispatchRequest {
|
|
| 76 |
- return dispatchRequest{
|
|
| 77 |
- builder: builder, |
|
| 78 |
- args: args, |
|
| 79 |
- attributes: options.node.Attributes, |
|
| 80 |
- original: options.node.Original, |
|
| 81 |
- flags: NewBFlagsWithArgs(options.node.Flags), |
|
| 82 |
- shlex: options.shlex, |
|
| 83 |
- state: options.state, |
|
| 84 |
- source: options.source, |
|
| 85 |
- } |
|
| 98 |
+// dispatchState is a data object which is modified by dispatchers |
|
| 99 |
+type dispatchState struct {
|
|
| 100 |
+ runConfig *container.Config |
|
| 101 |
+ maintainer string |
|
| 102 |
+ cmdSet bool |
|
| 103 |
+ imageID string |
|
| 104 |
+ baseImage builder.Image |
|
| 105 |
+ stageName string |
|
| 106 |
+ buildArgs *buildArgs |
|
| 86 | 107 |
} |
| 87 | 108 |
|
| 88 |
-type dispatcher func(dispatchRequest) error |
|
| 109 |
+func newDispatchState(baseArgs *buildArgs) *dispatchState {
|
|
| 110 |
+ args := baseArgs.Clone() |
|
| 111 |
+ args.ResetAllowed() |
|
| 112 |
+ return &dispatchState{runConfig: &container.Config{}, buildArgs: args}
|
|
| 113 |
+} |
|
| 89 | 114 |
|
| 90 |
-var evaluateTable map[string]dispatcher |
|
| 115 |
+type stagesBuildResults struct {
|
|
| 116 |
+ flat []*container.Config |
|
| 117 |
+ indexed map[string]*container.Config |
|
| 118 |
+} |
|
| 91 | 119 |
|
| 92 |
-func init() {
|
|
| 93 |
- evaluateTable = map[string]dispatcher{
|
|
| 94 |
- command.Add: add, |
|
| 95 |
- command.Arg: arg, |
|
| 96 |
- command.Cmd: cmd, |
|
| 97 |
- command.Copy: dispatchCopy, // copy() is a go builtin |
|
| 98 |
- command.Entrypoint: entrypoint, |
|
| 99 |
- command.Env: env, |
|
| 100 |
- command.Expose: expose, |
|
| 101 |
- command.From: from, |
|
| 102 |
- command.Healthcheck: healthcheck, |
|
| 103 |
- command.Label: label, |
|
| 104 |
- command.Maintainer: maintainer, |
|
| 105 |
- command.Onbuild: onbuild, |
|
| 106 |
- command.Run: run, |
|
| 107 |
- command.Shell: shell, |
|
| 108 |
- command.StopSignal: stopSignal, |
|
| 109 |
- command.User: user, |
|
| 110 |
- command.Volume: volume, |
|
| 111 |
- command.Workdir: workdir, |
|
| 120 |
+func newStagesBuildResults() *stagesBuildResults {
|
|
| 121 |
+ return &stagesBuildResults{
|
|
| 122 |
+ indexed: make(map[string]*container.Config), |
|
| 112 | 123 |
} |
| 113 | 124 |
} |
| 114 | 125 |
|
| 115 |
-func formatStep(stepN int, stepTotal int) string {
|
|
| 116 |
- return fmt.Sprintf("%d/%d", stepN+1, stepTotal)
|
|
| 126 |
+func (r *stagesBuildResults) getByName(name string) (*container.Config, bool) {
|
|
| 127 |
+ c, ok := r.indexed[strings.ToLower(name)] |
|
| 128 |
+ return c, ok |
|
| 117 | 129 |
} |
| 118 | 130 |
|
| 119 |
-// This method is the entrypoint to all statement handling routines. |
|
| 120 |
-// |
|
| 121 |
-// Almost all nodes will have this structure: |
|
| 122 |
-// Child[Node, Node, Node] where Child is from parser.Node.Children and each |
|
| 123 |
-// node comes from parser.Node.Next. This forms a "line" with a statement and |
|
| 124 |
-// arguments and we process them in this normalized form by hitting |
|
| 125 |
-// evaluateTable with the leaf nodes of the command and the Builder object. |
|
| 126 |
-// |
|
| 127 |
-// ONBUILD is a special case; in this case the parser will emit: |
|
| 128 |
-// Child[Node, Child[Node, Node...]] where the first node is the literal |
|
| 129 |
-// "onbuild" and the child entrypoint is the command of the ONBUILD statement, |
|
| 130 |
-// such as `RUN` in ONBUILD RUN foo. There is special case logic in here to |
|
| 131 |
-// deal with that, at least until it becomes more of a general concern with new |
|
| 132 |
-// features. |
|
| 133 |
-func (b *Builder) dispatch(options dispatchOptions) (*dispatchState, error) {
|
|
| 134 |
- node := options.node |
|
| 135 |
- cmd := node.Value |
|
| 136 |
- upperCasedCmd := strings.ToUpper(cmd) |
|
| 137 |
- |
|
| 138 |
- // To ensure the user is given a decent error message if the platform |
|
| 139 |
- // on which the daemon is running does not support a builder command. |
|
| 140 |
- if err := platformSupports(strings.ToLower(cmd)); err != nil {
|
|
| 141 |
- buildsFailed.WithValues(metricsCommandNotSupportedError).Inc() |
|
| 142 |
- return nil, validationError{err}
|
|
| 131 |
+func (r *stagesBuildResults) validateIndex(i int) error {
|
|
| 132 |
+ if i == len(r.flat) {
|
|
| 133 |
+ return errors.New("refers to current build stage")
|
|
| 143 | 134 |
} |
| 144 |
- |
|
| 145 |
- msg := bytes.NewBufferString(fmt.Sprintf("Step %s : %s%s",
|
|
| 146 |
- options.stepMsg, upperCasedCmd, formatFlags(node.Flags))) |
|
| 147 |
- |
|
| 148 |
- args := []string{}
|
|
| 149 |
- ast := node |
|
| 150 |
- if cmd == command.Onbuild {
|
|
| 151 |
- var err error |
|
| 152 |
- ast, args, err = handleOnBuildNode(node, msg) |
|
| 153 |
- if err != nil {
|
|
| 154 |
- return nil, validationError{err}
|
|
| 155 |
- } |
|
| 135 |
+ if i < 0 || i > len(r.flat) {
|
|
| 136 |
+ return errors.New("index out of bounds")
|
|
| 156 | 137 |
} |
| 138 |
+ return nil |
|
| 139 |
+} |
|
| 157 | 140 |
|
| 158 |
- runConfigEnv := options.state.runConfig.Env |
|
| 159 |
- envs := append(runConfigEnv, b.buildArgs.FilterAllowed(runConfigEnv)...) |
|
| 160 |
- processFunc := createProcessWordFunc(options.shlex, cmd, envs) |
|
| 161 |
- words, err := getDispatchArgsFromNode(ast, processFunc, msg) |
|
| 141 |
+func (r *stagesBuildResults) get(nameOrIndex string) (*container.Config, error) {
|
|
| 142 |
+ if c, ok := r.getByName(nameOrIndex); ok {
|
|
| 143 |
+ return c, nil |
|
| 144 |
+ } |
|
| 145 |
+ ix, err := strconv.ParseInt(nameOrIndex, 10, 0) |
|
| 162 | 146 |
if err != nil {
|
| 163 |
- buildsFailed.WithValues(metricsErrorProcessingCommandsError).Inc() |
|
| 164 |
- return nil, validationError{err}
|
|
| 147 |
+ return nil, nil |
|
| 165 | 148 |
} |
| 166 |
- args = append(args, words...) |
|
| 149 |
+ if err := r.validateIndex(int(ix)); err != nil {
|
|
| 150 |
+ return nil, err |
|
| 151 |
+ } |
|
| 152 |
+ return r.flat[ix], nil |
|
| 153 |
+} |
|
| 167 | 154 |
|
| 168 |
- fmt.Fprintln(b.Stdout, msg.String()) |
|
| 155 |
+func (r *stagesBuildResults) checkStageNameAvailable(name string) error {
|
|
| 156 |
+ if name != "" {
|
|
| 157 |
+ if _, ok := r.getByName(name); ok {
|
|
| 158 |
+ return errors.Errorf("%s stage name already used", name)
|
|
| 159 |
+ } |
|
| 160 |
+ } |
|
| 161 |
+ return nil |
|
| 162 |
+} |
|
| 169 | 163 |
|
| 170 |
- f, ok := evaluateTable[cmd] |
|
| 171 |
- if !ok {
|
|
| 172 |
- buildsFailed.WithValues(metricsUnknownInstructionError).Inc() |
|
| 173 |
- return nil, validationError{errors.Errorf("unknown instruction: %s", upperCasedCmd)}
|
|
| 164 |
+func (r *stagesBuildResults) commitStage(name string, config *container.Config) error {
|
|
| 165 |
+ if name != "" {
|
|
| 166 |
+ if _, ok := r.getByName(name); ok {
|
|
| 167 |
+ return errors.Errorf("%s stage name already used", name)
|
|
| 168 |
+ } |
|
| 169 |
+ r.indexed[strings.ToLower(name)] = config |
|
| 174 | 170 |
} |
| 175 |
- options.state.updateRunConfig() |
|
| 176 |
- err = f(newDispatchRequestFromOptions(options, b, args)) |
|
| 177 |
- return options.state, err |
|
| 171 |
+ r.flat = append(r.flat, config) |
|
| 172 |
+ return nil |
|
| 173 |
+} |
|
| 174 |
+ |
|
| 175 |
+func commitStage(state *dispatchState, stages *stagesBuildResults) error {
|
|
| 176 |
+ return stages.commitStage(state.stageName, state.runConfig) |
|
| 178 | 177 |
} |
| 179 | 178 |
|
| 180 |
-type dispatchOptions struct {
|
|
| 179 |
+type dispatchRequest struct {
|
|
| 181 | 180 |
state *dispatchState |
| 182 |
- stepMsg string |
|
| 183 |
- node *parser.Node |
|
| 184 | 181 |
shlex *ShellLex |
| 182 |
+ builder *Builder |
|
| 185 | 183 |
source builder.Source |
| 184 |
+ stages *stagesBuildResults |
|
| 186 | 185 |
} |
| 187 | 186 |
|
| 188 |
-// dispatchState is a data object which is modified by dispatchers |
|
| 189 |
-type dispatchState struct {
|
|
| 190 |
- runConfig *container.Config |
|
| 191 |
- maintainer string |
|
| 192 |
- cmdSet bool |
|
| 193 |
- imageID string |
|
| 194 |
- baseImage builder.Image |
|
| 195 |
- stageName string |
|
| 196 |
-} |
|
| 197 |
- |
|
| 198 |
-func newDispatchState() *dispatchState {
|
|
| 199 |
- return &dispatchState{runConfig: &container.Config{}}
|
|
| 187 |
+func newDispatchRequest(builder *Builder, escapeToken rune, source builder.Source, buildArgs *buildArgs, stages *stagesBuildResults) dispatchRequest {
|
|
| 188 |
+ return dispatchRequest{
|
|
| 189 |
+ state: newDispatchState(buildArgs), |
|
| 190 |
+ shlex: NewShellLex(escapeToken), |
|
| 191 |
+ builder: builder, |
|
| 192 |
+ source: source, |
|
| 193 |
+ stages: stages, |
|
| 194 |
+ } |
|
| 200 | 195 |
} |
| 201 | 196 |
|
| 202 | 197 |
func (s *dispatchState) updateRunConfig() {
|
| ... | ... |
@@ -220,12 +215,14 @@ func (s *dispatchState) beginStage(stageName string, image builder.Image) {
|
| 220 | 220 |
s.imageID = image.ImageID() |
| 221 | 221 |
|
| 222 | 222 |
if image.RunConfig() != nil {
|
| 223 |
- s.runConfig = image.RunConfig() |
|
| 223 |
+ s.runConfig = copyRunConfig(image.RunConfig()) // copy avoids referencing the same instance when 2 stages have the same base |
|
| 224 | 224 |
} else {
|
| 225 | 225 |
s.runConfig = &container.Config{}
|
| 226 | 226 |
} |
| 227 | 227 |
s.baseImage = image |
| 228 | 228 |
s.setDefaultPath() |
| 229 |
+ s.runConfig.OpenStdin = false |
|
| 230 |
+ s.runConfig.StdinOnce = false |
|
| 229 | 231 |
} |
| 230 | 232 |
|
| 231 | 233 |
// Add the default PATH to runConfig.ENV if one exists for the platform and there |
| ... | ... |
@@ -244,84 +241,3 @@ func (s *dispatchState) setDefaultPath() {
|
| 244 | 244 |
s.runConfig.Env = append(s.runConfig.Env, "PATH="+system.DefaultPathEnv(platform)) |
| 245 | 245 |
} |
| 246 | 246 |
} |
| 247 |
- |
|
| 248 |
-func handleOnBuildNode(ast *parser.Node, msg *bytes.Buffer) (*parser.Node, []string, error) {
|
|
| 249 |
- if ast.Next == nil {
|
|
| 250 |
- return nil, nil, validationError{errors.New("ONBUILD requires at least one argument")}
|
|
| 251 |
- } |
|
| 252 |
- ast = ast.Next.Children[0] |
|
| 253 |
- msg.WriteString(" " + ast.Value + formatFlags(ast.Flags))
|
|
| 254 |
- return ast, []string{ast.Value}, nil
|
|
| 255 |
-} |
|
| 256 |
- |
|
| 257 |
-func formatFlags(flags []string) string {
|
|
| 258 |
- if len(flags) > 0 {
|
|
| 259 |
- return " " + strings.Join(flags, " ") |
|
| 260 |
- } |
|
| 261 |
- return "" |
|
| 262 |
-} |
|
| 263 |
- |
|
| 264 |
-func getDispatchArgsFromNode(ast *parser.Node, processFunc processWordFunc, msg *bytes.Buffer) ([]string, error) {
|
|
| 265 |
- args := []string{}
|
|
| 266 |
- for i := 0; ast.Next != nil; i++ {
|
|
| 267 |
- ast = ast.Next |
|
| 268 |
- words, err := processFunc(ast.Value) |
|
| 269 |
- if err != nil {
|
|
| 270 |
- return nil, err |
|
| 271 |
- } |
|
| 272 |
- args = append(args, words...) |
|
| 273 |
- msg.WriteString(" " + ast.Value)
|
|
| 274 |
- } |
|
| 275 |
- return args, nil |
|
| 276 |
-} |
|
| 277 |
- |
|
| 278 |
-type processWordFunc func(string) ([]string, error) |
|
| 279 |
- |
|
| 280 |
-func createProcessWordFunc(shlex *ShellLex, cmd string, envs []string) processWordFunc {
|
|
| 281 |
- switch {
|
|
| 282 |
- case !replaceEnvAllowed[cmd]: |
|
| 283 |
- return func(word string) ([]string, error) {
|
|
| 284 |
- return []string{word}, nil
|
|
| 285 |
- } |
|
| 286 |
- case allowWordExpansion[cmd]: |
|
| 287 |
- return func(word string) ([]string, error) {
|
|
| 288 |
- return shlex.ProcessWords(word, envs) |
|
| 289 |
- } |
|
| 290 |
- default: |
|
| 291 |
- return func(word string) ([]string, error) {
|
|
| 292 |
- word, err := shlex.ProcessWord(word, envs) |
|
| 293 |
- return []string{word}, err
|
|
| 294 |
- } |
|
| 295 |
- } |
|
| 296 |
-} |
|
| 297 |
- |
|
| 298 |
-// checkDispatch does a simple check for syntax errors of the Dockerfile. |
|
| 299 |
-// Because some of the instructions can only be validated through runtime, |
|
| 300 |
-// arg, env, etc., this syntax check will not be complete and could not replace |
|
| 301 |
-// the runtime check. Instead, this function is only a helper that allows |
|
| 302 |
-// user to find out the obvious error in Dockerfile earlier on. |
|
| 303 |
-func checkDispatch(ast *parser.Node) error {
|
|
| 304 |
- cmd := ast.Value |
|
| 305 |
- upperCasedCmd := strings.ToUpper(cmd) |
|
| 306 |
- |
|
| 307 |
- // To ensure the user is given a decent error message if the platform |
|
| 308 |
- // on which the daemon is running does not support a builder command. |
|
| 309 |
- if err := platformSupports(strings.ToLower(cmd)); err != nil {
|
|
| 310 |
- return err |
|
| 311 |
- } |
|
| 312 |
- |
|
| 313 |
- // The instruction itself is ONBUILD, we will make sure it follows with at |
|
| 314 |
- // least one argument |
|
| 315 |
- if upperCasedCmd == "ONBUILD" {
|
|
| 316 |
- if ast.Next == nil {
|
|
| 317 |
- buildsFailed.WithValues(metricsMissingOnbuildArgumentsError).Inc() |
|
| 318 |
- return errors.New("ONBUILD requires at least one argument")
|
|
| 319 |
- } |
|
| 320 |
- } |
|
| 321 |
- |
|
| 322 |
- if _, ok := evaluateTable[cmd]; ok {
|
|
| 323 |
- return nil |
|
| 324 |
- } |
|
| 325 |
- buildsFailed.WithValues(metricsUnknownInstructionError).Inc() |
|
| 326 |
- return errors.Errorf("unknown instruction: %s", upperCasedCmd)
|
|
| 327 |
-} |
| ... | ... |
@@ -1,13 +1,9 @@ |
| 1 | 1 |
package dockerfile |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "io/ioutil" |
|
| 5 |
- "strings" |
|
| 6 | 4 |
"testing" |
| 7 | 5 |
|
| 8 |
- "github.com/docker/docker/api/types" |
|
| 9 |
- "github.com/docker/docker/api/types/container" |
|
| 10 |
- "github.com/docker/docker/builder/dockerfile/parser" |
|
| 6 |
+ "github.com/docker/docker/builder/dockerfile/instructions" |
|
| 11 | 7 |
"github.com/docker/docker/builder/remotecontext" |
| 12 | 8 |
"github.com/docker/docker/internal/testutil" |
| 13 | 9 |
"github.com/docker/docker/pkg/archive" |
| ... | ... |
@@ -15,8 +11,9 @@ import ( |
| 15 | 15 |
) |
| 16 | 16 |
|
| 17 | 17 |
type dispatchTestCase struct {
|
| 18 |
- name, dockerfile, expectedError string |
|
| 19 |
- files map[string]string |
|
| 18 |
+ name, expectedError string |
|
| 19 |
+ cmd instructions.Command |
|
| 20 |
+ files map[string]string |
|
| 20 | 21 |
} |
| 21 | 22 |
|
| 22 | 23 |
func init() {
|
| ... | ... |
@@ -24,108 +21,73 @@ func init() {
|
| 24 | 24 |
} |
| 25 | 25 |
|
| 26 | 26 |
func initDispatchTestCases() []dispatchTestCase {
|
| 27 |
- dispatchTestCases := []dispatchTestCase{{
|
|
| 28 |
- name: "copyEmptyWhitespace", |
|
| 29 |
- dockerfile: `COPY |
|
| 30 |
- quux \ |
|
| 31 |
- bar`, |
|
| 32 |
- expectedError: "COPY requires at least two arguments", |
|
| 33 |
- }, |
|
| 34 |
- {
|
|
| 35 |
- name: "ONBUILD forbidden FROM", |
|
| 36 |
- dockerfile: "ONBUILD FROM scratch", |
|
| 37 |
- expectedError: "FROM isn't allowed as an ONBUILD trigger", |
|
| 38 |
- files: nil, |
|
| 39 |
- }, |
|
| 40 |
- {
|
|
| 41 |
- name: "ONBUILD forbidden MAINTAINER", |
|
| 42 |
- dockerfile: "ONBUILD MAINTAINER docker.io", |
|
| 43 |
- expectedError: "MAINTAINER isn't allowed as an ONBUILD trigger", |
|
| 44 |
- files: nil, |
|
| 45 |
- }, |
|
| 46 |
- {
|
|
| 47 |
- name: "ARG two arguments", |
|
| 48 |
- dockerfile: "ARG foo bar", |
|
| 49 |
- expectedError: "ARG requires exactly one argument", |
|
| 50 |
- files: nil, |
|
| 51 |
- }, |
|
| 52 |
- {
|
|
| 53 |
- name: "MAINTAINER unknown flag", |
|
| 54 |
- dockerfile: "MAINTAINER --boo joe@example.com", |
|
| 55 |
- expectedError: "Unknown flag: boo", |
|
| 56 |
- files: nil, |
|
| 57 |
- }, |
|
| 58 |
- {
|
|
| 59 |
- name: "ADD multiple files to file", |
|
| 60 |
- dockerfile: "ADD file1.txt file2.txt test", |
|
| 61 |
- expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /", |
|
| 62 |
- files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
|
|
| 63 |
- }, |
|
| 64 |
- {
|
|
| 65 |
- name: "JSON ADD multiple files to file", |
|
| 66 |
- dockerfile: `ADD ["file1.txt", "file2.txt", "test"]`, |
|
| 67 |
- expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /", |
|
| 68 |
- files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
|
|
| 69 |
- }, |
|
| 70 |
- {
|
|
| 71 |
- name: "Wildcard ADD multiple files to file", |
|
| 72 |
- dockerfile: "ADD file*.txt test", |
|
| 27 |
+ dispatchTestCases := []dispatchTestCase{
|
|
| 28 |
+ {
|
|
| 29 |
+ name: "ADD multiple files to file", |
|
| 30 |
+ cmd: &instructions.AddCommand{SourcesAndDest: instructions.SourcesAndDest{
|
|
| 31 |
+ "file1.txt", |
|
| 32 |
+ "file2.txt", |
|
| 33 |
+ "test", |
|
| 34 |
+ }}, |
|
| 73 | 35 |
expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /", |
| 74 | 36 |
files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
|
| 75 | 37 |
}, |
| 76 | 38 |
{
|
| 77 |
- name: "Wildcard JSON ADD multiple files to file", |
|
| 78 |
- dockerfile: `ADD ["file*.txt", "test"]`, |
|
| 39 |
+ name: "Wildcard ADD multiple files to file", |
|
| 40 |
+ cmd: &instructions.AddCommand{SourcesAndDest: instructions.SourcesAndDest{
|
|
| 41 |
+ "file*.txt", |
|
| 42 |
+ "test", |
|
| 43 |
+ }}, |
|
| 79 | 44 |
expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /", |
| 80 | 45 |
files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
|
| 81 | 46 |
}, |
| 82 | 47 |
{
|
| 83 |
- name: "COPY multiple files to file", |
|
| 84 |
- dockerfile: "COPY file1.txt file2.txt test", |
|
| 85 |
- expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /", |
|
| 86 |
- files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
|
|
| 87 |
- }, |
|
| 88 |
- {
|
|
| 89 |
- name: "JSON COPY multiple files to file", |
|
| 90 |
- dockerfile: `COPY ["file1.txt", "file2.txt", "test"]`, |
|
| 48 |
+ name: "COPY multiple files to file", |
|
| 49 |
+ cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{
|
|
| 50 |
+ "file1.txt", |
|
| 51 |
+ "file2.txt", |
|
| 52 |
+ "test", |
|
| 53 |
+ }}, |
|
| 91 | 54 |
expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /", |
| 92 | 55 |
files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
|
| 93 | 56 |
}, |
| 94 | 57 |
{
|
| 95 |
- name: "ADD multiple files to file with whitespace", |
|
| 96 |
- dockerfile: `ADD [ "test file1.txt", "test file2.txt", "test" ]`, |
|
| 58 |
+ name: "ADD multiple files to file with whitespace", |
|
| 59 |
+ cmd: &instructions.AddCommand{SourcesAndDest: instructions.SourcesAndDest{
|
|
| 60 |
+ "test file1.txt", |
|
| 61 |
+ "test file2.txt", |
|
| 62 |
+ "test", |
|
| 63 |
+ }}, |
|
| 97 | 64 |
expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /", |
| 98 | 65 |
files: map[string]string{"test file1.txt": "test1", "test file2.txt": "test2"},
|
| 99 | 66 |
}, |
| 100 | 67 |
{
|
| 101 |
- name: "COPY multiple files to file with whitespace", |
|
| 102 |
- dockerfile: `COPY [ "test file1.txt", "test file2.txt", "test" ]`, |
|
| 68 |
+ name: "COPY multiple files to file with whitespace", |
|
| 69 |
+ cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{
|
|
| 70 |
+ "test file1.txt", |
|
| 71 |
+ "test file2.txt", |
|
| 72 |
+ "test", |
|
| 73 |
+ }}, |
|
| 103 | 74 |
expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /", |
| 104 | 75 |
files: map[string]string{"test file1.txt": "test1", "test file2.txt": "test2"},
|
| 105 | 76 |
}, |
| 106 | 77 |
{
|
| 107 |
- name: "COPY wildcard no files", |
|
| 108 |
- dockerfile: `COPY file*.txt /tmp/`, |
|
| 78 |
+ name: "COPY wildcard no files", |
|
| 79 |
+ cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{
|
|
| 80 |
+ "file*.txt", |
|
| 81 |
+ "/tmp/", |
|
| 82 |
+ }}, |
|
| 109 | 83 |
expectedError: "COPY failed: no source files were specified", |
| 110 | 84 |
files: nil, |
| 111 | 85 |
}, |
| 112 | 86 |
{
|
| 113 |
- name: "COPY url", |
|
| 114 |
- dockerfile: `COPY https://index.docker.io/robots.txt /`, |
|
| 87 |
+ name: "COPY url", |
|
| 88 |
+ cmd: &instructions.CopyCommand{SourcesAndDest: instructions.SourcesAndDest{
|
|
| 89 |
+ "https://index.docker.io/robots.txt", |
|
| 90 |
+ "/", |
|
| 91 |
+ }}, |
|
| 115 | 92 |
expectedError: "source can't be a URL for COPY", |
| 116 | 93 |
files: nil, |
| 117 |
- }, |
|
| 118 |
- {
|
|
| 119 |
- name: "Chaining ONBUILD", |
|
| 120 |
- dockerfile: `ONBUILD ONBUILD RUN touch foobar`, |
|
| 121 |
- expectedError: "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed", |
|
| 122 |
- files: nil, |
|
| 123 |
- }, |
|
| 124 |
- {
|
|
| 125 |
- name: "Invalid instruction", |
|
| 126 |
- dockerfile: `foo bar`, |
|
| 127 |
- expectedError: "unknown instruction: FOO", |
|
| 128 |
- files: nil, |
|
| 129 | 94 |
}} |
| 130 | 95 |
|
| 131 | 96 |
return dispatchTestCases |
| ... | ... |
@@ -171,33 +133,8 @@ func executeTestCase(t *testing.T, testCase dispatchTestCase) {
|
| 171 | 171 |
} |
| 172 | 172 |
}() |
| 173 | 173 |
|
| 174 |
- r := strings.NewReader(testCase.dockerfile) |
|
| 175 |
- result, err := parser.Parse(r) |
|
| 176 |
- |
|
| 177 |
- if err != nil {
|
|
| 178 |
- t.Fatalf("Error when parsing Dockerfile: %s", err)
|
|
| 179 |
- } |
|
| 180 |
- |
|
| 181 |
- options := &types.ImageBuildOptions{
|
|
| 182 |
- BuildArgs: make(map[string]*string), |
|
| 183 |
- } |
|
| 184 |
- |
|
| 185 |
- b := &Builder{
|
|
| 186 |
- options: options, |
|
| 187 |
- Stdout: ioutil.Discard, |
|
| 188 |
- buildArgs: newBuildArgs(options.BuildArgs), |
|
| 189 |
- } |
|
| 190 |
- |
|
| 191 |
- shlex := NewShellLex(parser.DefaultEscapeToken) |
|
| 192 |
- n := result.AST |
|
| 193 |
- state := &dispatchState{runConfig: &container.Config{}}
|
|
| 194 |
- opts := dispatchOptions{
|
|
| 195 |
- state: state, |
|
| 196 |
- stepMsg: formatStep(0, len(n.Children)), |
|
| 197 |
- node: n.Children[0], |
|
| 198 |
- shlex: shlex, |
|
| 199 |
- source: context, |
|
| 200 |
- } |
|
| 201 |
- _, err = b.dispatch(opts) |
|
| 174 |
+ b := newBuilderWithMockBackend() |
|
| 175 |
+ sb := newDispatchRequest(b, '`', context, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) |
|
| 176 |
+ err = dispatch(sb, testCase.cmd) |
|
| 202 | 177 |
testutil.ErrorContains(t, err, testCase.expectedError) |
| 203 | 178 |
} |
| 204 | 179 |
deleted file mode 100644 |
| ... | ... |
@@ -1,9 +0,0 @@ |
| 1 |
-// +build !windows |
|
| 2 |
- |
|
| 3 |
-package dockerfile |
|
| 4 |
- |
|
| 5 |
-// platformSupports is a short-term function to give users a quality error |
|
| 6 |
-// message if a Dockerfile uses a command not supported on the platform. |
|
| 7 |
-func platformSupports(command string) error {
|
|
| 8 |
- return nil |
|
| 9 |
-} |
| 10 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,13 +0,0 @@ |
| 1 |
-package dockerfile |
|
| 2 |
- |
|
| 3 |
-import "fmt" |
|
| 4 |
- |
|
| 5 |
-// platformSupports is gives users a quality error message if a Dockerfile uses |
|
| 6 |
-// a command not supported on the platform. |
|
| 7 |
-func platformSupports(command string) error {
|
|
| 8 |
- switch command {
|
|
| 9 |
- case "stopsignal": |
|
| 10 |
- return fmt.Errorf("The daemon on this platform does not support the command '%s'", command)
|
|
| 11 |
- } |
|
| 12 |
- return nil |
|
| 13 |
-} |
| ... | ... |
@@ -1,9 +1,6 @@ |
| 1 | 1 |
package dockerfile |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "strconv" |
|
| 5 |
- "strings" |
|
| 6 |
- |
|
| 7 | 4 |
"github.com/docker/docker/api/types/backend" |
| 8 | 5 |
"github.com/docker/docker/builder" |
| 9 | 6 |
"github.com/docker/docker/builder/remotecontext" |
| ... | ... |
@@ -13,79 +10,6 @@ import ( |
| 13 | 13 |
"golang.org/x/net/context" |
| 14 | 14 |
) |
| 15 | 15 |
|
| 16 |
-type buildStage struct {
|
|
| 17 |
- id string |
|
| 18 |
-} |
|
| 19 |
- |
|
| 20 |
-func newBuildStage(imageID string) *buildStage {
|
|
| 21 |
- return &buildStage{id: imageID}
|
|
| 22 |
-} |
|
| 23 |
- |
|
| 24 |
-func (b *buildStage) ImageID() string {
|
|
| 25 |
- return b.id |
|
| 26 |
-} |
|
| 27 |
- |
|
| 28 |
-func (b *buildStage) update(imageID string) {
|
|
| 29 |
- b.id = imageID |
|
| 30 |
-} |
|
| 31 |
- |
|
| 32 |
-// buildStages tracks each stage of a build so they can be retrieved by index |
|
| 33 |
-// or by name. |
|
| 34 |
-type buildStages struct {
|
|
| 35 |
- sequence []*buildStage |
|
| 36 |
- byName map[string]*buildStage |
|
| 37 |
-} |
|
| 38 |
- |
|
| 39 |
-func newBuildStages() *buildStages {
|
|
| 40 |
- return &buildStages{byName: make(map[string]*buildStage)}
|
|
| 41 |
-} |
|
| 42 |
- |
|
| 43 |
-func (s *buildStages) getByName(name string) (*buildStage, bool) {
|
|
| 44 |
- stage, ok := s.byName[strings.ToLower(name)] |
|
| 45 |
- return stage, ok |
|
| 46 |
-} |
|
| 47 |
- |
|
| 48 |
-func (s *buildStages) get(indexOrName string) (*buildStage, error) {
|
|
| 49 |
- index, err := strconv.Atoi(indexOrName) |
|
| 50 |
- if err == nil {
|
|
| 51 |
- if err := s.validateIndex(index); err != nil {
|
|
| 52 |
- return nil, err |
|
| 53 |
- } |
|
| 54 |
- return s.sequence[index], nil |
|
| 55 |
- } |
|
| 56 |
- if im, ok := s.byName[strings.ToLower(indexOrName)]; ok {
|
|
| 57 |
- return im, nil |
|
| 58 |
- } |
|
| 59 |
- return nil, nil |
|
| 60 |
-} |
|
| 61 |
- |
|
| 62 |
-func (s *buildStages) validateIndex(i int) error {
|
|
| 63 |
- if i < 0 || i >= len(s.sequence)-1 {
|
|
| 64 |
- if i == len(s.sequence)-1 {
|
|
| 65 |
- return errors.New("refers to current build stage")
|
|
| 66 |
- } |
|
| 67 |
- return errors.New("index out of bounds")
|
|
| 68 |
- } |
|
| 69 |
- return nil |
|
| 70 |
-} |
|
| 71 |
- |
|
| 72 |
-func (s *buildStages) add(name string, image builder.Image) error {
|
|
| 73 |
- stage := newBuildStage(image.ImageID()) |
|
| 74 |
- name = strings.ToLower(name) |
|
| 75 |
- if len(name) > 0 {
|
|
| 76 |
- if _, ok := s.byName[name]; ok {
|
|
| 77 |
- return errors.Errorf("duplicate name %s", name)
|
|
| 78 |
- } |
|
| 79 |
- s.byName[name] = stage |
|
| 80 |
- } |
|
| 81 |
- s.sequence = append(s.sequence, stage) |
|
| 82 |
- return nil |
|
| 83 |
-} |
|
| 84 |
- |
|
| 85 |
-func (s *buildStages) update(imageID string) {
|
|
| 86 |
- s.sequence[len(s.sequence)-1].update(imageID) |
|
| 87 |
-} |
|
| 88 |
- |
|
| 89 | 16 |
type getAndMountFunc func(string, bool) (builder.Image, builder.ReleaseableLayer, error) |
| 90 | 17 |
|
| 91 | 18 |
// imageSources mounts images and provides a cache for mounted images. It tracks |
| 92 | 19 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,183 @@ |
| 0 |
+package instructions |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "strings" |
|
| 5 |
+) |
|
| 6 |
+ |
|
| 7 |
+// FlagType is the type of the build flag |
|
| 8 |
+type FlagType int |
|
| 9 |
+ |
|
| 10 |
+const ( |
|
| 11 |
+ boolType FlagType = iota |
|
| 12 |
+ stringType |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+// BFlags contains all flags information for the builder |
|
| 16 |
+type BFlags struct {
|
|
| 17 |
+ Args []string // actual flags/args from cmd line |
|
| 18 |
+ flags map[string]*Flag |
|
| 19 |
+ used map[string]*Flag |
|
| 20 |
+ Err error |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+// Flag contains all information for a flag |
|
| 24 |
+type Flag struct {
|
|
| 25 |
+ bf *BFlags |
|
| 26 |
+ name string |
|
| 27 |
+ flagType FlagType |
|
| 28 |
+ Value string |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// NewBFlags returns the new BFlags struct |
|
| 32 |
+func NewBFlags() *BFlags {
|
|
| 33 |
+ return &BFlags{
|
|
| 34 |
+ flags: make(map[string]*Flag), |
|
| 35 |
+ used: make(map[string]*Flag), |
|
| 36 |
+ } |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+// NewBFlagsWithArgs returns the new BFlags struct with Args set to args |
|
| 40 |
+func NewBFlagsWithArgs(args []string) *BFlags {
|
|
| 41 |
+ flags := NewBFlags() |
|
| 42 |
+ flags.Args = args |
|
| 43 |
+ return flags |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+// AddBool adds a bool flag to BFlags |
|
| 47 |
+// Note, any error will be generated when Parse() is called (see Parse). |
|
| 48 |
+func (bf *BFlags) AddBool(name string, def bool) *Flag {
|
|
| 49 |
+ flag := bf.addFlag(name, boolType) |
|
| 50 |
+ if flag == nil {
|
|
| 51 |
+ return nil |
|
| 52 |
+ } |
|
| 53 |
+ if def {
|
|
| 54 |
+ flag.Value = "true" |
|
| 55 |
+ } else {
|
|
| 56 |
+ flag.Value = "false" |
|
| 57 |
+ } |
|
| 58 |
+ return flag |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+// AddString adds a string flag to BFlags |
|
| 62 |
+// Note, any error will be generated when Parse() is called (see Parse). |
|
| 63 |
+func (bf *BFlags) AddString(name string, def string) *Flag {
|
|
| 64 |
+ flag := bf.addFlag(name, stringType) |
|
| 65 |
+ if flag == nil {
|
|
| 66 |
+ return nil |
|
| 67 |
+ } |
|
| 68 |
+ flag.Value = def |
|
| 69 |
+ return flag |
|
| 70 |
+} |
|
| 71 |
+ |
|
| 72 |
+// addFlag is a generic func used by the other AddXXX() func |
|
| 73 |
+// to add a new flag to the BFlags struct. |
|
| 74 |
+// Note, any error will be generated when Parse() is called (see Parse). |
|
| 75 |
+func (bf *BFlags) addFlag(name string, flagType FlagType) *Flag {
|
|
| 76 |
+ if _, ok := bf.flags[name]; ok {
|
|
| 77 |
+ bf.Err = fmt.Errorf("Duplicate flag defined: %s", name)
|
|
| 78 |
+ return nil |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ newFlag := &Flag{
|
|
| 82 |
+ bf: bf, |
|
| 83 |
+ name: name, |
|
| 84 |
+ flagType: flagType, |
|
| 85 |
+ } |
|
| 86 |
+ bf.flags[name] = newFlag |
|
| 87 |
+ |
|
| 88 |
+ return newFlag |
|
| 89 |
+} |
|
| 90 |
+ |
|
| 91 |
+// IsUsed checks if the flag is used |
|
| 92 |
+func (fl *Flag) IsUsed() bool {
|
|
| 93 |
+ if _, ok := fl.bf.used[fl.name]; ok {
|
|
| 94 |
+ return true |
|
| 95 |
+ } |
|
| 96 |
+ return false |
|
| 97 |
+} |
|
| 98 |
+ |
|
| 99 |
+// IsTrue checks if a bool flag is true |
|
| 100 |
+func (fl *Flag) IsTrue() bool {
|
|
| 101 |
+ if fl.flagType != boolType {
|
|
| 102 |
+ // Should never get here |
|
| 103 |
+ panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name))
|
|
| 104 |
+ } |
|
| 105 |
+ return fl.Value == "true" |
|
| 106 |
+} |
|
| 107 |
+ |
|
| 108 |
+// Parse parses and checks if the BFlags is valid. |
|
| 109 |
+// Any error noticed during the AddXXX() funcs will be generated/returned |
|
| 110 |
+// here. We do this because an error during AddXXX() is more like a |
|
| 111 |
+// compile time error so it doesn't matter too much when we stop our |
|
| 112 |
+// processing as long as we do stop it, so this allows the code |
|
| 113 |
+// around AddXXX() to be just: |
|
| 114 |
+// defFlag := AddString("description", "")
|
|
| 115 |
+// w/o needing to add an if-statement around each one. |
|
| 116 |
+func (bf *BFlags) Parse() error {
|
|
| 117 |
+ // If there was an error while defining the possible flags |
|
| 118 |
+ // go ahead and bubble it back up here since we didn't do it |
|
| 119 |
+ // earlier in the processing |
|
| 120 |
+ if bf.Err != nil {
|
|
| 121 |
+ return fmt.Errorf("Error setting up flags: %s", bf.Err)
|
|
| 122 |
+ } |
|
| 123 |
+ |
|
| 124 |
+ for _, arg := range bf.Args {
|
|
| 125 |
+ if !strings.HasPrefix(arg, "--") {
|
|
| 126 |
+ return fmt.Errorf("Arg should start with -- : %s", arg)
|
|
| 127 |
+ } |
|
| 128 |
+ |
|
| 129 |
+ if arg == "--" {
|
|
| 130 |
+ return nil |
|
| 131 |
+ } |
|
| 132 |
+ |
|
| 133 |
+ arg = arg[2:] |
|
| 134 |
+ value := "" |
|
| 135 |
+ |
|
| 136 |
+ index := strings.Index(arg, "=") |
|
| 137 |
+ if index >= 0 {
|
|
| 138 |
+ value = arg[index+1:] |
|
| 139 |
+ arg = arg[:index] |
|
| 140 |
+ } |
|
| 141 |
+ |
|
| 142 |
+ flag, ok := bf.flags[arg] |
|
| 143 |
+ if !ok {
|
|
| 144 |
+ return fmt.Errorf("Unknown flag: %s", arg)
|
|
| 145 |
+ } |
|
| 146 |
+ |
|
| 147 |
+ if _, ok = bf.used[arg]; ok {
|
|
| 148 |
+ return fmt.Errorf("Duplicate flag specified: %s", arg)
|
|
| 149 |
+ } |
|
| 150 |
+ |
|
| 151 |
+ bf.used[arg] = flag |
|
| 152 |
+ |
|
| 153 |
+ switch flag.flagType {
|
|
| 154 |
+ case boolType: |
|
| 155 |
+ // value == "" is only ok if no "=" was specified |
|
| 156 |
+ if index >= 0 && value == "" {
|
|
| 157 |
+ return fmt.Errorf("Missing a value on flag: %s", arg)
|
|
| 158 |
+ } |
|
| 159 |
+ |
|
| 160 |
+ lower := strings.ToLower(value) |
|
| 161 |
+ if lower == "" {
|
|
| 162 |
+ flag.Value = "true" |
|
| 163 |
+ } else if lower == "true" || lower == "false" {
|
|
| 164 |
+ flag.Value = lower |
|
| 165 |
+ } else {
|
|
| 166 |
+ return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value)
|
|
| 167 |
+ } |
|
| 168 |
+ |
|
| 169 |
+ case stringType: |
|
| 170 |
+ if index < 0 {
|
|
| 171 |
+ return fmt.Errorf("Missing a value on flag: %s", arg)
|
|
| 172 |
+ } |
|
| 173 |
+ flag.Value = value |
|
| 174 |
+ |
|
| 175 |
+ default: |
|
| 176 |
+ panic("No idea what kind of flag we have! Should never get here!")
|
|
| 177 |
+ } |
|
| 178 |
+ |
|
| 179 |
+ } |
|
| 180 |
+ |
|
| 181 |
+ return nil |
|
| 182 |
+} |
| 0 | 183 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,187 @@ |
| 0 |
+package instructions |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+) |
|
| 5 |
+ |
|
| 6 |
+func TestBuilderFlags(t *testing.T) {
|
|
| 7 |
+ var expected string |
|
| 8 |
+ var err error |
|
| 9 |
+ |
|
| 10 |
+ // --- |
|
| 11 |
+ |
|
| 12 |
+ bf := NewBFlags() |
|
| 13 |
+ bf.Args = []string{}
|
|
| 14 |
+ if err := bf.Parse(); err != nil {
|
|
| 15 |
+ t.Fatalf("Test1 of %q was supposed to work: %s", bf.Args, err)
|
|
| 16 |
+ } |
|
| 17 |
+ |
|
| 18 |
+ // --- |
|
| 19 |
+ |
|
| 20 |
+ bf = NewBFlags() |
|
| 21 |
+ bf.Args = []string{"--"}
|
|
| 22 |
+ if err := bf.Parse(); err != nil {
|
|
| 23 |
+ t.Fatalf("Test2 of %q was supposed to work: %s", bf.Args, err)
|
|
| 24 |
+ } |
|
| 25 |
+ |
|
| 26 |
+ // --- |
|
| 27 |
+ |
|
| 28 |
+ bf = NewBFlags() |
|
| 29 |
+ flStr1 := bf.AddString("str1", "")
|
|
| 30 |
+ flBool1 := bf.AddBool("bool1", false)
|
|
| 31 |
+ bf.Args = []string{}
|
|
| 32 |
+ if err = bf.Parse(); err != nil {
|
|
| 33 |
+ t.Fatalf("Test3 of %q was supposed to work: %s", bf.Args, err)
|
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ if flStr1.IsUsed() {
|
|
| 37 |
+ t.Fatal("Test3 - str1 was not used!")
|
|
| 38 |
+ } |
|
| 39 |
+ if flBool1.IsUsed() {
|
|
| 40 |
+ t.Fatal("Test3 - bool1 was not used!")
|
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ // --- |
|
| 44 |
+ |
|
| 45 |
+ bf = NewBFlags() |
|
| 46 |
+ flStr1 = bf.AddString("str1", "HI")
|
|
| 47 |
+ flBool1 = bf.AddBool("bool1", false)
|
|
| 48 |
+ bf.Args = []string{}
|
|
| 49 |
+ |
|
| 50 |
+ if err = bf.Parse(); err != nil {
|
|
| 51 |
+ t.Fatalf("Test4 of %q was supposed to work: %s", bf.Args, err)
|
|
| 52 |
+ } |
|
| 53 |
+ |
|
| 54 |
+ if flStr1.Value != "HI" {
|
|
| 55 |
+ t.Fatal("Str1 was supposed to default to: HI")
|
|
| 56 |
+ } |
|
| 57 |
+ if flBool1.IsTrue() {
|
|
| 58 |
+ t.Fatal("Bool1 was supposed to default to: false")
|
|
| 59 |
+ } |
|
| 60 |
+ if flStr1.IsUsed() {
|
|
| 61 |
+ t.Fatal("Str1 was not used!")
|
|
| 62 |
+ } |
|
| 63 |
+ if flBool1.IsUsed() {
|
|
| 64 |
+ t.Fatal("Bool1 was not used!")
|
|
| 65 |
+ } |
|
| 66 |
+ |
|
| 67 |
+ // --- |
|
| 68 |
+ |
|
| 69 |
+ bf = NewBFlags() |
|
| 70 |
+ flStr1 = bf.AddString("str1", "HI")
|
|
| 71 |
+ bf.Args = []string{"--str1"}
|
|
| 72 |
+ |
|
| 73 |
+ if err = bf.Parse(); err == nil {
|
|
| 74 |
+ t.Fatalf("Test %q was supposed to fail", bf.Args)
|
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ // --- |
|
| 78 |
+ |
|
| 79 |
+ bf = NewBFlags() |
|
| 80 |
+ flStr1 = bf.AddString("str1", "HI")
|
|
| 81 |
+ bf.Args = []string{"--str1="}
|
|
| 82 |
+ |
|
| 83 |
+ if err = bf.Parse(); err != nil {
|
|
| 84 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 85 |
+ } |
|
| 86 |
+ |
|
| 87 |
+ expected = "" |
|
| 88 |
+ if flStr1.Value != expected {
|
|
| 89 |
+ t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected)
|
|
| 90 |
+ } |
|
| 91 |
+ |
|
| 92 |
+ // --- |
|
| 93 |
+ |
|
| 94 |
+ bf = NewBFlags() |
|
| 95 |
+ flStr1 = bf.AddString("str1", "HI")
|
|
| 96 |
+ bf.Args = []string{"--str1=BYE"}
|
|
| 97 |
+ |
|
| 98 |
+ if err = bf.Parse(); err != nil {
|
|
| 99 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ expected = "BYE" |
|
| 103 |
+ if flStr1.Value != expected {
|
|
| 104 |
+ t.Fatalf("Str1 (%q) should be: %q", flStr1.Value, expected)
|
|
| 105 |
+ } |
|
| 106 |
+ |
|
| 107 |
+ // --- |
|
| 108 |
+ |
|
| 109 |
+ bf = NewBFlags() |
|
| 110 |
+ flBool1 = bf.AddBool("bool1", false)
|
|
| 111 |
+ bf.Args = []string{"--bool1"}
|
|
| 112 |
+ |
|
| 113 |
+ if err = bf.Parse(); err != nil {
|
|
| 114 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ if !flBool1.IsTrue() {
|
|
| 118 |
+ t.Fatal("Test-b1 Bool1 was supposed to be true")
|
|
| 119 |
+ } |
|
| 120 |
+ |
|
| 121 |
+ // --- |
|
| 122 |
+ |
|
| 123 |
+ bf = NewBFlags() |
|
| 124 |
+ flBool1 = bf.AddBool("bool1", false)
|
|
| 125 |
+ bf.Args = []string{"--bool1=true"}
|
|
| 126 |
+ |
|
| 127 |
+ if err = bf.Parse(); err != nil {
|
|
| 128 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 129 |
+ } |
|
| 130 |
+ |
|
| 131 |
+ if !flBool1.IsTrue() {
|
|
| 132 |
+ t.Fatal("Test-b2 Bool1 was supposed to be true")
|
|
| 133 |
+ } |
|
| 134 |
+ |
|
| 135 |
+ // --- |
|
| 136 |
+ |
|
| 137 |
+ bf = NewBFlags() |
|
| 138 |
+ flBool1 = bf.AddBool("bool1", false)
|
|
| 139 |
+ bf.Args = []string{"--bool1=false"}
|
|
| 140 |
+ |
|
| 141 |
+ if err = bf.Parse(); err != nil {
|
|
| 142 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 143 |
+ } |
|
| 144 |
+ |
|
| 145 |
+ if flBool1.IsTrue() {
|
|
| 146 |
+ t.Fatal("Test-b3 Bool1 was supposed to be false")
|
|
| 147 |
+ } |
|
| 148 |
+ |
|
| 149 |
+ // --- |
|
| 150 |
+ |
|
| 151 |
+ bf = NewBFlags() |
|
| 152 |
+ flBool1 = bf.AddBool("bool1", false)
|
|
| 153 |
+ bf.Args = []string{"--bool1=false1"}
|
|
| 154 |
+ |
|
| 155 |
+ if err = bf.Parse(); err == nil {
|
|
| 156 |
+ t.Fatalf("Test %q was supposed to fail", bf.Args)
|
|
| 157 |
+ } |
|
| 158 |
+ |
|
| 159 |
+ // --- |
|
| 160 |
+ |
|
| 161 |
+ bf = NewBFlags() |
|
| 162 |
+ flBool1 = bf.AddBool("bool1", false)
|
|
| 163 |
+ bf.Args = []string{"--bool2"}
|
|
| 164 |
+ |
|
| 165 |
+ if err = bf.Parse(); err == nil {
|
|
| 166 |
+ t.Fatalf("Test %q was supposed to fail", bf.Args)
|
|
| 167 |
+ } |
|
| 168 |
+ |
|
| 169 |
+ // --- |
|
| 170 |
+ |
|
| 171 |
+ bf = NewBFlags() |
|
| 172 |
+ flStr1 = bf.AddString("str1", "HI")
|
|
| 173 |
+ flBool1 = bf.AddBool("bool1", false)
|
|
| 174 |
+ bf.Args = []string{"--bool1", "--str1=BYE"}
|
|
| 175 |
+ |
|
| 176 |
+ if err = bf.Parse(); err != nil {
|
|
| 177 |
+ t.Fatalf("Test %q was supposed to work: %s", bf.Args, err)
|
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ if flStr1.Value != "BYE" {
|
|
| 181 |
+ t.Fatalf("Test %s, str1 should be BYE", bf.Args)
|
|
| 182 |
+ } |
|
| 183 |
+ if !flBool1.IsTrue() {
|
|
| 184 |
+ t.Fatalf("Test %s, bool1 should be true", bf.Args)
|
|
| 185 |
+ } |
|
| 186 |
+} |
| 0 | 187 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,396 @@ |
| 0 |
+package instructions |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "errors" |
|
| 4 |
+ |
|
| 5 |
+ "strings" |
|
| 6 |
+ |
|
| 7 |
+ "github.com/docker/docker/api/types/container" |
|
| 8 |
+ "github.com/docker/docker/api/types/strslice" |
|
| 9 |
+) |
|
| 10 |
+ |
|
| 11 |
+// KeyValuePair represent an arbitrary named value (usefull in slice insted of map[string] string to preserve ordering) |
|
| 12 |
+type KeyValuePair struct {
|
|
| 13 |
+ Key string |
|
| 14 |
+ Value string |
|
| 15 |
+} |
|
| 16 |
+ |
|
| 17 |
+func (kvp *KeyValuePair) String() string {
|
|
| 18 |
+ return kvp.Key + "=" + kvp.Value |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+// Command is implemented by every command present in a dockerfile |
|
| 22 |
+type Command interface {
|
|
| 23 |
+ Name() string |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+// KeyValuePairs is a slice of KeyValuePair |
|
| 27 |
+type KeyValuePairs []KeyValuePair |
|
| 28 |
+ |
|
| 29 |
+// withNameAndCode is the base of every command in a Dockerfile (String() returns its source code) |
|
| 30 |
+type withNameAndCode struct {
|
|
| 31 |
+ code string |
|
| 32 |
+ name string |
|
| 33 |
+} |
|
| 34 |
+ |
|
| 35 |
+func (c *withNameAndCode) String() string {
|
|
| 36 |
+ return c.code |
|
| 37 |
+} |
|
| 38 |
+ |
|
| 39 |
+// Name of the command |
|
| 40 |
+func (c *withNameAndCode) Name() string {
|
|
| 41 |
+ return c.name |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+func newWithNameAndCode(req parseRequest) withNameAndCode {
|
|
| 45 |
+ return withNameAndCode{code: strings.TrimSpace(req.original), name: req.command}
|
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+// SingleWordExpander is a provider for variable expansion where 1 word => 1 output |
|
| 49 |
+type SingleWordExpander func(word string) (string, error) |
|
| 50 |
+ |
|
| 51 |
+// SupportsSingleWordExpansion interface marks a command as supporting variable expansion |
|
| 52 |
+type SupportsSingleWordExpansion interface {
|
|
| 53 |
+ Expand(expander SingleWordExpander) error |
|
| 54 |
+} |
|
| 55 |
+ |
|
| 56 |
+// PlatformSpecific adds platform checks to a command |
|
| 57 |
+type PlatformSpecific interface {
|
|
| 58 |
+ CheckPlatform(platform string) error |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+func expandKvp(kvp KeyValuePair, expander SingleWordExpander) (KeyValuePair, error) {
|
|
| 62 |
+ key, err := expander(kvp.Key) |
|
| 63 |
+ if err != nil {
|
|
| 64 |
+ return KeyValuePair{}, err
|
|
| 65 |
+ } |
|
| 66 |
+ value, err := expander(kvp.Value) |
|
| 67 |
+ if err != nil {
|
|
| 68 |
+ return KeyValuePair{}, err
|
|
| 69 |
+ } |
|
| 70 |
+ return KeyValuePair{Key: key, Value: value}, nil
|
|
| 71 |
+} |
|
| 72 |
+func expandKvpsInPlace(kvps KeyValuePairs, expander SingleWordExpander) error {
|
|
| 73 |
+ for i, kvp := range kvps {
|
|
| 74 |
+ newKvp, err := expandKvp(kvp, expander) |
|
| 75 |
+ if err != nil {
|
|
| 76 |
+ return err |
|
| 77 |
+ } |
|
| 78 |
+ kvps[i] = newKvp |
|
| 79 |
+ } |
|
| 80 |
+ return nil |
|
| 81 |
+} |
|
| 82 |
+ |
|
| 83 |
+func expandSliceInPlace(values []string, expander SingleWordExpander) error {
|
|
| 84 |
+ for i, v := range values {
|
|
| 85 |
+ newValue, err := expander(v) |
|
| 86 |
+ if err != nil {
|
|
| 87 |
+ return err |
|
| 88 |
+ } |
|
| 89 |
+ values[i] = newValue |
|
| 90 |
+ } |
|
| 91 |
+ return nil |
|
| 92 |
+} |
|
| 93 |
+ |
|
| 94 |
+// EnvCommand : ENV key1 value1 [keyN valueN...] |
|
| 95 |
+type EnvCommand struct {
|
|
| 96 |
+ withNameAndCode |
|
| 97 |
+ Env KeyValuePairs // kvp slice instead of map to preserve ordering |
|
| 98 |
+} |
|
| 99 |
+ |
|
| 100 |
+// Expand variables |
|
| 101 |
+func (c *EnvCommand) Expand(expander SingleWordExpander) error {
|
|
| 102 |
+ return expandKvpsInPlace(c.Env, expander) |
|
| 103 |
+} |
|
| 104 |
+ |
|
| 105 |
+// MaintainerCommand : MAINTAINER maintainer_name |
|
| 106 |
+type MaintainerCommand struct {
|
|
| 107 |
+ withNameAndCode |
|
| 108 |
+ Maintainer string |
|
| 109 |
+} |
|
| 110 |
+ |
|
| 111 |
+// LabelCommand : LABEL some json data describing the image |
|
| 112 |
+// |
|
| 113 |
+// Sets the Label variable foo to bar, |
|
| 114 |
+// |
|
| 115 |
+type LabelCommand struct {
|
|
| 116 |
+ withNameAndCode |
|
| 117 |
+ Labels KeyValuePairs // kvp slice instead of map to preserve ordering |
|
| 118 |
+} |
|
| 119 |
+ |
|
| 120 |
+// Expand variables |
|
| 121 |
+func (c *LabelCommand) Expand(expander SingleWordExpander) error {
|
|
| 122 |
+ return expandKvpsInPlace(c.Labels, expander) |
|
| 123 |
+} |
|
| 124 |
+ |
|
| 125 |
+// SourcesAndDest represent a list of source files and a destination |
|
| 126 |
+type SourcesAndDest []string |
|
| 127 |
+ |
|
| 128 |
+// Sources list the source paths |
|
| 129 |
+func (s SourcesAndDest) Sources() []string {
|
|
| 130 |
+ res := make([]string, len(s)-1) |
|
| 131 |
+ copy(res, s[:len(s)-1]) |
|
| 132 |
+ return res |
|
| 133 |
+} |
|
| 134 |
+ |
|
| 135 |
+// Dest path of the operation |
|
| 136 |
+func (s SourcesAndDest) Dest() string {
|
|
| 137 |
+ return s[len(s)-1] |
|
| 138 |
+} |
|
| 139 |
+ |
|
| 140 |
+// AddCommand : ADD foo /path |
|
| 141 |
+// |
|
| 142 |
+// Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling |
|
| 143 |
+// exist here. If you do not wish to have this automatic handling, use COPY. |
|
| 144 |
+// |
|
| 145 |
+type AddCommand struct {
|
|
| 146 |
+ withNameAndCode |
|
| 147 |
+ SourcesAndDest |
|
| 148 |
+ Chown string |
|
| 149 |
+} |
|
| 150 |
+ |
|
| 151 |
+// Expand variables |
|
| 152 |
+func (c *AddCommand) Expand(expander SingleWordExpander) error {
|
|
| 153 |
+ return expandSliceInPlace(c.SourcesAndDest, expander) |
|
| 154 |
+} |
|
| 155 |
+ |
|
| 156 |
+// CopyCommand : COPY foo /path |
|
| 157 |
+// |
|
| 158 |
+// Same as 'ADD' but without the tar and remote url handling. |
|
| 159 |
+// |
|
| 160 |
+type CopyCommand struct {
|
|
| 161 |
+ withNameAndCode |
|
| 162 |
+ SourcesAndDest |
|
| 163 |
+ From string |
|
| 164 |
+ Chown string |
|
| 165 |
+} |
|
| 166 |
+ |
|
| 167 |
+// Expand variables |
|
| 168 |
+func (c *CopyCommand) Expand(expander SingleWordExpander) error {
|
|
| 169 |
+ return expandSliceInPlace(c.SourcesAndDest, expander) |
|
| 170 |
+} |
|
| 171 |
+ |
|
| 172 |
+// OnbuildCommand : ONBUILD <some other command> |
|
| 173 |
+type OnbuildCommand struct {
|
|
| 174 |
+ withNameAndCode |
|
| 175 |
+ Expression string |
|
| 176 |
+} |
|
| 177 |
+ |
|
| 178 |
+// WorkdirCommand : WORKDIR /tmp |
|
| 179 |
+// |
|
| 180 |
+// Set the working directory for future RUN/CMD/etc statements. |
|
| 181 |
+// |
|
| 182 |
+type WorkdirCommand struct {
|
|
| 183 |
+ withNameAndCode |
|
| 184 |
+ Path string |
|
| 185 |
+} |
|
| 186 |
+ |
|
| 187 |
+// Expand variables |
|
| 188 |
+func (c *WorkdirCommand) Expand(expander SingleWordExpander) error {
|
|
| 189 |
+ p, err := expander(c.Path) |
|
| 190 |
+ if err != nil {
|
|
| 191 |
+ return err |
|
| 192 |
+ } |
|
| 193 |
+ c.Path = p |
|
| 194 |
+ return nil |
|
| 195 |
+} |
|
| 196 |
+ |
|
| 197 |
+// ShellDependantCmdLine represents a cmdline optionaly prepended with the shell |
|
| 198 |
+type ShellDependantCmdLine struct {
|
|
| 199 |
+ CmdLine strslice.StrSlice |
|
| 200 |
+ PrependShell bool |
|
| 201 |
+} |
|
| 202 |
+ |
|
| 203 |
+// RunCommand : RUN some command yo |
|
| 204 |
+// |
|
| 205 |
+// run a command and commit the image. Args are automatically prepended with |
|
| 206 |
+// the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under |
|
| 207 |
+// Windows, in the event there is only one argument The difference in processing: |
|
| 208 |
+// |
|
| 209 |
+// RUN echo hi # sh -c echo hi (Linux) |
|
| 210 |
+// RUN echo hi # cmd /S /C echo hi (Windows) |
|
| 211 |
+// RUN [ "echo", "hi" ] # echo hi |
|
| 212 |
+// |
|
| 213 |
+type RunCommand struct {
|
|
| 214 |
+ withNameAndCode |
|
| 215 |
+ ShellDependantCmdLine |
|
| 216 |
+} |
|
| 217 |
+ |
|
| 218 |
+// CmdCommand : CMD foo |
|
| 219 |
+// |
|
| 220 |
+// Set the default command to run in the container (which may be empty). |
|
| 221 |
+// Argument handling is the same as RUN. |
|
| 222 |
+// |
|
| 223 |
+type CmdCommand struct {
|
|
| 224 |
+ withNameAndCode |
|
| 225 |
+ ShellDependantCmdLine |
|
| 226 |
+} |
|
| 227 |
+ |
|
| 228 |
+// HealthCheckCommand : HEALTHCHECK foo |
|
| 229 |
+// |
|
| 230 |
+// Set the default healthcheck command to run in the container (which may be empty). |
|
| 231 |
+// Argument handling is the same as RUN. |
|
| 232 |
+// |
|
| 233 |
+type HealthCheckCommand struct {
|
|
| 234 |
+ withNameAndCode |
|
| 235 |
+ Health *container.HealthConfig |
|
| 236 |
+} |
|
| 237 |
+ |
|
| 238 |
+// EntrypointCommand : ENTRYPOINT /usr/sbin/nginx |
|
| 239 |
+// |
|
| 240 |
+// Set the entrypoint to /usr/sbin/nginx. Will accept the CMD as the arguments |
|
| 241 |
+// to /usr/sbin/nginx. Uses the default shell if not in JSON format. |
|
| 242 |
+// |
|
| 243 |
+// Handles command processing similar to CMD and RUN, only req.runConfig.Entrypoint |
|
| 244 |
+// is initialized at newBuilder time instead of through argument parsing. |
|
| 245 |
+// |
|
| 246 |
+type EntrypointCommand struct {
|
|
| 247 |
+ withNameAndCode |
|
| 248 |
+ ShellDependantCmdLine |
|
| 249 |
+} |
|
| 250 |
+ |
|
| 251 |
+// ExposeCommand : EXPOSE 6667/tcp 7000/tcp |
|
| 252 |
+// |
|
| 253 |
+// Expose ports for links and port mappings. This all ends up in |
|
| 254 |
+// req.runConfig.ExposedPorts for runconfig. |
|
| 255 |
+// |
|
| 256 |
+type ExposeCommand struct {
|
|
| 257 |
+ withNameAndCode |
|
| 258 |
+ Ports []string |
|
| 259 |
+} |
|
| 260 |
+ |
|
| 261 |
+// UserCommand : USER foo |
|
| 262 |
+// |
|
| 263 |
+// Set the user to 'foo' for future commands and when running the |
|
| 264 |
+// ENTRYPOINT/CMD at container run time. |
|
| 265 |
+// |
|
| 266 |
+type UserCommand struct {
|
|
| 267 |
+ withNameAndCode |
|
| 268 |
+ User string |
|
| 269 |
+} |
|
| 270 |
+ |
|
| 271 |
+// Expand variables |
|
| 272 |
+func (c *UserCommand) Expand(expander SingleWordExpander) error {
|
|
| 273 |
+ p, err := expander(c.User) |
|
| 274 |
+ if err != nil {
|
|
| 275 |
+ return err |
|
| 276 |
+ } |
|
| 277 |
+ c.User = p |
|
| 278 |
+ return nil |
|
| 279 |
+} |
|
| 280 |
+ |
|
| 281 |
+// VolumeCommand : VOLUME /foo |
|
| 282 |
+// |
|
| 283 |
+// Expose the volume /foo for use. Will also accept the JSON array form. |
|
| 284 |
+// |
|
| 285 |
+type VolumeCommand struct {
|
|
| 286 |
+ withNameAndCode |
|
| 287 |
+ Volumes []string |
|
| 288 |
+} |
|
| 289 |
+ |
|
| 290 |
+// Expand variables |
|
| 291 |
+func (c *VolumeCommand) Expand(expander SingleWordExpander) error {
|
|
| 292 |
+ return expandSliceInPlace(c.Volumes, expander) |
|
| 293 |
+} |
|
| 294 |
+ |
|
| 295 |
+// StopSignalCommand : STOPSIGNAL signal |
|
| 296 |
+// |
|
| 297 |
+// Set the signal that will be used to kill the container. |
|
| 298 |
+type StopSignalCommand struct {
|
|
| 299 |
+ withNameAndCode |
|
| 300 |
+ Signal string |
|
| 301 |
+} |
|
| 302 |
+ |
|
| 303 |
+// Expand variables |
|
| 304 |
+func (c *StopSignalCommand) Expand(expander SingleWordExpander) error {
|
|
| 305 |
+ p, err := expander(c.Signal) |
|
| 306 |
+ if err != nil {
|
|
| 307 |
+ return err |
|
| 308 |
+ } |
|
| 309 |
+ c.Signal = p |
|
| 310 |
+ return nil |
|
| 311 |
+} |
|
| 312 |
+ |
|
| 313 |
+// CheckPlatform checks that the command is supported in the target platform |
|
| 314 |
+func (c *StopSignalCommand) CheckPlatform(platform string) error {
|
|
| 315 |
+ if platform == "windows" {
|
|
| 316 |
+ return errors.New("The daemon on this platform does not support the command stopsignal")
|
|
| 317 |
+ } |
|
| 318 |
+ return nil |
|
| 319 |
+} |
|
| 320 |
+ |
|
| 321 |
+// ArgCommand : ARG name[=value] |
|
| 322 |
+// |
|
| 323 |
+// Adds the variable foo to the trusted list of variables that can be passed |
|
| 324 |
+// to builder using the --build-arg flag for expansion/substitution or passing to 'run'. |
|
| 325 |
+// Dockerfile author may optionally set a default value of this variable. |
|
| 326 |
+type ArgCommand struct {
|
|
| 327 |
+ withNameAndCode |
|
| 328 |
+ Key string |
|
| 329 |
+ Value *string |
|
| 330 |
+} |
|
| 331 |
+ |
|
| 332 |
+// Expand variables |
|
| 333 |
+func (c *ArgCommand) Expand(expander SingleWordExpander) error {
|
|
| 334 |
+ p, err := expander(c.Key) |
|
| 335 |
+ if err != nil {
|
|
| 336 |
+ return err |
|
| 337 |
+ } |
|
| 338 |
+ c.Key = p |
|
| 339 |
+ if c.Value != nil {
|
|
| 340 |
+ p, err = expander(*c.Value) |
|
| 341 |
+ if err != nil {
|
|
| 342 |
+ return err |
|
| 343 |
+ } |
|
| 344 |
+ c.Value = &p |
|
| 345 |
+ } |
|
| 346 |
+ return nil |
|
| 347 |
+} |
|
| 348 |
+ |
|
| 349 |
+// ShellCommand : SHELL powershell -command |
|
| 350 |
+// |
|
| 351 |
+// Set the non-default shell to use. |
|
| 352 |
+type ShellCommand struct {
|
|
| 353 |
+ withNameAndCode |
|
| 354 |
+ Shell strslice.StrSlice |
|
| 355 |
+} |
|
| 356 |
+ |
|
| 357 |
+// Stage represents a single stage in a multi-stage build |
|
| 358 |
+type Stage struct {
|
|
| 359 |
+ Name string |
|
| 360 |
+ Commands []Command |
|
| 361 |
+ BaseName string |
|
| 362 |
+ SourceCode string |
|
| 363 |
+} |
|
| 364 |
+ |
|
| 365 |
+// AddCommand to the stage |
|
| 366 |
+func (s *Stage) AddCommand(cmd Command) {
|
|
| 367 |
+ // todo: validate cmd type |
|
| 368 |
+ s.Commands = append(s.Commands, cmd) |
|
| 369 |
+} |
|
| 370 |
+ |
|
| 371 |
+// IsCurrentStage check if the stage name is the current stage |
|
| 372 |
+func IsCurrentStage(s []Stage, name string) bool {
|
|
| 373 |
+ if len(s) == 0 {
|
|
| 374 |
+ return false |
|
| 375 |
+ } |
|
| 376 |
+ return s[len(s)-1].Name == name |
|
| 377 |
+} |
|
| 378 |
+ |
|
| 379 |
+// CurrentStage return the last stage in a slice |
|
| 380 |
+func CurrentStage(s []Stage) (*Stage, error) {
|
|
| 381 |
+ if len(s) == 0 {
|
|
| 382 |
+ return nil, errors.New("No build stage in current context")
|
|
| 383 |
+ } |
|
| 384 |
+ return &s[len(s)-1], nil |
|
| 385 |
+} |
|
| 386 |
+ |
|
| 387 |
+// HasStage looks for the presence of a given stage name |
|
| 388 |
+func HasStage(s []Stage, name string) (int, bool) {
|
|
| 389 |
+ for i, stage := range s {
|
|
| 390 |
+ if stage.Name == name {
|
|
| 391 |
+ return i, true |
|
| 392 |
+ } |
|
| 393 |
+ } |
|
| 394 |
+ return -1, false |
|
| 395 |
+} |
| 0 | 9 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,27 @@ |
| 0 |
+package instructions |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "path/filepath" |
|
| 5 |
+ "regexp" |
|
| 6 |
+ "strings" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+func errNotJSON(command, original string) error {
|
|
| 10 |
+ // For Windows users, give a hint if it looks like it might contain |
|
| 11 |
+ // a path which hasn't been escaped such as ["c:\windows\system32\prog.exe", "-param"], |
|
| 12 |
+ // as JSON must be escaped. Unfortunate... |
|
| 13 |
+ // |
|
| 14 |
+ // Specifically looking for quote-driveletter-colon-backslash, there's no |
|
| 15 |
+ // double backslash and a [] pair. No, this is not perfect, but it doesn't |
|
| 16 |
+ // have to be. It's simply a hint to make life a little easier. |
|
| 17 |
+ extra := "" |
|
| 18 |
+ original = filepath.FromSlash(strings.ToLower(strings.Replace(strings.ToLower(original), strings.ToLower(command)+" ", "", -1))) |
|
| 19 |
+ if len(regexp.MustCompile(`"[a-z]:\\.*`).FindStringSubmatch(original)) > 0 && |
|
| 20 |
+ !strings.Contains(original, `\\`) && |
|
| 21 |
+ strings.Contains(original, "[") && |
|
| 22 |
+ strings.Contains(original, "]") {
|
|
| 23 |
+ extra = fmt.Sprintf(`. It looks like '%s' includes a file path without an escaped back-slash. JSON requires back-slashes to be escaped such as ["c:\\path\\to\\file.exe", "/parameter"]`, original) |
|
| 24 |
+ } |
|
| 25 |
+ return fmt.Errorf("%s requires the arguments to be in JSON form%s", command, extra)
|
|
| 26 |
+} |
| 0 | 27 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,635 @@ |
| 0 |
+package instructions |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "regexp" |
|
| 5 |
+ "sort" |
|
| 6 |
+ "strconv" |
|
| 7 |
+ "strings" |
|
| 8 |
+ "time" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/docker/docker/api/types/container" |
|
| 11 |
+ "github.com/docker/docker/api/types/strslice" |
|
| 12 |
+ "github.com/docker/docker/builder/dockerfile/command" |
|
| 13 |
+ "github.com/docker/docker/builder/dockerfile/parser" |
|
| 14 |
+ "github.com/pkg/errors" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+type parseRequest struct {
|
|
| 18 |
+ command string |
|
| 19 |
+ args []string |
|
| 20 |
+ attributes map[string]bool |
|
| 21 |
+ flags *BFlags |
|
| 22 |
+ original string |
|
| 23 |
+} |
|
| 24 |
+ |
|
| 25 |
+func nodeArgs(node *parser.Node) []string {
|
|
| 26 |
+ result := []string{}
|
|
| 27 |
+ for ; node.Next != nil; node = node.Next {
|
|
| 28 |
+ arg := node.Next |
|
| 29 |
+ if len(arg.Children) == 0 {
|
|
| 30 |
+ result = append(result, arg.Value) |
|
| 31 |
+ } else if len(arg.Children) == 1 {
|
|
| 32 |
+ //sub command |
|
| 33 |
+ result = append(result, arg.Children[0].Value) |
|
| 34 |
+ result = append(result, nodeArgs(arg.Children[0])...) |
|
| 35 |
+ } |
|
| 36 |
+ } |
|
| 37 |
+ return result |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+func newParseRequestFromNode(node *parser.Node) parseRequest {
|
|
| 41 |
+ return parseRequest{
|
|
| 42 |
+ command: node.Value, |
|
| 43 |
+ args: nodeArgs(node), |
|
| 44 |
+ attributes: node.Attributes, |
|
| 45 |
+ original: node.Original, |
|
| 46 |
+ flags: NewBFlagsWithArgs(node.Flags), |
|
| 47 |
+ } |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// ParseInstruction converts an AST to a typed instruction (either a command or a build stage beginning when encountering a `FROM` statement) |
|
| 51 |
+func ParseInstruction(node *parser.Node) (interface{}, error) {
|
|
| 52 |
+ req := newParseRequestFromNode(node) |
|
| 53 |
+ switch node.Value {
|
|
| 54 |
+ case command.Env: |
|
| 55 |
+ return parseEnv(req) |
|
| 56 |
+ case command.Maintainer: |
|
| 57 |
+ return parseMaintainer(req) |
|
| 58 |
+ case command.Label: |
|
| 59 |
+ return parseLabel(req) |
|
| 60 |
+ case command.Add: |
|
| 61 |
+ return parseAdd(req) |
|
| 62 |
+ case command.Copy: |
|
| 63 |
+ return parseCopy(req) |
|
| 64 |
+ case command.From: |
|
| 65 |
+ return parseFrom(req) |
|
| 66 |
+ case command.Onbuild: |
|
| 67 |
+ return parseOnBuild(req) |
|
| 68 |
+ case command.Workdir: |
|
| 69 |
+ return parseWorkdir(req) |
|
| 70 |
+ case command.Run: |
|
| 71 |
+ return parseRun(req) |
|
| 72 |
+ case command.Cmd: |
|
| 73 |
+ return parseCmd(req) |
|
| 74 |
+ case command.Healthcheck: |
|
| 75 |
+ return parseHealthcheck(req) |
|
| 76 |
+ case command.Entrypoint: |
|
| 77 |
+ return parseEntrypoint(req) |
|
| 78 |
+ case command.Expose: |
|
| 79 |
+ return parseExpose(req) |
|
| 80 |
+ case command.User: |
|
| 81 |
+ return parseUser(req) |
|
| 82 |
+ case command.Volume: |
|
| 83 |
+ return parseVolume(req) |
|
| 84 |
+ case command.StopSignal: |
|
| 85 |
+ return parseStopSignal(req) |
|
| 86 |
+ case command.Arg: |
|
| 87 |
+ return parseArg(req) |
|
| 88 |
+ case command.Shell: |
|
| 89 |
+ return parseShell(req) |
|
| 90 |
+ } |
|
| 91 |
+ |
|
| 92 |
+ return nil, &UnknownInstruction{Instruction: node.Value, Line: node.StartLine}
|
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+// ParseCommand converts an AST to a typed Command |
|
| 96 |
+func ParseCommand(node *parser.Node) (Command, error) {
|
|
| 97 |
+ s, err := ParseInstruction(node) |
|
| 98 |
+ if err != nil {
|
|
| 99 |
+ return nil, err |
|
| 100 |
+ } |
|
| 101 |
+ if c, ok := s.(Command); ok {
|
|
| 102 |
+ return c, nil |
|
| 103 |
+ } |
|
| 104 |
+ return nil, errors.Errorf("%T is not a command type", s)
|
|
| 105 |
+} |
|
| 106 |
+ |
|
| 107 |
+// UnknownInstruction represents an error occuring when a command is unresolvable |
|
| 108 |
+type UnknownInstruction struct {
|
|
| 109 |
+ Line int |
|
| 110 |
+ Instruction string |
|
| 111 |
+} |
|
| 112 |
+ |
|
| 113 |
+func (e *UnknownInstruction) Error() string {
|
|
| 114 |
+ return fmt.Sprintf("unknown instruction: %s", strings.ToUpper(e.Instruction))
|
|
| 115 |
+} |
|
| 116 |
+ |
|
| 117 |
+// IsUnknownInstruction checks if the error is an UnknownInstruction or a parseError containing an UnknownInstruction |
|
| 118 |
+func IsUnknownInstruction(err error) bool {
|
|
| 119 |
+ _, ok := err.(*UnknownInstruction) |
|
| 120 |
+ if !ok {
|
|
| 121 |
+ var pe *parseError |
|
| 122 |
+ if pe, ok = err.(*parseError); ok {
|
|
| 123 |
+ _, ok = pe.inner.(*UnknownInstruction) |
|
| 124 |
+ } |
|
| 125 |
+ } |
|
| 126 |
+ return ok |
|
| 127 |
+} |
|
| 128 |
+ |
|
| 129 |
+type parseError struct {
|
|
| 130 |
+ inner error |
|
| 131 |
+ node *parser.Node |
|
| 132 |
+} |
|
| 133 |
+ |
|
| 134 |
+func (e *parseError) Error() string {
|
|
| 135 |
+ return fmt.Sprintf("Dockerfile parse error line %d: %v", e.node.StartLine, e.inner.Error())
|
|
| 136 |
+} |
|
| 137 |
+ |
|
| 138 |
+// Parse a docker file into a collection of buildable stages |
|
| 139 |
+func Parse(ast *parser.Node) (stages []Stage, metaArgs []ArgCommand, err error) {
|
|
| 140 |
+ for _, n := range ast.Children {
|
|
| 141 |
+ cmd, err := ParseInstruction(n) |
|
| 142 |
+ if err != nil {
|
|
| 143 |
+ return nil, nil, &parseError{inner: err, node: n}
|
|
| 144 |
+ } |
|
| 145 |
+ if len(stages) == 0 {
|
|
| 146 |
+ // meta arg case |
|
| 147 |
+ if a, isArg := cmd.(*ArgCommand); isArg {
|
|
| 148 |
+ metaArgs = append(metaArgs, *a) |
|
| 149 |
+ continue |
|
| 150 |
+ } |
|
| 151 |
+ } |
|
| 152 |
+ switch c := cmd.(type) {
|
|
| 153 |
+ case *Stage: |
|
| 154 |
+ stages = append(stages, *c) |
|
| 155 |
+ case Command: |
|
| 156 |
+ stage, err := CurrentStage(stages) |
|
| 157 |
+ if err != nil {
|
|
| 158 |
+ return nil, nil, err |
|
| 159 |
+ } |
|
| 160 |
+ stage.AddCommand(c) |
|
| 161 |
+ default: |
|
| 162 |
+ return nil, nil, errors.Errorf("%T is not a command type", cmd)
|
|
| 163 |
+ } |
|
| 164 |
+ |
|
| 165 |
+ } |
|
| 166 |
+ return stages, metaArgs, nil |
|
| 167 |
+} |
|
| 168 |
+ |
|
| 169 |
+func parseKvps(args []string, cmdName string) (KeyValuePairs, error) {
|
|
| 170 |
+ if len(args) == 0 {
|
|
| 171 |
+ return nil, errAtLeastOneArgument(cmdName) |
|
| 172 |
+ } |
|
| 173 |
+ if len(args)%2 != 0 {
|
|
| 174 |
+ // should never get here, but just in case |
|
| 175 |
+ return nil, errTooManyArguments(cmdName) |
|
| 176 |
+ } |
|
| 177 |
+ var res KeyValuePairs |
|
| 178 |
+ for j := 0; j < len(args); j += 2 {
|
|
| 179 |
+ if len(args[j]) == 0 {
|
|
| 180 |
+ return nil, errBlankCommandNames(cmdName) |
|
| 181 |
+ } |
|
| 182 |
+ name := args[j] |
|
| 183 |
+ value := args[j+1] |
|
| 184 |
+ res = append(res, KeyValuePair{Key: name, Value: value})
|
|
| 185 |
+ } |
|
| 186 |
+ return res, nil |
|
| 187 |
+} |
|
| 188 |
+ |
|
| 189 |
+func parseEnv(req parseRequest) (*EnvCommand, error) {
|
|
| 190 |
+ |
|
| 191 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 192 |
+ return nil, err |
|
| 193 |
+ } |
|
| 194 |
+ envs, err := parseKvps(req.args, "ENV") |
|
| 195 |
+ if err != nil {
|
|
| 196 |
+ return nil, err |
|
| 197 |
+ } |
|
| 198 |
+ return &EnvCommand{
|
|
| 199 |
+ Env: envs, |
|
| 200 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 201 |
+ }, nil |
|
| 202 |
+} |
|
| 203 |
+ |
|
| 204 |
+func parseMaintainer(req parseRequest) (*MaintainerCommand, error) {
|
|
| 205 |
+ if len(req.args) != 1 {
|
|
| 206 |
+ return nil, errExactlyOneArgument("MAINTAINER")
|
|
| 207 |
+ } |
|
| 208 |
+ |
|
| 209 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 210 |
+ return nil, err |
|
| 211 |
+ } |
|
| 212 |
+ return &MaintainerCommand{
|
|
| 213 |
+ Maintainer: req.args[0], |
|
| 214 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 215 |
+ }, nil |
|
| 216 |
+} |
|
| 217 |
+ |
|
| 218 |
+func parseLabel(req parseRequest) (*LabelCommand, error) {
|
|
| 219 |
+ |
|
| 220 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 221 |
+ return nil, err |
|
| 222 |
+ } |
|
| 223 |
+ |
|
| 224 |
+ labels, err := parseKvps(req.args, "LABEL") |
|
| 225 |
+ if err != nil {
|
|
| 226 |
+ return nil, err |
|
| 227 |
+ } |
|
| 228 |
+ |
|
| 229 |
+ return &LabelCommand{
|
|
| 230 |
+ Labels: labels, |
|
| 231 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 232 |
+ }, nil |
|
| 233 |
+} |
|
| 234 |
+ |
|
| 235 |
+func parseAdd(req parseRequest) (*AddCommand, error) {
|
|
| 236 |
+ if len(req.args) < 2 {
|
|
| 237 |
+ return nil, errAtLeastTwoArguments("ADD")
|
|
| 238 |
+ } |
|
| 239 |
+ flChown := req.flags.AddString("chown", "")
|
|
| 240 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 241 |
+ return nil, err |
|
| 242 |
+ } |
|
| 243 |
+ return &AddCommand{
|
|
| 244 |
+ SourcesAndDest: SourcesAndDest(req.args), |
|
| 245 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 246 |
+ Chown: flChown.Value, |
|
| 247 |
+ }, nil |
|
| 248 |
+} |
|
| 249 |
+ |
|
| 250 |
+func parseCopy(req parseRequest) (*CopyCommand, error) {
|
|
| 251 |
+ if len(req.args) < 2 {
|
|
| 252 |
+ return nil, errAtLeastTwoArguments("COPY")
|
|
| 253 |
+ } |
|
| 254 |
+ flChown := req.flags.AddString("chown", "")
|
|
| 255 |
+ flFrom := req.flags.AddString("from", "")
|
|
| 256 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 257 |
+ return nil, err |
|
| 258 |
+ } |
|
| 259 |
+ return &CopyCommand{
|
|
| 260 |
+ SourcesAndDest: SourcesAndDest(req.args), |
|
| 261 |
+ From: flFrom.Value, |
|
| 262 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 263 |
+ Chown: flChown.Value, |
|
| 264 |
+ }, nil |
|
| 265 |
+} |
|
| 266 |
+ |
|
| 267 |
+func parseFrom(req parseRequest) (*Stage, error) {
|
|
| 268 |
+ stageName, err := parseBuildStageName(req.args) |
|
| 269 |
+ if err != nil {
|
|
| 270 |
+ return nil, err |
|
| 271 |
+ } |
|
| 272 |
+ |
|
| 273 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 274 |
+ return nil, err |
|
| 275 |
+ } |
|
| 276 |
+ code := strings.TrimSpace(req.original) |
|
| 277 |
+ |
|
| 278 |
+ return &Stage{
|
|
| 279 |
+ BaseName: req.args[0], |
|
| 280 |
+ Name: stageName, |
|
| 281 |
+ SourceCode: code, |
|
| 282 |
+ Commands: []Command{},
|
|
| 283 |
+ }, nil |
|
| 284 |
+ |
|
| 285 |
+} |
|
| 286 |
+ |
|
| 287 |
+func parseBuildStageName(args []string) (string, error) {
|
|
| 288 |
+ stageName := "" |
|
| 289 |
+ switch {
|
|
| 290 |
+ case len(args) == 3 && strings.EqualFold(args[1], "as"): |
|
| 291 |
+ stageName = strings.ToLower(args[2]) |
|
| 292 |
+ if ok, _ := regexp.MatchString("^[a-z][a-z0-9-_\\.]*$", stageName); !ok {
|
|
| 293 |
+ return "", errors.Errorf("invalid name for build stage: %q, name can't start with a number or contain symbols", stageName)
|
|
| 294 |
+ } |
|
| 295 |
+ case len(args) != 1: |
|
| 296 |
+ return "", errors.New("FROM requires either one or three arguments")
|
|
| 297 |
+ } |
|
| 298 |
+ |
|
| 299 |
+ return stageName, nil |
|
| 300 |
+} |
|
| 301 |
+ |
|
| 302 |
+func parseOnBuild(req parseRequest) (*OnbuildCommand, error) {
|
|
| 303 |
+ if len(req.args) == 0 {
|
|
| 304 |
+ return nil, errAtLeastOneArgument("ONBUILD")
|
|
| 305 |
+ } |
|
| 306 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 307 |
+ return nil, err |
|
| 308 |
+ } |
|
| 309 |
+ |
|
| 310 |
+ triggerInstruction := strings.ToUpper(strings.TrimSpace(req.args[0])) |
|
| 311 |
+ switch strings.ToUpper(triggerInstruction) {
|
|
| 312 |
+ case "ONBUILD": |
|
| 313 |
+ return nil, errors.New("Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed")
|
|
| 314 |
+ case "MAINTAINER", "FROM": |
|
| 315 |
+ return nil, fmt.Errorf("%s isn't allowed as an ONBUILD trigger", triggerInstruction)
|
|
| 316 |
+ } |
|
| 317 |
+ |
|
| 318 |
+ original := regexp.MustCompile(`(?i)^\s*ONBUILD\s*`).ReplaceAllString(req.original, "") |
|
| 319 |
+ return &OnbuildCommand{
|
|
| 320 |
+ Expression: original, |
|
| 321 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 322 |
+ }, nil |
|
| 323 |
+ |
|
| 324 |
+} |
|
| 325 |
+ |
|
| 326 |
+func parseWorkdir(req parseRequest) (*WorkdirCommand, error) {
|
|
| 327 |
+ if len(req.args) != 1 {
|
|
| 328 |
+ return nil, errExactlyOneArgument("WORKDIR")
|
|
| 329 |
+ } |
|
| 330 |
+ |
|
| 331 |
+ err := req.flags.Parse() |
|
| 332 |
+ if err != nil {
|
|
| 333 |
+ return nil, err |
|
| 334 |
+ } |
|
| 335 |
+ return &WorkdirCommand{
|
|
| 336 |
+ Path: req.args[0], |
|
| 337 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 338 |
+ }, nil |
|
| 339 |
+ |
|
| 340 |
+} |
|
| 341 |
+ |
|
| 342 |
+func parseShellDependentCommand(req parseRequest, emptyAsNil bool) ShellDependantCmdLine {
|
|
| 343 |
+ args := handleJSONArgs(req.args, req.attributes) |
|
| 344 |
+ cmd := strslice.StrSlice(args) |
|
| 345 |
+ if emptyAsNil && len(cmd) == 0 {
|
|
| 346 |
+ cmd = nil |
|
| 347 |
+ } |
|
| 348 |
+ return ShellDependantCmdLine{
|
|
| 349 |
+ CmdLine: cmd, |
|
| 350 |
+ PrependShell: !req.attributes["json"], |
|
| 351 |
+ } |
|
| 352 |
+} |
|
| 353 |
+ |
|
| 354 |
+func parseRun(req parseRequest) (*RunCommand, error) {
|
|
| 355 |
+ |
|
| 356 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 357 |
+ return nil, err |
|
| 358 |
+ } |
|
| 359 |
+ return &RunCommand{
|
|
| 360 |
+ ShellDependantCmdLine: parseShellDependentCommand(req, false), |
|
| 361 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 362 |
+ }, nil |
|
| 363 |
+ |
|
| 364 |
+} |
|
| 365 |
+ |
|
| 366 |
+func parseCmd(req parseRequest) (*CmdCommand, error) {
|
|
| 367 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 368 |
+ return nil, err |
|
| 369 |
+ } |
|
| 370 |
+ return &CmdCommand{
|
|
| 371 |
+ ShellDependantCmdLine: parseShellDependentCommand(req, false), |
|
| 372 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 373 |
+ }, nil |
|
| 374 |
+ |
|
| 375 |
+} |
|
| 376 |
+ |
|
| 377 |
+func parseEntrypoint(req parseRequest) (*EntrypointCommand, error) {
|
|
| 378 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 379 |
+ return nil, err |
|
| 380 |
+ } |
|
| 381 |
+ |
|
| 382 |
+ cmd := &EntrypointCommand{
|
|
| 383 |
+ ShellDependantCmdLine: parseShellDependentCommand(req, true), |
|
| 384 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 385 |
+ } |
|
| 386 |
+ |
|
| 387 |
+ return cmd, nil |
|
| 388 |
+} |
|
| 389 |
+ |
|
| 390 |
+// parseOptInterval(flag) is the duration of flag.Value, or 0 if |
|
| 391 |
+// empty. An error is reported if the value is given and less than minimum duration. |
|
| 392 |
+func parseOptInterval(f *Flag) (time.Duration, error) {
|
|
| 393 |
+ s := f.Value |
|
| 394 |
+ if s == "" {
|
|
| 395 |
+ return 0, nil |
|
| 396 |
+ } |
|
| 397 |
+ d, err := time.ParseDuration(s) |
|
| 398 |
+ if err != nil {
|
|
| 399 |
+ return 0, err |
|
| 400 |
+ } |
|
| 401 |
+ if d < container.MinimumDuration {
|
|
| 402 |
+ return 0, fmt.Errorf("Interval %#v cannot be less than %s", f.name, container.MinimumDuration)
|
|
| 403 |
+ } |
|
| 404 |
+ return d, nil |
|
| 405 |
+} |
|
| 406 |
+func parseHealthcheck(req parseRequest) (*HealthCheckCommand, error) {
|
|
| 407 |
+ if len(req.args) == 0 {
|
|
| 408 |
+ return nil, errAtLeastOneArgument("HEALTHCHECK")
|
|
| 409 |
+ } |
|
| 410 |
+ cmd := &HealthCheckCommand{
|
|
| 411 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 412 |
+ } |
|
| 413 |
+ |
|
| 414 |
+ typ := strings.ToUpper(req.args[0]) |
|
| 415 |
+ args := req.args[1:] |
|
| 416 |
+ if typ == "NONE" {
|
|
| 417 |
+ if len(args) != 0 {
|
|
| 418 |
+ return nil, errors.New("HEALTHCHECK NONE takes no arguments")
|
|
| 419 |
+ } |
|
| 420 |
+ test := strslice.StrSlice{typ}
|
|
| 421 |
+ cmd.Health = &container.HealthConfig{
|
|
| 422 |
+ Test: test, |
|
| 423 |
+ } |
|
| 424 |
+ } else {
|
|
| 425 |
+ |
|
| 426 |
+ healthcheck := container.HealthConfig{}
|
|
| 427 |
+ |
|
| 428 |
+ flInterval := req.flags.AddString("interval", "")
|
|
| 429 |
+ flTimeout := req.flags.AddString("timeout", "")
|
|
| 430 |
+ flStartPeriod := req.flags.AddString("start-period", "")
|
|
| 431 |
+ flRetries := req.flags.AddString("retries", "")
|
|
| 432 |
+ |
|
| 433 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 434 |
+ return nil, err |
|
| 435 |
+ } |
|
| 436 |
+ |
|
| 437 |
+ switch typ {
|
|
| 438 |
+ case "CMD": |
|
| 439 |
+ cmdSlice := handleJSONArgs(args, req.attributes) |
|
| 440 |
+ if len(cmdSlice) == 0 {
|
|
| 441 |
+ return nil, errors.New("Missing command after HEALTHCHECK CMD")
|
|
| 442 |
+ } |
|
| 443 |
+ |
|
| 444 |
+ if !req.attributes["json"] {
|
|
| 445 |
+ typ = "CMD-SHELL" |
|
| 446 |
+ } |
|
| 447 |
+ |
|
| 448 |
+ healthcheck.Test = strslice.StrSlice(append([]string{typ}, cmdSlice...))
|
|
| 449 |
+ default: |
|
| 450 |
+ return nil, fmt.Errorf("Unknown type %#v in HEALTHCHECK (try CMD)", typ)
|
|
| 451 |
+ } |
|
| 452 |
+ |
|
| 453 |
+ interval, err := parseOptInterval(flInterval) |
|
| 454 |
+ if err != nil {
|
|
| 455 |
+ return nil, err |
|
| 456 |
+ } |
|
| 457 |
+ healthcheck.Interval = interval |
|
| 458 |
+ |
|
| 459 |
+ timeout, err := parseOptInterval(flTimeout) |
|
| 460 |
+ if err != nil {
|
|
| 461 |
+ return nil, err |
|
| 462 |
+ } |
|
| 463 |
+ healthcheck.Timeout = timeout |
|
| 464 |
+ |
|
| 465 |
+ startPeriod, err := parseOptInterval(flStartPeriod) |
|
| 466 |
+ if err != nil {
|
|
| 467 |
+ return nil, err |
|
| 468 |
+ } |
|
| 469 |
+ healthcheck.StartPeriod = startPeriod |
|
| 470 |
+ |
|
| 471 |
+ if flRetries.Value != "" {
|
|
| 472 |
+ retries, err := strconv.ParseInt(flRetries.Value, 10, 32) |
|
| 473 |
+ if err != nil {
|
|
| 474 |
+ return nil, err |
|
| 475 |
+ } |
|
| 476 |
+ if retries < 1 {
|
|
| 477 |
+ return nil, fmt.Errorf("--retries must be at least 1 (not %d)", retries)
|
|
| 478 |
+ } |
|
| 479 |
+ healthcheck.Retries = int(retries) |
|
| 480 |
+ } else {
|
|
| 481 |
+ healthcheck.Retries = 0 |
|
| 482 |
+ } |
|
| 483 |
+ |
|
| 484 |
+ cmd.Health = &healthcheck |
|
| 485 |
+ } |
|
| 486 |
+ return cmd, nil |
|
| 487 |
+} |
|
| 488 |
+ |
|
| 489 |
+func parseExpose(req parseRequest) (*ExposeCommand, error) {
|
|
| 490 |
+ portsTab := req.args |
|
| 491 |
+ |
|
| 492 |
+ if len(req.args) == 0 {
|
|
| 493 |
+ return nil, errAtLeastOneArgument("EXPOSE")
|
|
| 494 |
+ } |
|
| 495 |
+ |
|
| 496 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 497 |
+ return nil, err |
|
| 498 |
+ } |
|
| 499 |
+ |
|
| 500 |
+ sort.Strings(portsTab) |
|
| 501 |
+ return &ExposeCommand{
|
|
| 502 |
+ Ports: portsTab, |
|
| 503 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 504 |
+ }, nil |
|
| 505 |
+} |
|
| 506 |
+ |
|
| 507 |
+func parseUser(req parseRequest) (*UserCommand, error) {
|
|
| 508 |
+ if len(req.args) != 1 {
|
|
| 509 |
+ return nil, errExactlyOneArgument("USER")
|
|
| 510 |
+ } |
|
| 511 |
+ |
|
| 512 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 513 |
+ return nil, err |
|
| 514 |
+ } |
|
| 515 |
+ return &UserCommand{
|
|
| 516 |
+ User: req.args[0], |
|
| 517 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 518 |
+ }, nil |
|
| 519 |
+} |
|
| 520 |
+ |
|
| 521 |
+func parseVolume(req parseRequest) (*VolumeCommand, error) {
|
|
| 522 |
+ if len(req.args) == 0 {
|
|
| 523 |
+ return nil, errAtLeastOneArgument("VOLUME")
|
|
| 524 |
+ } |
|
| 525 |
+ |
|
| 526 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 527 |
+ return nil, err |
|
| 528 |
+ } |
|
| 529 |
+ |
|
| 530 |
+ cmd := &VolumeCommand{
|
|
| 531 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 532 |
+ } |
|
| 533 |
+ |
|
| 534 |
+ for _, v := range req.args {
|
|
| 535 |
+ v = strings.TrimSpace(v) |
|
| 536 |
+ if v == "" {
|
|
| 537 |
+ return nil, errors.New("VOLUME specified can not be an empty string")
|
|
| 538 |
+ } |
|
| 539 |
+ cmd.Volumes = append(cmd.Volumes, v) |
|
| 540 |
+ } |
|
| 541 |
+ return cmd, nil |
|
| 542 |
+ |
|
| 543 |
+} |
|
| 544 |
+ |
|
| 545 |
+func parseStopSignal(req parseRequest) (*StopSignalCommand, error) {
|
|
| 546 |
+ if len(req.args) != 1 {
|
|
| 547 |
+ return nil, errExactlyOneArgument("STOPSIGNAL")
|
|
| 548 |
+ } |
|
| 549 |
+ sig := req.args[0] |
|
| 550 |
+ |
|
| 551 |
+ cmd := &StopSignalCommand{
|
|
| 552 |
+ Signal: sig, |
|
| 553 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 554 |
+ } |
|
| 555 |
+ return cmd, nil |
|
| 556 |
+ |
|
| 557 |
+} |
|
| 558 |
+ |
|
| 559 |
+func parseArg(req parseRequest) (*ArgCommand, error) {
|
|
| 560 |
+ if len(req.args) != 1 {
|
|
| 561 |
+ return nil, errExactlyOneArgument("ARG")
|
|
| 562 |
+ } |
|
| 563 |
+ |
|
| 564 |
+ var ( |
|
| 565 |
+ name string |
|
| 566 |
+ newValue *string |
|
| 567 |
+ ) |
|
| 568 |
+ |
|
| 569 |
+ arg := req.args[0] |
|
| 570 |
+ // 'arg' can just be a name or name-value pair. Note that this is different |
|
| 571 |
+ // from 'env' that handles the split of name and value at the parser level. |
|
| 572 |
+ // The reason for doing it differently for 'arg' is that we support just |
|
| 573 |
+ // defining an arg and not assign it a value (while 'env' always expects a |
|
| 574 |
+ // name-value pair). If possible, it will be good to harmonize the two. |
|
| 575 |
+ if strings.Contains(arg, "=") {
|
|
| 576 |
+ parts := strings.SplitN(arg, "=", 2) |
|
| 577 |
+ if len(parts[0]) == 0 {
|
|
| 578 |
+ return nil, errBlankCommandNames("ARG")
|
|
| 579 |
+ } |
|
| 580 |
+ |
|
| 581 |
+ name = parts[0] |
|
| 582 |
+ newValue = &parts[1] |
|
| 583 |
+ } else {
|
|
| 584 |
+ name = arg |
|
| 585 |
+ } |
|
| 586 |
+ |
|
| 587 |
+ return &ArgCommand{
|
|
| 588 |
+ Key: name, |
|
| 589 |
+ Value: newValue, |
|
| 590 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 591 |
+ }, nil |
|
| 592 |
+} |
|
| 593 |
+ |
|
| 594 |
+func parseShell(req parseRequest) (*ShellCommand, error) {
|
|
| 595 |
+ if err := req.flags.Parse(); err != nil {
|
|
| 596 |
+ return nil, err |
|
| 597 |
+ } |
|
| 598 |
+ shellSlice := handleJSONArgs(req.args, req.attributes) |
|
| 599 |
+ switch {
|
|
| 600 |
+ case len(shellSlice) == 0: |
|
| 601 |
+ // SHELL [] |
|
| 602 |
+ return nil, errAtLeastOneArgument("SHELL")
|
|
| 603 |
+ case req.attributes["json"]: |
|
| 604 |
+ // SHELL ["powershell", "-command"] |
|
| 605 |
+ |
|
| 606 |
+ return &ShellCommand{
|
|
| 607 |
+ Shell: strslice.StrSlice(shellSlice), |
|
| 608 |
+ withNameAndCode: newWithNameAndCode(req), |
|
| 609 |
+ }, nil |
|
| 610 |
+ default: |
|
| 611 |
+ // SHELL powershell -command - not JSON |
|
| 612 |
+ return nil, errNotJSON("SHELL", req.original)
|
|
| 613 |
+ } |
|
| 614 |
+} |
|
| 615 |
+ |
|
| 616 |
+func errAtLeastOneArgument(command string) error {
|
|
| 617 |
+ return errors.Errorf("%s requires at least one argument", command)
|
|
| 618 |
+} |
|
| 619 |
+ |
|
| 620 |
+func errExactlyOneArgument(command string) error {
|
|
| 621 |
+ return errors.Errorf("%s requires exactly one argument", command)
|
|
| 622 |
+} |
|
| 623 |
+ |
|
| 624 |
+func errAtLeastTwoArguments(command string) error {
|
|
| 625 |
+ return errors.Errorf("%s requires at least two arguments", command)
|
|
| 626 |
+} |
|
| 627 |
+ |
|
| 628 |
+func errBlankCommandNames(command string) error {
|
|
| 629 |
+ return errors.Errorf("%s names can not be blank", command)
|
|
| 630 |
+} |
|
| 631 |
+ |
|
| 632 |
+func errTooManyArguments(command string) error {
|
|
| 633 |
+ return errors.Errorf("Bad input to %s, too many arguments", command)
|
|
| 634 |
+} |
| 0 | 635 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,204 @@ |
| 0 |
+package instructions |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "strings" |
|
| 4 |
+ "testing" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/docker/docker/builder/dockerfile/command" |
|
| 7 |
+ "github.com/docker/docker/builder/dockerfile/parser" |
|
| 8 |
+ "github.com/docker/docker/internal/testutil" |
|
| 9 |
+ "github.com/stretchr/testify/assert" |
|
| 10 |
+ "github.com/stretchr/testify/require" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+func TestCommandsExactlyOneArgument(t *testing.T) {
|
|
| 14 |
+ commands := []string{
|
|
| 15 |
+ "MAINTAINER", |
|
| 16 |
+ "WORKDIR", |
|
| 17 |
+ "USER", |
|
| 18 |
+ "STOPSIGNAL", |
|
| 19 |
+ } |
|
| 20 |
+ |
|
| 21 |
+ for _, command := range commands {
|
|
| 22 |
+ ast, err := parser.Parse(strings.NewReader(command)) |
|
| 23 |
+ require.NoError(t, err) |
|
| 24 |
+ _, err = ParseInstruction(ast.AST.Children[0]) |
|
| 25 |
+ assert.EqualError(t, err, errExactlyOneArgument(command).Error()) |
|
| 26 |
+ } |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+func TestCommandsAtLeastOneArgument(t *testing.T) {
|
|
| 30 |
+ commands := []string{
|
|
| 31 |
+ "ENV", |
|
| 32 |
+ "LABEL", |
|
| 33 |
+ "ONBUILD", |
|
| 34 |
+ "HEALTHCHECK", |
|
| 35 |
+ "EXPOSE", |
|
| 36 |
+ "VOLUME", |
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ for _, command := range commands {
|
|
| 40 |
+ ast, err := parser.Parse(strings.NewReader(command)) |
|
| 41 |
+ require.NoError(t, err) |
|
| 42 |
+ _, err = ParseInstruction(ast.AST.Children[0]) |
|
| 43 |
+ assert.EqualError(t, err, errAtLeastOneArgument(command).Error()) |
|
| 44 |
+ } |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 47 |
+func TestCommandsAtLeastTwoArgument(t *testing.T) {
|
|
| 48 |
+ commands := []string{
|
|
| 49 |
+ "ADD", |
|
| 50 |
+ "COPY", |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ for _, command := range commands {
|
|
| 54 |
+ ast, err := parser.Parse(strings.NewReader(command + " arg1")) |
|
| 55 |
+ require.NoError(t, err) |
|
| 56 |
+ _, err = ParseInstruction(ast.AST.Children[0]) |
|
| 57 |
+ assert.EqualError(t, err, errAtLeastTwoArguments(command).Error()) |
|
| 58 |
+ } |
|
| 59 |
+} |
|
| 60 |
+ |
|
| 61 |
+func TestCommandsTooManyArguments(t *testing.T) {
|
|
| 62 |
+ commands := []string{
|
|
| 63 |
+ "ENV", |
|
| 64 |
+ "LABEL", |
|
| 65 |
+ } |
|
| 66 |
+ |
|
| 67 |
+ for _, command := range commands {
|
|
| 68 |
+ node := &parser.Node{
|
|
| 69 |
+ Original: command + "arg1 arg2 arg3", |
|
| 70 |
+ Value: strings.ToLower(command), |
|
| 71 |
+ Next: &parser.Node{
|
|
| 72 |
+ Value: "arg1", |
|
| 73 |
+ Next: &parser.Node{
|
|
| 74 |
+ Value: "arg2", |
|
| 75 |
+ Next: &parser.Node{
|
|
| 76 |
+ Value: "arg3", |
|
| 77 |
+ }, |
|
| 78 |
+ }, |
|
| 79 |
+ }, |
|
| 80 |
+ } |
|
| 81 |
+ _, err := ParseInstruction(node) |
|
| 82 |
+ assert.EqualError(t, err, errTooManyArguments(command).Error()) |
|
| 83 |
+ } |
|
| 84 |
+} |
|
| 85 |
+ |
|
| 86 |
+func TestCommandsBlankNames(t *testing.T) {
|
|
| 87 |
+ commands := []string{
|
|
| 88 |
+ "ENV", |
|
| 89 |
+ "LABEL", |
|
| 90 |
+ } |
|
| 91 |
+ |
|
| 92 |
+ for _, command := range commands {
|
|
| 93 |
+ node := &parser.Node{
|
|
| 94 |
+ Original: command + " =arg2", |
|
| 95 |
+ Value: strings.ToLower(command), |
|
| 96 |
+ Next: &parser.Node{
|
|
| 97 |
+ Value: "", |
|
| 98 |
+ Next: &parser.Node{
|
|
| 99 |
+ Value: "arg2", |
|
| 100 |
+ }, |
|
| 101 |
+ }, |
|
| 102 |
+ } |
|
| 103 |
+ _, err := ParseInstruction(node) |
|
| 104 |
+ assert.EqualError(t, err, errBlankCommandNames(command).Error()) |
|
| 105 |
+ } |
|
| 106 |
+} |
|
| 107 |
+ |
|
| 108 |
+func TestHealthCheckCmd(t *testing.T) {
|
|
| 109 |
+ node := &parser.Node{
|
|
| 110 |
+ Value: command.Healthcheck, |
|
| 111 |
+ Next: &parser.Node{
|
|
| 112 |
+ Value: "CMD", |
|
| 113 |
+ Next: &parser.Node{
|
|
| 114 |
+ Value: "hello", |
|
| 115 |
+ Next: &parser.Node{
|
|
| 116 |
+ Value: "world", |
|
| 117 |
+ }, |
|
| 118 |
+ }, |
|
| 119 |
+ }, |
|
| 120 |
+ } |
|
| 121 |
+ cmd, err := ParseInstruction(node) |
|
| 122 |
+ assert.NoError(t, err) |
|
| 123 |
+ hc, ok := cmd.(*HealthCheckCommand) |
|
| 124 |
+ assert.True(t, ok) |
|
| 125 |
+ expected := []string{"CMD-SHELL", "hello world"}
|
|
| 126 |
+ assert.Equal(t, expected, hc.Health.Test) |
|
| 127 |
+} |
|
| 128 |
+ |
|
| 129 |
+func TestParseOptInterval(t *testing.T) {
|
|
| 130 |
+ flInterval := &Flag{
|
|
| 131 |
+ name: "interval", |
|
| 132 |
+ flagType: stringType, |
|
| 133 |
+ Value: "50ns", |
|
| 134 |
+ } |
|
| 135 |
+ _, err := parseOptInterval(flInterval) |
|
| 136 |
+ testutil.ErrorContains(t, err, "cannot be less than 1ms") |
|
| 137 |
+ |
|
| 138 |
+ flInterval.Value = "1ms" |
|
| 139 |
+ _, err = parseOptInterval(flInterval) |
|
| 140 |
+ require.NoError(t, err) |
|
| 141 |
+} |
|
| 142 |
+ |
|
| 143 |
+func TestErrorCases(t *testing.T) {
|
|
| 144 |
+ cases := []struct {
|
|
| 145 |
+ name string |
|
| 146 |
+ dockerfile string |
|
| 147 |
+ expectedError string |
|
| 148 |
+ }{
|
|
| 149 |
+ {
|
|
| 150 |
+ name: "copyEmptyWhitespace", |
|
| 151 |
+ dockerfile: `COPY |
|
| 152 |
+ quux \ |
|
| 153 |
+ bar`, |
|
| 154 |
+ expectedError: "COPY requires at least two arguments", |
|
| 155 |
+ }, |
|
| 156 |
+ {
|
|
| 157 |
+ name: "ONBUILD forbidden FROM", |
|
| 158 |
+ dockerfile: "ONBUILD FROM scratch", |
|
| 159 |
+ expectedError: "FROM isn't allowed as an ONBUILD trigger", |
|
| 160 |
+ }, |
|
| 161 |
+ {
|
|
| 162 |
+ name: "ONBUILD forbidden MAINTAINER", |
|
| 163 |
+ dockerfile: "ONBUILD MAINTAINER docker.io", |
|
| 164 |
+ expectedError: "MAINTAINER isn't allowed as an ONBUILD trigger", |
|
| 165 |
+ }, |
|
| 166 |
+ {
|
|
| 167 |
+ name: "ARG two arguments", |
|
| 168 |
+ dockerfile: "ARG foo bar", |
|
| 169 |
+ expectedError: "ARG requires exactly one argument", |
|
| 170 |
+ }, |
|
| 171 |
+ {
|
|
| 172 |
+ name: "MAINTAINER unknown flag", |
|
| 173 |
+ dockerfile: "MAINTAINER --boo joe@example.com", |
|
| 174 |
+ expectedError: "Unknown flag: boo", |
|
| 175 |
+ }, |
|
| 176 |
+ {
|
|
| 177 |
+ name: "Chaining ONBUILD", |
|
| 178 |
+ dockerfile: `ONBUILD ONBUILD RUN touch foobar`, |
|
| 179 |
+ expectedError: "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed", |
|
| 180 |
+ }, |
|
| 181 |
+ {
|
|
| 182 |
+ name: "Invalid instruction", |
|
| 183 |
+ dockerfile: `foo bar`, |
|
| 184 |
+ expectedError: "unknown instruction: FOO", |
|
| 185 |
+ }, |
|
| 186 |
+ } |
|
| 187 |
+ for _, c := range cases {
|
|
| 188 |
+ r := strings.NewReader(c.dockerfile) |
|
| 189 |
+ ast, err := parser.Parse(r) |
|
| 190 |
+ |
|
| 191 |
+ if err != nil {
|
|
| 192 |
+ t.Fatalf("Error when parsing Dockerfile: %s", err)
|
|
| 193 |
+ } |
|
| 194 |
+ n := ast.AST.Children[0] |
|
| 195 |
+ _, err = ParseInstruction(n) |
|
| 196 |
+ if err != nil {
|
|
| 197 |
+ testutil.ErrorContains(t, err, c.expectedError) |
|
| 198 |
+ return |
|
| 199 |
+ } |
|
| 200 |
+ t.Fatalf("No error when executing test %s", c.name)
|
|
| 201 |
+ } |
|
| 202 |
+ |
|
| 203 |
+} |
| 0 | 204 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,19 @@ |
| 0 |
+package instructions |
|
| 1 |
+ |
|
| 2 |
+import "strings" |
|
| 3 |
+ |
|
| 4 |
+// handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile |
|
| 5 |
+// for exec form it returns untouched args slice |
|
| 6 |
+// for shell form it returns concatenated args as the first element of a slice |
|
| 7 |
+func handleJSONArgs(args []string, attributes map[string]bool) []string {
|
|
| 8 |
+ if len(args) == 0 {
|
|
| 9 |
+ return []string{}
|
|
| 10 |
+ } |
|
| 11 |
+ |
|
| 12 |
+ if attributes != nil && attributes["json"] {
|
|
| 13 |
+ return args |
|
| 14 |
+ } |
|
| 15 |
+ |
|
| 16 |
+ // literal string command, not an exec array |
|
| 17 |
+ return []string{strings.Join(args, " ")}
|
|
| 18 |
+} |
| 0 | 19 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,65 @@ |
| 0 |
+package instructions |
|
| 1 |
+ |
|
| 2 |
+import "testing" |
|
| 3 |
+ |
|
| 4 |
+type testCase struct {
|
|
| 5 |
+ name string |
|
| 6 |
+ args []string |
|
| 7 |
+ attributes map[string]bool |
|
| 8 |
+ expected []string |
|
| 9 |
+} |
|
| 10 |
+ |
|
| 11 |
+func initTestCases() []testCase {
|
|
| 12 |
+ testCases := []testCase{}
|
|
| 13 |
+ |
|
| 14 |
+ testCases = append(testCases, testCase{
|
|
| 15 |
+ name: "empty args", |
|
| 16 |
+ args: []string{},
|
|
| 17 |
+ attributes: make(map[string]bool), |
|
| 18 |
+ expected: []string{},
|
|
| 19 |
+ }) |
|
| 20 |
+ |
|
| 21 |
+ jsonAttributes := make(map[string]bool) |
|
| 22 |
+ jsonAttributes["json"] = true |
|
| 23 |
+ |
|
| 24 |
+ testCases = append(testCases, testCase{
|
|
| 25 |
+ name: "json attribute with one element", |
|
| 26 |
+ args: []string{"foo"},
|
|
| 27 |
+ attributes: jsonAttributes, |
|
| 28 |
+ expected: []string{"foo"},
|
|
| 29 |
+ }) |
|
| 30 |
+ |
|
| 31 |
+ testCases = append(testCases, testCase{
|
|
| 32 |
+ name: "json attribute with two elements", |
|
| 33 |
+ args: []string{"foo", "bar"},
|
|
| 34 |
+ attributes: jsonAttributes, |
|
| 35 |
+ expected: []string{"foo", "bar"},
|
|
| 36 |
+ }) |
|
| 37 |
+ |
|
| 38 |
+ testCases = append(testCases, testCase{
|
|
| 39 |
+ name: "no attributes", |
|
| 40 |
+ args: []string{"foo", "bar"},
|
|
| 41 |
+ attributes: nil, |
|
| 42 |
+ expected: []string{"foo bar"},
|
|
| 43 |
+ }) |
|
| 44 |
+ |
|
| 45 |
+ return testCases |
|
| 46 |
+} |
|
| 47 |
+ |
|
| 48 |
+func TestHandleJSONArgs(t *testing.T) {
|
|
| 49 |
+ testCases := initTestCases() |
|
| 50 |
+ |
|
| 51 |
+ for _, test := range testCases {
|
|
| 52 |
+ arguments := handleJSONArgs(test.args, test.attributes) |
|
| 53 |
+ |
|
| 54 |
+ if len(arguments) != len(test.expected) {
|
|
| 55 |
+ t.Fatalf("In test \"%s\": length of returned slice is incorrect. Expected: %d, got: %d", test.name, len(test.expected), len(arguments))
|
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ for i := range test.expected {
|
|
| 59 |
+ if arguments[i] != test.expected[i] {
|
|
| 60 |
+ t.Fatalf("In test \"%s\": element as position %d is incorrect. Expected: %s, got: %s", test.name, i, test.expected[i], arguments[i])
|
|
| 61 |
+ } |
|
| 62 |
+ } |
|
| 63 |
+ } |
|
| 64 |
+} |
| ... | ... |
@@ -124,7 +124,6 @@ func (b *Builder) commitContainer(dispatchState *dispatchState, id string, conta |
| 124 | 124 |
} |
| 125 | 125 |
|
| 126 | 126 |
dispatchState.imageID = imageID |
| 127 |
- b.buildStages.update(imageID) |
|
| 128 | 127 |
return nil |
| 129 | 128 |
} |
| 130 | 129 |
|
| ... | ... |
@@ -164,7 +163,6 @@ func (b *Builder) exportImage(state *dispatchState, imageMount *imageMount, runC |
| 164 | 164 |
|
| 165 | 165 |
state.imageID = exportedImage.ImageID() |
| 166 | 166 |
b.imageSources.Add(newImageMount(exportedImage, newLayer)) |
| 167 |
- b.buildStages.update(state.imageID) |
|
| 168 | 167 |
return nil |
| 169 | 168 |
} |
| 170 | 169 |
|
| ... | ... |
@@ -460,7 +458,6 @@ func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container. |
| 460 | 460 |
fmt.Fprint(b.Stdout, " ---> Using cache\n") |
| 461 | 461 |
|
| 462 | 462 |
dispatchState.imageID = cachedID |
| 463 |
- b.buildStages.update(dispatchState.imageID) |
|
| 464 | 463 |
return true, nil |
| 465 | 464 |
} |
| 466 | 465 |
|
| 467 | 466 |
deleted file mode 100644 |
| ... | ... |
@@ -1,19 +0,0 @@ |
| 1 |
-package dockerfile |
|
| 2 |
- |
|
| 3 |
-import "strings" |
|
| 4 |
- |
|
| 5 |
-// handleJSONArgs parses command passed to CMD, ENTRYPOINT, RUN and SHELL instruction in Dockerfile |
|
| 6 |
-// for exec form it returns untouched args slice |
|
| 7 |
-// for shell form it returns concatenated args as the first element of a slice |
|
| 8 |
-func handleJSONArgs(args []string, attributes map[string]bool) []string {
|
|
| 9 |
- if len(args) == 0 {
|
|
| 10 |
- return []string{}
|
|
| 11 |
- } |
|
| 12 |
- |
|
| 13 |
- if attributes != nil && attributes["json"] {
|
|
| 14 |
- return args |
|
| 15 |
- } |
|
| 16 |
- |
|
| 17 |
- // literal string command, not an exec array |
|
| 18 |
- return []string{strings.Join(args, " ")}
|
|
| 19 |
-} |
| 20 | 1 |
deleted file mode 100644 |
| ... | ... |
@@ -1,65 +0,0 @@ |
| 1 |
-package dockerfile |
|
| 2 |
- |
|
| 3 |
-import "testing" |
|
| 4 |
- |
|
| 5 |
-type testCase struct {
|
|
| 6 |
- name string |
|
| 7 |
- args []string |
|
| 8 |
- attributes map[string]bool |
|
| 9 |
- expected []string |
|
| 10 |
-} |
|
| 11 |
- |
|
| 12 |
-func initTestCases() []testCase {
|
|
| 13 |
- testCases := []testCase{}
|
|
| 14 |
- |
|
| 15 |
- testCases = append(testCases, testCase{
|
|
| 16 |
- name: "empty args", |
|
| 17 |
- args: []string{},
|
|
| 18 |
- attributes: make(map[string]bool), |
|
| 19 |
- expected: []string{},
|
|
| 20 |
- }) |
|
| 21 |
- |
|
| 22 |
- jsonAttributes := make(map[string]bool) |
|
| 23 |
- jsonAttributes["json"] = true |
|
| 24 |
- |
|
| 25 |
- testCases = append(testCases, testCase{
|
|
| 26 |
- name: "json attribute with one element", |
|
| 27 |
- args: []string{"foo"},
|
|
| 28 |
- attributes: jsonAttributes, |
|
| 29 |
- expected: []string{"foo"},
|
|
| 30 |
- }) |
|
| 31 |
- |
|
| 32 |
- testCases = append(testCases, testCase{
|
|
| 33 |
- name: "json attribute with two elements", |
|
| 34 |
- args: []string{"foo", "bar"},
|
|
| 35 |
- attributes: jsonAttributes, |
|
| 36 |
- expected: []string{"foo", "bar"},
|
|
| 37 |
- }) |
|
| 38 |
- |
|
| 39 |
- testCases = append(testCases, testCase{
|
|
| 40 |
- name: "no attributes", |
|
| 41 |
- args: []string{"foo", "bar"},
|
|
| 42 |
- attributes: nil, |
|
| 43 |
- expected: []string{"foo bar"},
|
|
| 44 |
- }) |
|
| 45 |
- |
|
| 46 |
- return testCases |
|
| 47 |
-} |
|
| 48 |
- |
|
| 49 |
-func TestHandleJSONArgs(t *testing.T) {
|
|
| 50 |
- testCases := initTestCases() |
|
| 51 |
- |
|
| 52 |
- for _, test := range testCases {
|
|
| 53 |
- arguments := handleJSONArgs(test.args, test.attributes) |
|
| 54 |
- |
|
| 55 |
- if len(arguments) != len(test.expected) {
|
|
| 56 |
- t.Fatalf("In test \"%s\": length of returned slice is incorrect. Expected: %d, got: %d", test.name, len(test.expected), len(arguments))
|
|
| 57 |
- } |
|
| 58 |
- |
|
| 59 |
- for i := range test.expected {
|
|
| 60 |
- if arguments[i] != test.expected[i] {
|
|
| 61 |
- t.Fatalf("In test \"%s\": element as position %d is incorrect. Expected: %s, got: %s", test.name, i, test.expected[i], arguments[i])
|
|
| 62 |
- } |
|
| 63 |
- } |
|
| 64 |
- } |
|
| 65 |
-} |
| ... | ... |
@@ -438,6 +438,82 @@ func (s *DockerSuite) TestBuildChownOnCopy(c *check.C) {
|
| 438 | 438 |
assert.Contains(c, string(out), "Successfully built") |
| 439 | 439 |
} |
| 440 | 440 |
|
| 441 |
+func (s *DockerSuite) TestBuildCopyCacheOnFileChange(c *check.C) {
|
|
| 442 |
+ |
|
| 443 |
+ dockerfile := `FROM busybox |
|
| 444 |
+COPY file /file` |
|
| 445 |
+ |
|
| 446 |
+ ctx1 := fakecontext.New(c, "", |
|
| 447 |
+ fakecontext.WithDockerfile(dockerfile), |
|
| 448 |
+ fakecontext.WithFile("file", "foo"))
|
|
| 449 |
+ ctx2 := fakecontext.New(c, "", |
|
| 450 |
+ fakecontext.WithDockerfile(dockerfile), |
|
| 451 |
+ fakecontext.WithFile("file", "bar"))
|
|
| 452 |
+ |
|
| 453 |
+ var build = func(ctx *fakecontext.Fake) string {
|
|
| 454 |
+ res, body, err := request.Post("/build",
|
|
| 455 |
+ request.RawContent(ctx.AsTarReader(c)), |
|
| 456 |
+ request.ContentType("application/x-tar"))
|
|
| 457 |
+ |
|
| 458 |
+ require.NoError(c, err) |
|
| 459 |
+ assert.Equal(c, http.StatusOK, res.StatusCode) |
|
| 460 |
+ |
|
| 461 |
+ out, err := request.ReadBody(body) |
|
| 462 |
+ |
|
| 463 |
+ ids := getImageIDsFromBuild(c, out) |
|
| 464 |
+ return ids[len(ids)-1] |
|
| 465 |
+ } |
|
| 466 |
+ |
|
| 467 |
+ id1 := build(ctx1) |
|
| 468 |
+ id2 := build(ctx1) |
|
| 469 |
+ id3 := build(ctx2) |
|
| 470 |
+ |
|
| 471 |
+ if id1 != id2 {
|
|
| 472 |
+ c.Fatal("didn't use the cache")
|
|
| 473 |
+ } |
|
| 474 |
+ if id1 == id3 {
|
|
| 475 |
+ c.Fatal("COPY With different source file should not share same cache")
|
|
| 476 |
+ } |
|
| 477 |
+} |
|
| 478 |
+ |
|
| 479 |
+func (s *DockerSuite) TestBuildAddCacheOnFileChange(c *check.C) {
|
|
| 480 |
+ |
|
| 481 |
+ dockerfile := `FROM busybox |
|
| 482 |
+ADD file /file` |
|
| 483 |
+ |
|
| 484 |
+ ctx1 := fakecontext.New(c, "", |
|
| 485 |
+ fakecontext.WithDockerfile(dockerfile), |
|
| 486 |
+ fakecontext.WithFile("file", "foo"))
|
|
| 487 |
+ ctx2 := fakecontext.New(c, "", |
|
| 488 |
+ fakecontext.WithDockerfile(dockerfile), |
|
| 489 |
+ fakecontext.WithFile("file", "bar"))
|
|
| 490 |
+ |
|
| 491 |
+ var build = func(ctx *fakecontext.Fake) string {
|
|
| 492 |
+ res, body, err := request.Post("/build",
|
|
| 493 |
+ request.RawContent(ctx.AsTarReader(c)), |
|
| 494 |
+ request.ContentType("application/x-tar"))
|
|
| 495 |
+ |
|
| 496 |
+ require.NoError(c, err) |
|
| 497 |
+ assert.Equal(c, http.StatusOK, res.StatusCode) |
|
| 498 |
+ |
|
| 499 |
+ out, err := request.ReadBody(body) |
|
| 500 |
+ |
|
| 501 |
+ ids := getImageIDsFromBuild(c, out) |
|
| 502 |
+ return ids[len(ids)-1] |
|
| 503 |
+ } |
|
| 504 |
+ |
|
| 505 |
+ id1 := build(ctx1) |
|
| 506 |
+ id2 := build(ctx1) |
|
| 507 |
+ id3 := build(ctx2) |
|
| 508 |
+ |
|
| 509 |
+ if id1 != id2 {
|
|
| 510 |
+ c.Fatal("didn't use the cache")
|
|
| 511 |
+ } |
|
| 512 |
+ if id1 == id3 {
|
|
| 513 |
+ c.Fatal("COPY With different source file should not share same cache")
|
|
| 514 |
+ } |
|
| 515 |
+} |
|
| 516 |
+ |
|
| 441 | 517 |
func (s *DockerSuite) TestBuildWithSession(c *check.C) {
|
| 442 | 518 |
testRequires(c, ExperimentalDaemon) |
| 443 | 519 |
|
| ... | ... |
@@ -1173,12 +1173,13 @@ func (s *DockerSuite) TestBuildForceRm(c *check.C) {
|
| 1173 | 1173 |
containerCountBefore := getContainerCount(c) |
| 1174 | 1174 |
name := "testbuildforcerm" |
| 1175 | 1175 |
|
| 1176 |
- buildImage(name, cli.WithFlags("--force-rm"), build.WithBuildContext(c,
|
|
| 1177 |
- build.WithFile("Dockerfile", `FROM `+minimalBaseImage()+`
|
|
| 1176 |
+ r := buildImage(name, cli.WithFlags("--force-rm"), build.WithBuildContext(c,
|
|
| 1177 |
+ build.WithFile("Dockerfile", `FROM busybox
|
|
| 1178 | 1178 |
RUN true |
| 1179 |
- RUN thiswillfail`))).Assert(c, icmd.Expected{
|
|
| 1180 |
- ExitCode: 1, |
|
| 1181 |
- }) |
|
| 1179 |
+ RUN thiswillfail`))) |
|
| 1180 |
+ if r.ExitCode != 1 && r.ExitCode != 127 { // different on Linux / Windows
|
|
| 1181 |
+ c.Fatalf("Wrong exit code")
|
|
| 1182 |
+ } |
|
| 1182 | 1183 |
|
| 1183 | 1184 |
containerCountAfter := getContainerCount(c) |
| 1184 | 1185 |
if containerCountBefore != containerCountAfter {
|
| ... | ... |
@@ -4542,7 +4543,6 @@ func (s *DockerSuite) TestBuildBuildTimeArgOverrideEnvDefinedBeforeArg(c *check. |
| 4542 | 4542 |
} |
| 4543 | 4543 |
|
| 4544 | 4544 |
func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) {
|
| 4545 |
- testRequires(c, DaemonIsLinux) // Windows does not support ARG |
|
| 4546 | 4545 |
imgName := "bldvarstest" |
| 4547 | 4546 |
|
| 4548 | 4547 |
wdVar := "WDIR" |
| ... | ... |
@@ -4559,6 +4559,10 @@ func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) {
|
| 4559 | 4559 |
userVal := "testUser" |
| 4560 | 4560 |
volVar := "VOL" |
| 4561 | 4561 |
volVal := "/testVol/" |
| 4562 |
+ if DaemonIsWindows() {
|
|
| 4563 |
+ volVal = "C:\\testVol" |
|
| 4564 |
+ wdVal = "C:\\tmp" |
|
| 4565 |
+ } |
|
| 4562 | 4566 |
|
| 4563 | 4567 |
buildImageSuccessfully(c, imgName, |
| 4564 | 4568 |
cli.WithFlags( |
| ... | ... |
@@ -4594,7 +4598,7 @@ func (s *DockerSuite) TestBuildBuildTimeArgExpansion(c *check.C) {
|
| 4594 | 4594 |
) |
| 4595 | 4595 |
|
| 4596 | 4596 |
res := inspectField(c, imgName, "Config.WorkingDir") |
| 4597 |
- c.Check(res, check.Equals, filepath.ToSlash(wdVal)) |
|
| 4597 |
+ c.Check(filepath.ToSlash(res), check.Equals, filepath.ToSlash(wdVal)) |
|
| 4598 | 4598 |
|
| 4599 | 4599 |
var resArr []string |
| 4600 | 4600 |
inspectFieldAndUnmarshall(c, imgName, "Config.Env", &resArr) |