github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/dockerfile/ast.go (about) 1 package dockerfile 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "strings" 8 9 "github.com/distribution/reference" 10 "github.com/docker/cli/opts" 11 "github.com/moby/buildkit/frontend/dockerfile/command" 12 "github.com/moby/buildkit/frontend/dockerfile/instructions" 13 "github.com/moby/buildkit/frontend/dockerfile/parser" 14 "github.com/moby/buildkit/frontend/dockerfile/shell" 15 "github.com/pkg/errors" 16 17 "github.com/tilt-dev/tilt/internal/container" 18 ) 19 20 type AST struct { 21 directives []*parser.Directive 22 result *parser.Result 23 } 24 25 func ParseAST(df Dockerfile) (AST, error) { 26 result, err := parser.Parse(newReader(df)) 27 if err != nil { 28 return AST{}, errors.Wrap(err, "dockerfile.ParseAST") 29 } 30 31 dParser := &parser.DirectiveParser{} 32 directives, err := dParser.ParseAll([]byte(df)) 33 if err != nil { 34 return AST{}, errors.Wrap(err, "dockerfile.ParseAST") 35 } 36 37 return AST{ 38 directives: directives, 39 result: result, 40 }, nil 41 } 42 43 func (a AST) extractBaseNameInFromCommand(node *parser.Node, shlex *shell.Lex, metaArgs []instructions.ArgCommand) string { 44 if node.Next == nil { 45 return "" 46 } 47 48 inst, err := instructions.ParseInstruction(node) 49 if err != nil { 50 return node.Next.Value // if there's a parsing error, fallback to the first arg 51 } 52 53 fromInst, ok := inst.(*instructions.Stage) 54 if !ok || fromInst.BaseName == "" { 55 return "" 56 } 57 58 // The base image name may have ARG expansions in it. Do the default 59 // substitution. 60 argsMap := fakeArgsMap(shlex, metaArgs) 61 baseName, err := shlex.ProcessWordWithMap(fromInst.BaseName, argsMap) 62 if err != nil { 63 // If anything fails, just use the hard-coded BaseName. 64 return fromInst.BaseName 65 } 66 return baseName 67 68 } 69 70 // Find all images referenced in this dockerfile and call the visitor function. 71 // If the visitor function returns a new image, substitute that image into the dockerfile. 72 func (a AST) traverseImageRefs(visitor func(node *parser.Node, ref reference.Named) reference.Named, dockerfileArgs []instructions.ArgCommand) error { 73 metaArgs := append([]instructions.ArgCommand(nil), dockerfileArgs...) 74 shlex := shell.NewLex(a.result.EscapeToken) 75 76 return a.Traverse(func(node *parser.Node) error { 77 switch strings.ToLower(node.Value) { 78 case command.Arg: 79 inst, err := instructions.ParseInstruction(node) 80 if err != nil { 81 return nil // ignore parsing error 82 } 83 84 argCmd, ok := inst.(*instructions.ArgCommand) 85 if !ok { 86 return nil 87 } 88 89 // args within the Dockerfile are prepended because they provide defaults that are overridden by actual args 90 metaArgs = append([]instructions.ArgCommand{*argCmd}, metaArgs...) 91 92 case command.From: 93 baseName := a.extractBaseNameInFromCommand(node, shlex, metaArgs) 94 if baseName == "" { 95 return nil // ignore parsing error 96 } 97 98 ref, err := container.ParseNamed(baseName) 99 if err != nil { 100 return nil // drop the error, we don't care about malformed images 101 } 102 newRef := visitor(node, ref) 103 if newRef != nil { 104 node.Next.Value = container.FamiliarString(newRef) 105 } 106 107 case command.Copy: 108 if len(node.Flags) == 0 { 109 return nil 110 } 111 112 inst, err := instructions.ParseInstruction(node) 113 if err != nil { 114 return nil // ignore parsing error 115 } 116 117 copyCmd, ok := inst.(*instructions.CopyCommand) 118 if !ok { 119 return nil 120 } 121 122 ref, err := container.ParseNamed(copyCmd.From) 123 if err != nil { 124 return nil // drop the error, we don't care about malformed images 125 } 126 127 newRef := visitor(node, ref) 128 if newRef != nil { 129 for i, flag := range node.Flags { 130 if strings.HasPrefix(flag, "--from=") { 131 node.Flags[i] = fmt.Sprintf("--from=%s", container.FamiliarString(newRef)) 132 } 133 } 134 } 135 } 136 137 return nil 138 }) 139 } 140 141 func (a AST) InjectImageDigest(selector container.RefSelector, ref reference.NamedTagged, buildArgs []string) (bool, error) { 142 modified := false 143 err := a.traverseImageRefs(func(node *parser.Node, toReplace reference.Named) reference.Named { 144 if selector.Matches(toReplace) { 145 modified = true 146 return ref 147 } 148 return nil 149 }, argInstructions(buildArgs)) 150 return modified, err 151 } 152 153 // Post-order traversal of the Dockerfile AST. 154 // Halts immediately on error. 155 func (a AST) Traverse(visit func(*parser.Node) error) error { 156 return a.traverseNode(a.result.AST, visit) 157 } 158 159 func (a AST) traverseNode(node *parser.Node, visit func(*parser.Node) error) error { 160 for _, c := range node.Children { 161 err := a.traverseNode(c, visit) 162 if err != nil { 163 return err 164 } 165 } 166 return visit(node) 167 } 168 169 func (a AST) Print() (Dockerfile, error) { 170 buf := bytes.NewBuffer(nil) 171 currentLine := 1 172 173 directiveFmt := "# %s = %s\n" 174 for _, v := range a.directives { 175 _, err := fmt.Fprintf(buf, directiveFmt, v.Name, v.Value) 176 if err != nil { 177 return "", err 178 } 179 currentLine++ 180 } 181 182 for _, node := range a.result.AST.Children { 183 for currentLine < node.StartLine { 184 _, err := buf.Write([]byte("\n")) 185 if err != nil { 186 return "", err 187 } 188 currentLine++ 189 } 190 191 lineCount, err := a.printNode(node, buf) 192 if err != nil { 193 return "", err 194 } 195 196 currentLine = node.StartLine + lineCount 197 } 198 return Dockerfile(buf.String()), nil 199 } 200 201 // Loosely adapted from 202 // https://github.com/jessfraz/dockfmt/blob/master/format.go 203 // Returns the number of lines printed. 204 func (a AST) printNode(node *parser.Node, writer io.Writer) (int, error) { 205 var v string 206 207 // format per directive 208 switch strings.ToLower(node.Value) { 209 // all the commands that use parseMaybeJSON 210 // https://github.com/moby/buildkit/blob/2ec7d53b00f24624cda0adfbdceed982623a93b3/frontend/dockerfile/parser/parser.go#L152 211 case command.Cmd, command.Entrypoint, command.Run, command.Shell: 212 v = fmtCmd(node) 213 case command.Label: 214 v = fmtLabel(node) 215 default: 216 v = fmtDefault(node) 217 } 218 219 _, err := fmt.Fprintln(writer, v) 220 if err != nil { 221 return 0, err 222 } 223 return strings.Count(v, "\n") + 1, nil 224 } 225 226 func getCmd(n *parser.Node) []string { 227 if n == nil { 228 return nil 229 } 230 231 cmd := []string{strings.ToUpper(n.Value)} 232 if len(n.Flags) > 0 { 233 cmd = append(cmd, n.Flags...) 234 } 235 236 return append(cmd, getCmdArgs(n)...) 237 } 238 239 func getCmdArgs(n *parser.Node) []string { 240 if n == nil { 241 return nil 242 } 243 244 cmd := []string{} 245 for node := n.Next; node != nil; node = node.Next { 246 cmd = append(cmd, node.Value) 247 if len(node.Flags) > 0 { 248 cmd = append(cmd, node.Flags...) 249 } 250 } 251 252 return cmd 253 } 254 255 func appendHeredocs(node *parser.Node, cmdLine string) string { 256 if len(node.Heredocs) == 0 { 257 return cmdLine 258 } 259 lines := []string{cmdLine} 260 for _, h := range node.Heredocs { 261 lines = append(lines, fmt.Sprintf("\n%s%s", h.Content, h.Name)) 262 } 263 return strings.Join(lines, "") 264 } 265 266 func fmtCmd(node *parser.Node) string { 267 if node.Attributes["json"] { 268 cmd := []string{strings.ToUpper(node.Value)} 269 if len(node.Flags) > 0 { 270 cmd = append(cmd, node.Flags...) 271 } 272 273 encoded := []string{} 274 for _, c := range getCmdArgs(node) { 275 encoded = append(encoded, fmt.Sprintf("%q", c)) 276 } 277 return appendHeredocs(node, fmt.Sprintf("%s [%s]", strings.Join(cmd, " "), strings.Join(encoded, ", "))) 278 } 279 280 cmd := getCmd(node) 281 return appendHeredocs(node, strings.Join(cmd, " ")) 282 } 283 284 func fmtDefault(node *parser.Node) string { 285 cmd := getCmd(node) 286 return appendHeredocs(node, strings.Join(cmd, " ")) 287 } 288 289 func fmtLabel(node *parser.Node) string { 290 cmd := getCmd(node) 291 assignments := []string{cmd[0]} 292 for i := 1; i < len(cmd); i += 2 { 293 if i+1 < len(cmd) { 294 assignments = append(assignments, fmt.Sprintf("%s=%s", cmd[i], cmd[i+1])) 295 } else { 296 assignments = append(assignments, cmd[i]) 297 } 298 } 299 return strings.Join(assignments, " ") 300 } 301 302 func newReader(df Dockerfile) io.Reader { 303 return bytes.NewBufferString(string(df)) 304 } 305 306 // Loosely adapted from the buildkit code for turning args into a map. 307 // Iterate through them and do substitutions in order. 308 func fakeArgsMap(shlex *shell.Lex, args []instructions.ArgCommand) map[string]string { 309 m := make(map[string]string) 310 for _, argCmd := range args { 311 val := "" 312 for _, a := range argCmd.Args { 313 if a.Value != nil { 314 val, _ = shlex.ProcessWordWithMap(*(a.Value), m) 315 } 316 m[a.Key] = val 317 } 318 } 319 return m 320 } 321 322 // argInstructions converts a map of build arguments into a slice of ArgCommand structs. 323 // 324 // Since the map guarantees uniqueness, there is no defined order of the resulting slice. 325 func argInstructions(buildArgs []string) []instructions.ArgCommand { 326 var out []instructions.ArgCommand 327 for k, v := range opts.ConvertKVStringsToMapWithNil(buildArgs) { 328 out = append(out, instructions.ArgCommand{Args: []instructions.KeyValuePairOptional{ 329 { 330 Key: k, 331 Value: v, 332 }, 333 }}) 334 } 335 return out 336 }