istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/docker-builder/dockerfile/parse.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package dockerfile 16 17 import ( 18 "fmt" 19 "os" 20 "path/filepath" 21 "strings" 22 23 "github.com/moby/buildkit/frontend/dockerfile/parser" 24 "github.com/moby/buildkit/frontend/dockerfile/shell" 25 26 istiolog "istio.io/istio/pkg/log" 27 "istio.io/istio/tools/docker-builder/builder" 28 ) 29 30 // Option is a functional option for remote operations. 31 type Option func(*options) error 32 33 type options struct { 34 args map[string]string 35 ignoreRuns bool 36 baseDir string 37 } 38 39 // WithArgs sets the input args to the dockerfile 40 func WithArgs(a map[string]string) Option { 41 return func(o *options) error { 42 o.args = a 43 return nil 44 } 45 } 46 47 // IgnoreRuns tells the parser to ignore RUN statements rather than failing 48 func IgnoreRuns() Option { 49 return func(o *options) error { 50 o.ignoreRuns = true 51 return nil 52 } 53 } 54 55 // BaseDir is the directory that files are copied relative to. If not set, the base directory of the Dockerfile is used. 56 func BaseDir(dir string) Option { 57 return func(o *options) error { 58 o.baseDir = dir 59 return nil 60 } 61 } 62 63 var log = istiolog.RegisterScope("dockerfile", "") 64 65 type state struct { 66 args map[string]string 67 env map[string]string 68 labels map[string]string 69 bases map[string]string 70 71 copies map[string]string // copies stores a map of destination path -> source path 72 user string 73 workdir string 74 base string 75 entrypoint []string 76 cmd []string 77 78 shlex *shell.Lex 79 } 80 81 func cut(s, sep string) (before, after string) { 82 if i := strings.Index(s, sep); i >= 0 { 83 return s[:i], s[i+len(sep):] 84 } 85 return s, "" 86 } 87 88 // Parse parses the provided Dockerfile with the given args 89 func Parse(f string, opts ...Option) (builder.Args, error) { 90 empty := builder.Args{} 91 o := &options{ 92 baseDir: filepath.Dir(f), 93 } 94 95 for _, option := range opts { 96 if err := option(o); err != nil { 97 return empty, err 98 } 99 } 100 101 cmds, err := parseFile(f) 102 if err != nil { 103 return empty, fmt.Errorf("parse dockerfile %v: %v", f, err) 104 } 105 s := state{ 106 args: map[string]string{}, 107 env: map[string]string{}, 108 bases: map[string]string{}, 109 copies: map[string]string{}, 110 labels: map[string]string{}, 111 } 112 shlex := shell.NewLex('\\') 113 s.shlex = shlex 114 for k, v := range o.args { 115 s.args[k] = v 116 } 117 for _, c := range cmds { 118 switch c.Cmd { 119 case "ARG": 120 k, v := cut(c.Value[0], "=") 121 _, f := s.args[k] 122 if !f { 123 s.args[k] = v 124 } 125 case "FROM": 126 img := c.Value[0] 127 s.base = s.Expand(img) 128 if a, f := s.bases[s.base]; f { 129 s.base = a 130 } 131 if len(c.Value) == 3 { // FROM x as y 132 s.bases[c.Value[2]] = s.base 133 } 134 case "COPY": 135 // TODO you can copy multiple. This also doesn't handle folder semantics well 136 src := s.Expand(c.Value[0]) 137 dst := s.Expand(c.Value[1]) 138 s.copies[dst] = src 139 case "USER": 140 s.user = c.Value[0] 141 case "ENTRYPOINT": 142 s.entrypoint = c.Value 143 case "CMD": 144 s.cmd = c.Value 145 case "LABEL": 146 k := s.Expand(c.Value[0]) 147 v := s.Expand(c.Value[1]) 148 s.labels[k] = v 149 case "ENV": 150 k := s.Expand(c.Value[0]) 151 v := s.Expand(c.Value[1]) 152 s.env[k] = v 153 case "WORKDIR": 154 v := s.Expand(c.Value[0]) 155 s.workdir = v 156 case "RUN": 157 if o.ignoreRuns { 158 log.Warnf("Skipping RUN: %v", c.Value) 159 } else { 160 return empty, fmt.Errorf("unsupported RUN command: %v", c.Value) 161 } 162 default: 163 log.Warnf("did not handle %+v", c) 164 } 165 log.Debugf("%v: %+v", filepath.Base(c.Original), s) 166 } 167 return builder.Args{ 168 Env: s.env, 169 Labels: s.labels, 170 Cmd: s.cmd, 171 User: s.user, 172 WorkDir: s.workdir, 173 Entrypoint: s.entrypoint, 174 Base: s.base, 175 FilesBase: o.baseDir, 176 Files: s.copies, 177 }, nil 178 } 179 180 func (s state) Expand(i string) string { 181 avail := map[string]string{} 182 for k, v := range s.args { 183 avail[k] = v 184 } 185 for k, v := range s.env { 186 avail[k] = v 187 } 188 r, _ := s.shlex.ProcessWordWithMap(i, avail) 189 return r 190 } 191 192 // Below is inspired by MIT licensed https://github.com/asottile/dockerfile 193 194 // Command represents a single line (layer) in a Dockerfile. 195 // For example `FROM ubuntu:xenial` 196 type Command struct { 197 Cmd string // lowercased command name (ex: `from`) 198 SubCmd string // for ONBUILD only this holds the sub-command 199 JSON bool // whether the value is written in json form 200 Original string // The original source line 201 StartLine int // The original source line number which starts this command 202 EndLine int // The original source line number which ends this command 203 Flags []string // Any flags such as `--from=...` for `COPY`. 204 Value []string // The contents of the command (ex: `ubuntu:xenial`) 205 } 206 207 // parseFile parses a Dockerfile from a filename. 208 func parseFile(filename string) ([]Command, error) { 209 file, err := os.Open(filename) 210 if err != nil { 211 return nil, err 212 } 213 defer file.Close() 214 215 res, err := parser.Parse(file) 216 if err != nil { 217 return nil, err 218 } 219 220 var ret []Command 221 for _, child := range res.AST.Children { 222 cmd := Command{ 223 Cmd: child.Value, 224 Original: child.Original, 225 StartLine: child.StartLine, 226 EndLine: child.EndLine, 227 Flags: child.Flags, 228 } 229 230 // Only happens for ONBUILD 231 if child.Next != nil && len(child.Next.Children) > 0 { 232 cmd.SubCmd = child.Next.Children[0].Value 233 child = child.Next.Children[0] 234 } 235 236 cmd.JSON = child.Attributes["json"] 237 for n := child.Next; n != nil; n = n.Next { 238 cmd.Value = append(cmd.Value, n.Value) 239 } 240 241 ret = append(ret, cmd) 242 } 243 return ret, nil 244 }