Browse code

Add support for Dockerfile CMD options

This adds support for Dockerfile commands to have options - e.g:
COPY --user=john foo /tmp/
COPY --ignore-mtime foo /tmp/

Supports both booleans and strings.

Signed-off-by: Doug Davis <dug@us.ibm.com>

Doug Davis authored on 2015/01/28 00:57:34
Showing 8 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,155 @@
0
+package builder
1
+
2
+import (
3
+	"fmt"
4
+	"strings"
5
+)
6
+
7
+type FlagType int
8
+
9
+const (
10
+	boolType FlagType = iota
11
+	stringType
12
+)
13
+
14
+type BuilderFlags struct {
15
+	Args  []string // actual flags/args from cmd line
16
+	flags map[string]*Flag
17
+	used  map[string]*Flag
18
+	Err   error
19
+}
20
+
21
+type Flag struct {
22
+	bf       *BuilderFlags
23
+	name     string
24
+	flagType FlagType
25
+	Value    string
26
+}
27
+
28
+func NewBuilderFlags() *BuilderFlags {
29
+	return &BuilderFlags{
30
+		flags: make(map[string]*Flag),
31
+		used:  make(map[string]*Flag),
32
+	}
33
+}
34
+
35
+func (bf *BuilderFlags) AddBool(name string, def bool) *Flag {
36
+	flag := bf.addFlag(name, boolType)
37
+	if flag == nil {
38
+		return nil
39
+	}
40
+	if def {
41
+		flag.Value = "true"
42
+	} else {
43
+		flag.Value = "false"
44
+	}
45
+	return flag
46
+}
47
+
48
+func (bf *BuilderFlags) AddString(name string, def string) *Flag {
49
+	flag := bf.addFlag(name, stringType)
50
+	if flag == nil {
51
+		return nil
52
+	}
53
+	flag.Value = def
54
+	return flag
55
+}
56
+
57
+func (bf *BuilderFlags) addFlag(name string, flagType FlagType) *Flag {
58
+	if _, ok := bf.flags[name]; ok {
59
+		bf.Err = fmt.Errorf("Duplicate flag defined: %s", name)
60
+		return nil
61
+	}
62
+
63
+	newFlag := &Flag{
64
+		bf:       bf,
65
+		name:     name,
66
+		flagType: flagType,
67
+	}
68
+	bf.flags[name] = newFlag
69
+
70
+	return newFlag
71
+}
72
+
73
+func (fl *Flag) IsUsed() bool {
74
+	if _, ok := fl.bf.used[fl.name]; ok {
75
+		return true
76
+	}
77
+	return false
78
+}
79
+
80
+func (fl *Flag) IsTrue() bool {
81
+	if fl.flagType != boolType {
82
+		// Should never get here
83
+		panic(fmt.Errorf("Trying to use IsTrue on a non-boolean: %s", fl.name))
84
+	}
85
+	return fl.Value == "true"
86
+}
87
+
88
+func (bf *BuilderFlags) Parse() error {
89
+	// If there was an error while defining the possible flags
90
+	// go ahead and bubble it back up here since we didn't do it
91
+	// earlier in the processing
92
+	if bf.Err != nil {
93
+		return fmt.Errorf("Error setting up flags: %s", bf.Err)
94
+	}
95
+
96
+	for _, arg := range bf.Args {
97
+		if !strings.HasPrefix(arg, "--") {
98
+			return fmt.Errorf("Arg should start with -- : %s", arg)
99
+		}
100
+
101
+		if arg == "--" {
102
+			return nil
103
+		}
104
+
105
+		arg = arg[2:]
106
+		value := ""
107
+
108
+		index := strings.Index(arg, "=")
109
+		if index >= 0 {
110
+			value = arg[index+1:]
111
+			arg = arg[:index]
112
+		}
113
+
114
+		flag, ok := bf.flags[arg]
115
+		if !ok {
116
+			return fmt.Errorf("Unknown flag: %s", arg)
117
+		}
118
+
119
+		if _, ok = bf.used[arg]; ok {
120
+			return fmt.Errorf("Duplicate flag specified: %s", arg)
121
+		}
122
+
123
+		bf.used[arg] = flag
124
+
125
+		switch flag.flagType {
126
+		case boolType:
127
+			// value == "" is only ok if no "=" was specified
128
+			if index >= 0 && value == "" {
129
+				return fmt.Errorf("Missing a value on flag: %s", arg)
130
+			}
131
+
132
+			lower := strings.ToLower(value)
133
+			if lower == "" {
134
+				flag.Value = "true"
135
+			} else if lower == "true" || lower == "false" {
136
+				flag.Value = lower
137
+			} else {
138
+				return fmt.Errorf("Expecting boolean value for flag %s, not: %s", arg, value)
139
+			}
140
+
141
+		case stringType:
142
+			if index < 0 {
143
+				return fmt.Errorf("Missing a value on flag: %s", arg)
144
+			}
145
+			flag.Value = value
146
+
147
+		default:
148
+			panic(fmt.Errorf("No idea what kind of flag we have! Should never get here!"))
149
+		}
150
+
151
+	}
152
+
153
+	return nil
154
+}
0 155
new file mode 100644
... ...
@@ -0,0 +1,187 @@
0
+package builder
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 := NewBuilderFlags()
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 = NewBuilderFlags()
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 = NewBuilderFlags()
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() == true {
37
+		t.Fatalf("Test3 - str1 was not used!")
38
+	}
39
+	if flBool1.IsUsed() == true {
40
+		t.Fatalf("Test3 - bool1 was not used!")
41
+	}
42
+
43
+	// ---
44
+
45
+	bf = NewBuilderFlags()
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.Fatalf("Str1 was supposed to default to: HI")
56
+	}
57
+	if flBool1.IsTrue() {
58
+		t.Fatalf("Bool1 was supposed to default to: false")
59
+	}
60
+	if flStr1.IsUsed() == true {
61
+		t.Fatalf("Str1 was not used!")
62
+	}
63
+	if flBool1.IsUsed() == true {
64
+		t.Fatalf("Bool1 was not used!")
65
+	}
66
+
67
+	// ---
68
+
69
+	bf = NewBuilderFlags()
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 = NewBuilderFlags()
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 = NewBuilderFlags()
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 = NewBuilderFlags()
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.Fatalf("Test-b1 Bool1 was supposed to be true")
119
+	}
120
+
121
+	// ---
122
+
123
+	bf = NewBuilderFlags()
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.Fatalf("Test-b2 Bool1 was supposed to be true")
133
+	}
134
+
135
+	// ---
136
+
137
+	bf = NewBuilderFlags()
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.Fatalf("Test-b3 Bool1 was supposed to be false")
147
+	}
148
+
149
+	// ---
150
+
151
+	bf = NewBuilderFlags()
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 = NewBuilderFlags()
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 = NewBuilderFlags()
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("Teset %s, str1 should be BYE", bf.Args)
182
+	}
183
+	if !flBool1.IsTrue() {
184
+		t.Fatalf("Teset %s, bool1 should be true", bf.Args)
185
+	}
186
+}
... ...
@@ -47,6 +47,22 @@ func env(b *Builder, args []string, attributes map[string]bool, original string)
47 47
 		return fmt.Errorf("Bad input to ENV, too many args")
