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  }