48 48
 	}
49 49
 
50
+	// TODO/FIXME/NOT USED
51
+	// Just here to show how to use the builder flags stuff within the
52
+	// context of a builder command. Will remove once we actually add
53
+	// a builder command to something!
54
+	/*
55
+		flBool1 := b.BuilderFlags.AddBool("bool1", false)
56
+		flStr1 := b.BuilderFlags.AddString("str1", "HI")
57
+
58
+		if err := b.BuilderFlags.Parse(); err != nil {
59
+			return err
60
+		}
61
+
62
+		fmt.Printf("Bool1:%v\n", flBool1)
63
+		fmt.Printf("Str1:%v\n", flStr1)
64
+	*/
65
+
50 66
 	commitStr := "ENV"
51 67
 
52 68
 	for j := 0; j < len(args); j++ {
... ...
@@ -116,6 +116,7 @@ type Builder struct {
116 116
 	image          string        // image name for commit processing
117 117
 	maintainer     string        // maintainer name. could probably be removed.
118 118
 	cmdSet         bool          // indicates is CMD was set in current Dockerfile
119
+	BuilderFlags   *BuilderFlags // current cmd's BuilderFlags - temporary
119 120
 	context        tarsum.TarSum // the context is a tarball that is uploaded by the client
120 121
 	contextPath    string        // the path of the temporary directory the local context is unpacked to (server side)
121 122
 	noBaseImage    bool          // indicates that this build does not start from any base image, but is being built from an empty file system.
... ...
@@ -276,8 +277,9 @@ func (b *Builder) dispatch(stepN int, ast *parser.Node) error {
276 276
 	cmd := ast.Value
277 277
 	attrs := ast.Attributes
278 278
 	original := ast.Original
279
+	flags := ast.Flags
279 280
 	strs := []string{}
280
-	msg := fmt.Sprintf("Step %d : %s", stepN, strings.ToUpper(cmd))
281
+	msg := fmt.Sprintf("Step %d : %s", stepN, original)
281 282
 
282 283
 	if cmd == "onbuild" {
283 284
 		if ast.Next == nil {
... ...
@@ -325,6 +327,8 @@ func (b *Builder) dispatch(stepN int, ast *parser.Node) error {
325 325
 	// XXX yes, we skip any cmds that are not valid; the parser should have
326 326
 	// picked these out already.
327 327
 	if f, ok := evaluateTable[cmd]; ok {
328
+		b.BuilderFlags = NewBuilderFlags()
329
+		b.BuilderFlags.Args = flags
328 330
 		return f(b, strList, attrs, original)
329 331
 	}
330 332
 
... ...
@@ -29,6 +29,7 @@ type Node struct {
29 29
 	Children   []*Node         // the children of this sexp
30 30
 	Attributes map[string]bool // special attributes for this node
31 31
 	Original   string          // original line used before parsing
32
+	Flags      []string        // only top Node should have this set
32 33
 }
33 34
 
34 35
 var (
... ...
@@ -75,7 +76,7 @@ func parseLine(line string) (string, *Node, error) {
75 75
 		return line, nil, nil
76 76
 	}
77 77
 
78
-	cmd, args, err := splitCommand(line)
78
+	cmd, flags, args, err := splitCommand(line)
79 79
 	if err != nil {
80 80
 		return "", nil, err
81 81
 	}
... ...
@@ -91,6 +92,7 @@ func parseLine(line string) (string, *Node, error) {
91 91
 	node.Next = sexp
92 92
 	node.Attributes = attrs
93 93
 	node.Original = line
94
+	node.Flags = flags
94 95
 
95 96
 	return "", node, nil
96 97
 }
97 98
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+FROM scratch
1
+COPY foo /tmp/
2
+COPY --user=me foo /tmp/
3
+COPY --doit=true foo /tmp/
4
+COPY --user=me --doit=true foo /tmp/
5
+COPY --doit=true -- foo /tmp/
6
+COPY -- foo /tmp/
7
+CMD --doit [ "a", "b" ]
8
+CMD --doit=true -- [ "a", "b" ]
9
+CMD --doit -- [ ]
0 10
new file mode 100644
... ...
@@ -0,0 +1,10 @@
0
+(from "scratch")
1
+(copy "foo" "/tmp/")
2
+(copy ["--user=me"] "foo" "/tmp/")
3
+(copy ["--doit=true"] "foo" "/tmp/")
4
+(copy ["--user=me" "--doit=true"] "foo" "/tmp/")
5
+(copy ["--doit=true"] "foo" "/tmp/")
6
+(copy "foo" "/tmp/")
7
+(cmd ["--doit"] "a" "b")
8
+(cmd ["--doit=true"] "a" "b")
9
+(cmd ["--doit"])
... ...
@@ -1,8 +1,10 @@
1 1
 package parser
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"strconv"
5 6
 	"strings"
7
+	"unicode"
6 8
 )
7 9
 
8 10
 // dumps the AST defined by `node` as a list of sexps. Returns a string
... ...
@@ -11,6 +13,10 @@ func (node *Node) Dump() string {
11 11
 	str := ""
12 12
 	str += node.Value
13 13
 
14
+	if len(node.Flags) > 0 {
15
+		str += fmt.Sprintf(" %q", node.Flags)
16
+	}
17
+
14 18
 	for _, n := range node.Children {
15 19
 		str += "(" + n.Dump() + ")\n"
16 20
 	}
... ...
@@ -48,20 +54,23 @@ func fullDispatch(cmd, args string) (*Node, map[string]bool, error) {
48 48
 
49 49
 // splitCommand takes a single line of text and parses out the cmd and args,
50 50
 // which are used for dispatching to more exact parsing functions.
51
-func splitCommand(line string) (string, string, error) {
51
+func splitCommand(line string) (string, []string, string, error) {
52 52
 	var args string
53
+	var flags []string
53 54
 
54 55
 	// Make sure we get the same results irrespective of leading/trailing spaces
55 56
 	cmdline := TOKEN_WHITESPACE.Split(strings.TrimSpace(line), 2)
56 57
 	cmd := strings.ToLower(cmdline[0])
57 58
 
58 59
 	if len(cmdline) == 2 {
59
-		args = strings.TrimSpace(cmdline[1])
60
+		var err error
61
+		args, flags, err = extractBuilderFlags(cmdline[1])
62
+		if err != nil {
63
+			return "", nil, "", err
64
+		}
60 65
 	}
61 66
 
62
-	// the cmd should never have whitespace, but it's possible for the args to
63
-	// have trailing whitespace.
64
-	return cmd, args, nil
67
+	return cmd, flags, strings.TrimSpace(args), nil
65 68
 }
66 69
 
67 70
 // covers comments and empty lines. Lines should be trimmed before passing to
... ...
@@ -74,3 +83,94 @@ func stripComments(line string) string {
74 74
 
75 75
 	return line
76 76
 }
77
+
78
+func extractBuilderFlags(line string) (string, []string, error) {
79
+	// Parses the BuilderFlags and returns the remaining part of the line
80
+
81
+	const (
82
+		inSpaces = iota // looking for start of a word
83
+		inWord
84
+		inQuote
85
+	)
86
+
87
+	words := []string{}
88
+	phase := inSpaces
89
+	word := ""
90
+	quote := '\000'
91
+	blankOK := false
92
+	var ch rune
93
+
94
+	for pos := 0; pos <= len(line); pos++ {
95
+		if pos != len(line) {
96
+			ch = rune(line[pos])
97
+		}
98
+
99
+		if phase == inSpaces { // Looking for start of word
100
+			if pos == len(line) { // end of input
101
+				break
102
+			}
103
+			if unicode.IsSpace(ch) { // skip spaces
104
+				continue
105
+			}
106
+
107
+			// Only keep going if the next word starts with --
108
+			if ch != '-' || pos+1 == len(line) || rune(line[pos+1]) != '-' {
109
+				return line[pos:], words, nil
110
+			}
111
+
112
+			phase = inWord // found someting with "--", fall thru
113
+		}
114
+		if (phase == inWord || phase == inQuote) && (pos == len(line)) {
115
+			if word != "--" && (blankOK || len(word) > 0) {
116
+				words = append(words, word)
117
+			}
118
+			break
119
+		}
120
+		if phase == inWord {
121
+			if unicode.IsSpace(ch) {
122
+				phase = inSpaces
123
+				if word == "--" {
124
+					return line[pos:], words, nil
125
+				}
126
+				if blankOK || len(word) > 0 {
127
+					words = append(words, word)
128
+				}
129
+				word = ""
130
+				blankOK = false
131
+				continue
132
+			}
133
+			if ch == '\'' || ch == '"' {
134
+				quote = ch
135
+				blankOK = true
136
+				phase = inQuote
137
+				continue
138
+			}
139
+			if ch == '\\' {
140
+				if pos+1 == len(line) {
141
+					continue // just skip \ at end
142
+				}
143
+				pos++
144
+				ch = rune(line[pos])
145
+			}
146
+			word += string(ch)
147
+			continue
148
+		}
149
+		if phase == inQuote {
150
+			if ch == quote {
151
+				phase = inWord
152
+				continue
153
+			}
154
+			if ch == '\\' {
155
+				if pos+1 == len(line) {
156
+					phase = inWord
157
+					continue // just skip \ at end
158
+				}
159
+				pos++
160
+				ch = rune(line[pos])
161
+			}
162
+			word += string(ch)
163
+		}
164
+	}
165
+
166
+	return "", words, nil
167
+}