package dockerfile2llb import ( "bytes" "context" "encoding/json" "fmt" "maps" "math" "net/url" "os" "path" "path/filepath" "regexp" "runtime" "slices" "strconv" "strings" "sync" "time" "github.com/containerd/platforms" "github.com/distribution/reference" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/client/llb/imagemetaresolver" "github.com/moby/buildkit/client/llb/sourceresolver" "github.com/moby/buildkit/frontend/dockerfile/dfgitutil" "github.com/moby/buildkit/frontend/dockerfile/instructions" "github.com/moby/buildkit/frontend/dockerfile/linter" "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/moby/buildkit/frontend/dockerfile/shell" "github.com/moby/buildkit/frontend/dockerui" "github.com/moby/buildkit/frontend/subrequests/lint" "github.com/moby/buildkit/frontend/subrequests/outline" "github.com/moby/buildkit/frontend/subrequests/targets" "github.com/moby/buildkit/identity" "github.com/moby/buildkit/solver/pb" "github.com/moby/buildkit/util/apicaps" "github.com/moby/buildkit/util/suggest" "github.com/moby/buildkit/util/system" dockerspec "github.com/moby/docker-image-spec/specs-go/v1" "github.com/moby/patternmatcher" "github.com/moby/sys/signal" digest "github.com/opencontainers/go-digest" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" mode "github.com/tonistiigi/dchapes-mode" "golang.org/x/sync/errgroup" ) const ( emptyImageName = "scratch" historyComment = "buildkit.dockerfile.v0" sbomScanContext = "BUILDKIT_SBOM_SCAN_CONTEXT" sbomScanStage = "BUILDKIT_SBOM_SCAN_STAGE" ) var ( secretsRegexpOnce sync.Once secretsRegexp *regexp.Regexp secretsAllowRegexp *regexp.Regexp ) var nonEnvArgs = map[string]struct{}{ sbomScanContext: {}, sbomScanStage: {}, } type ConvertOpt struct { dockerui.Config Client *dockerui.Client MainContext *llb.State SourceMap *llb.SourceMap TargetPlatform *ocispecs.Platform MetaResolver llb.ImageMetaResolver LLBCaps *apicaps.CapSet Warn linter.LintWarnFunc AllStages bool } type SBOMTargets struct { Core llb.State Extras map[string]llb.State IgnoreCache bool } func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (st *llb.State, img, baseImg *dockerspec.DockerOCIImage, sbom *SBOMTargets, err error) { ds, err := toDispatchState(ctx, dt, opt) if err != nil { return nil, nil, nil, nil, err } sbom = &SBOMTargets{ Core: ds.state, Extras: map[string]llb.State{}, } if ds.scanContext { sbom.Extras["context"] = ds.opt.buildContext } if ds.ignoreCache { sbom.IgnoreCache = true } for dsi := range allReachableStages(ds) { if ds != dsi && dsi.scanStage { sbom.Extras[dsi.stageName] = dsi.state if dsi.ignoreCache { sbom.IgnoreCache = true } } } return &ds.state, &ds.image, ds.baseImg, sbom, nil } func Dockerfile2Outline(ctx context.Context, dt []byte, opt ConvertOpt) (*outline.Outline, error) { ds, err := toDispatchState(ctx, dt, opt) if err != nil { return nil, err } o := ds.Outline(dt) return &o, nil } func DockerfileLint(ctx context.Context, dt []byte, opt ConvertOpt) (*lint.LintResults, error) { results := &lint.LintResults{} sourceIndex := results.AddSource(opt.SourceMap) opt.Warn = func(rulename, description, url, fmtmsg string, location []parser.Range) { results.AddWarning(rulename, description, url, fmtmsg, sourceIndex, location) } // for lint, no target means all targets if opt.Target == "" { opt.AllStages = true } _, err := toDispatchState(ctx, dt, opt) var errLoc *parser.LocationError if err != nil { buildErr := &lint.BuildError{ Message: err.Error(), } if errors.As(err, &errLoc) { ranges := mergeLocations(errLoc.Locations...) buildErr.Location = toPBLocation(sourceIndex, ranges) } results.Error = buildErr } return results, nil } func ListTargets(ctx context.Context, dt []byte) (*targets.List, error) { dockerfile, err := parser.Parse(bytes.NewReader(dt)) if err != nil { return nil, err } stages, _, err := instructions.Parse(dockerfile.AST, nil) if err != nil { return nil, err } l := &targets.List{ Sources: [][]byte{dt}, } for i, s := range stages { t := targets.Target{ Name: s.Name, Description: s.Comment, Default: i == len(stages)-1, Base: s.BaseName, Platform: s.Platform, Location: toSourceLocation(s.Location), } l.Targets = append(l.Targets, t) } return l, nil } func newRuleLinter(dt []byte, opt *ConvertOpt) (*linter.Linter, error) { var lintConfig *linter.Config if opt.Client != nil && opt.Client.LinterConfig != nil { lintConfig = opt.Client.LinterConfig } else { var err error lintOptionStr, _, _, _ := parser.ParseDirective("check", dt) lintConfig, err = linter.ParseLintOptions(lintOptionStr) if err != nil { return nil, errors.Wrapf(err, "failed to parse check options") } } lintConfig.Warn = opt.Warn return linter.New(lintConfig), nil } func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchState, error) { if len(dt) == 0 { return nil, errors.Errorf("the Dockerfile cannot be empty") } if opt.Client != nil && opt.MainContext != nil { return nil, errors.Errorf("Client and MainContext cannot both be provided") } namedContext := func(name string, copt dockerui.ContextOpt) (*dockerui.NamedContext, error) { if opt.Client == nil { return nil, nil } if !strings.EqualFold(name, "scratch") && !strings.EqualFold(name, "context") { if copt.Platform == nil { copt.Platform = opt.TargetPlatform } return opt.Client.NamedContext(name, copt) } return nil, nil } lint, err := newRuleLinter(dt, &opt) if err != nil { return nil, err } if opt.Client != nil && opt.LLBCaps == nil { caps := opt.Client.BuildOpts().LLBCaps opt.LLBCaps = &caps } dockerfile, err := parser.Parse(bytes.NewReader(dt)) if err != nil { return nil, err } // Moby still uses the `dockerfile.PrintWarnings` method to print non-empty // continuation line warnings. We iterate over those warnings here. for _, warning := range dockerfile.Warnings { // The `dockerfile.Warnings` *should* only contain warnings about empty continuation // lines, but we'll check the warning message to be sure, so that we don't accidentally // process warnings that are not related to empty continuation lines twice. if warning.URL == linter.RuleNoEmptyContinuation.URL { location := []parser.Range{*warning.Location} msg := linter.RuleNoEmptyContinuation.Format() lint.Run(&linter.RuleNoEmptyContinuation, location, msg) } } proxyEnv := proxyEnvFromBuildArgs(opt.BuildArgs) stages, argCmds, err := instructions.Parse(dockerfile.AST, lint) if err != nil { return nil, err } if len(stages) == 0 { return nil, errors.New("dockerfile contains no stages to build") } validateStageNames(stages, lint) validateCommandCasing(stages, lint) platformOpt := buildPlatformOpt(&opt) targetName := opt.Target if targetName == "" { targetName = stages[len(stages)-1].Name } globalArgs := defaultArgs(platformOpt, opt.BuildArgs, targetName) shlex := shell.NewLex(dockerfile.EscapeToken) outline := newOutlineCapture() // Validate that base images continue to be valid even // when no build arguments are used. validateBaseImagesWithDefaultArgs(stages, shlex, globalArgs, argCmds, lint) // Rebuild the arguments using the provided build arguments // for the remainder of the build. globalArgs, outline.allArgs, err = buildMetaArgs(globalArgs, shlex, argCmds, opt.BuildArgs) if err != nil { return nil, err } metaResolver := opt.MetaResolver if metaResolver == nil { metaResolver = imagemetaresolver.Default() } allDispatchStates := newDispatchStates() // set base state for every image for i, st := range stages { nameMatch, err := shlex.ProcessWordWithMatches(st.BaseName, globalArgs) argKeys := unusedFromArgsCheckKeys(globalArgs, outline.allArgs) reportUnusedFromArgs(argKeys, nameMatch.Unmatched, st.Location, lint) used := nameMatch.Matched if used == nil { used = map[string]struct{}{} } if err != nil { return nil, parser.WithLocation(err, st.Location) } if nameMatch.Result == "" { return nil, parser.WithLocation(errors.Errorf("base name (%s) should not be blank", st.BaseName), st.Location) } st.BaseName = nameMatch.Result ds := &dispatchState{ stage: st, deps: make(map[*dispatchState]instructions.Command), ctxPaths: make(map[string]struct{}), paths: make(map[string]struct{}), stageName: st.Name, prefixPlatform: opt.MultiPlatformRequested, outline: outline.clone(), epoch: opt.Epoch, } if v := st.Platform; v != "" { platMatch, err := shlex.ProcessWordWithMatches(v, globalArgs) argKeys := unusedFromArgsCheckKeys(globalArgs, outline.allArgs) reportUnusedFromArgs(argKeys, platMatch.Unmatched, st.Location, lint) reportRedundantTargetPlatform(st.Platform, platMatch, st.Location, globalArgs, lint) reportConstPlatformDisallowed(st.Name, platMatch, st.Location, lint) if err != nil { return nil, parser.WithLocation(errors.Wrapf(err, "failed to process arguments for platform %s", platMatch.Result), st.Location) } if platMatch.Result == "" { err := errors.Errorf("empty platform value from expression %s", v) err = parser.WithLocation(err, st.Location) err = wrapSuggestAny(err, platMatch.Unmatched, globalArgs.Keys()) return nil, err } p, err := platforms.Parse(platMatch.Result) if err != nil { err = parser.WithLocation(err, st.Location) err = wrapSuggestAny(err, platMatch.Unmatched, globalArgs.Keys()) return nil, parser.WithLocation(errors.Wrapf(err, "failed to parse platform %s", v), st.Location) } for k := range platMatch.Matched { used[k] = struct{}{} } ds.platform = &p } if st.Name != "" { nc, err := namedContext(st.Name, dockerui.ContextOpt{ Platform: ds.platform, ResolveMode: opt.ImageResolveMode.String(), AsyncLocalOpts: ds.asyncLocalOpts, }) if err != nil { return nil, err } if nc != nil { ds.namedContext = nc allDispatchStates.addState(ds) ds.base = nil // reset base set by addState continue } } if st.Name == "" { ds.stageName = fmt.Sprintf("stage-%d", i) } allDispatchStates.addState(ds) for k := range used { ds.outline.usedArgs[k] = struct{}{} } total := 0 if ds.stage.BaseName != emptyImageName && ds.base == nil { total = 1 } for _, cmd := range ds.stage.Commands { switch cmd.(type) { case *instructions.AddCommand, *instructions.CopyCommand, *instructions.RunCommand: total++ case *instructions.WorkdirCommand: total++ } } ds.cmdTotal = total if opt.Client != nil { ds.ignoreCache = opt.Client.IsNoCache(st.Name) } } var target *dispatchState if opt.Target == "" { target = allDispatchStates.lastTarget() } else { var ok bool target, ok = allDispatchStates.findStateByName(opt.Target) if !ok { return nil, suggest.WrapError(errors.Errorf("target stage %q could not be found", opt.Target), opt.Target, allDispatchStates.names(), true) } } // fill dependencies to stages so unreachable ones can avoid loading image configs for _, d := range allDispatchStates.states { d.commands = make([]command, len(d.stage.Commands)) for i, cmd := range d.stage.Commands { newCmd, err := toCommand(cmd, allDispatchStates, shlex) if err != nil { return nil, err } d.commands[i] = newCmd for _, src := range newCmd.sources { if src != nil { d.deps[src] = cmd if src.unregistered { allDispatchStates.addState(src) } } } } } if err := validateCircularDependency(allDispatchStates.states); err != nil { return nil, err } if len(allDispatchStates.states) == 1 { allDispatchStates.states[0].stageName = "" } resolveReachableStages := func(ctx context.Context, all []*dispatchState, target *dispatchState) (map[*dispatchState]struct{}, error) { allReachable := allReachableStages(target) eg, ctx := errgroup.WithContext(ctx) for i, d := range all { _, reachable := allReachable[d] if opt.AllStages { reachable = true } // resolve image config for every stage if d.base == nil && !d.dispatched && !d.resolved { d.resolved = reachable // avoid re-resolving if called again after onbuild if d.stage.BaseName == emptyImageName && d.namedContext == nil { d.state = llb.Scratch() d.image = emptyImage(platformOpt.targetPlatform) d.platform = &platformOpt.targetPlatform if d.unregistered { d.dispatched = true } continue } func(i int, d *dispatchState) { eg.Go(func() (err error) { defer func() { if err != nil { err = parser.WithLocation(err, d.stage.Location) } if d.unregistered { // implicit stages don't need further dispatch d.dispatched = true } }() origName := d.stage.BaseName ref, err := reference.ParseNormalizedNamed(d.stage.BaseName) if err != nil { return errors.Wrapf(err, "failed to parse stage name %q", d.stage.BaseName) } platform := d.platform if platform == nil { platform = &platformOpt.targetPlatform } d.stage.BaseName = reference.TagNameOnly(ref).String() var isScratch bool if reachable { // stage was named context if d.namedContext != nil { st, img, err := d.namedContext.Load(ctx) if err != nil { return err } d.dispatched = true d.state = *st if img != nil { img.Created = nil d.image = *img if img.Architecture != "" && img.OS != "" { d.platform = &ocispecs.Platform{ OS: img.OS, Architecture: img.Architecture, Variant: img.Variant, OSVersion: img.OSVersion, } if img.OSFeatures != nil { d.platform.OSFeatures = slices.Clone(img.OSFeatures) } } } return nil } // check if base is named context nc, err := namedContext(d.stage.BaseName, dockerui.ContextOpt{ ResolveMode: opt.ImageResolveMode.String(), Platform: platform, AsyncLocalOpts: d.asyncLocalOpts, }) if err != nil { return err } if nc != nil { st, img, err := nc.Load(ctx) if err != nil { return err } if img == nil { imgp := emptyImage(*platform) img = &imgp } d.baseImg = cloneX(img) // immutable img.Created = nil d.image = *img d.state = st.Platform(*platform) d.platform = platform return nil } prefix := "[" if opt.MultiPlatformRequested && platform != nil { prefix += platforms.FormatAll(*platform) + " " } prefix += "internal]" mutRef, dgst, dt, err := metaResolver.ResolveImageConfig(ctx, d.stage.BaseName, sourceresolver.Opt{ LogName: fmt.Sprintf("%s load metadata for %s", prefix, d.stage.BaseName), Platform: platform, ImageOpt: &sourceresolver.ResolveImageOpt{ ResolveMode: opt.ImageResolveMode.String(), }, }) if err != nil { return suggest.WrapError(errors.Wrap(err, origName), origName, append(allDispatchStates.names(), commonImageNames()...), true) } if ref.String() != mutRef { ref, err = reference.ParseNormalizedNamed(mutRef) if err != nil { return errors.Wrapf(err, "failed to parse ref %q", mutRef) } } var img dockerspec.DockerOCIImage if err := json.Unmarshal(dt, &img); err != nil { return errors.Wrap(err, "failed to parse image config") } d.baseImg = cloneX(&img) // immutable img.Created = nil // if there is no explicit target platform, try to match based on image config if d.platform == nil && platformOpt.implicitTarget { p := autoDetectPlatform(img, *platform, platformOpt.buildPlatforms) platform = &p } if dgst != "" { ref, err = reference.WithDigest(ref, dgst) if err != nil { return err } } d.stage.BaseName = ref.String() if len(img.RootFS.DiffIDs) == 0 { isScratch = true // schema1 images can't return diffIDs so double check :( for _, h := range img.History { if !h.EmptyLayer { isScratch = false break } } } d.image = img } if isScratch { d.state = llb.Scratch() } else { d.state = llb.Image(d.stage.BaseName, dfCmd(d.stage.SourceCode), llb.Platform(*platform), opt.ImageResolveMode, llb.WithCustomName(prefixCommand(d, "FROM "+d.stage.BaseName, opt.MultiPlatformRequested, platform, emptyEnvs{})), location(opt.SourceMap, d.stage.Location), ) if reachable { validateBaseImagePlatform(origName, *platform, d.image.Platform, d.stage.Location, lint) } } d.platform = platform return nil }) }(i, d) } } if err := eg.Wait(); err != nil { return nil, err } return allReachable, nil } var allReachable map[*dispatchState]struct{} for { allReachable, err = resolveReachableStages(ctx, allDispatchStates.states, target) if err != nil { return nil, err } // initialize onbuild triggers in case they create new dependencies newDeps := false for d := range allReachable { d.init() onbuilds := slices.Clone(d.image.Config.OnBuild) if d.base != nil && !d.onBuildInit { for _, cmd := range d.base.commands { if obCmd, ok := cmd.Command.(*instructions.OnbuildCommand); ok { onbuilds = append(onbuilds, obCmd.Expression) } } d.onBuildInit = true } if len(onbuilds) > 0 { if b, err := initOnBuildTriggers(d, onbuilds, allDispatchStates, shlex); err != nil { return nil, parser.SetLocation(err, d.stage.Location) } else if b { newDeps = true } d.image.Config.OnBuild = nil } } // in case new dependencies were added, we need to re-resolve reachable stages if !newDeps { break } } buildContext := &mutableOutput{} ctxPaths := map[string]struct{}{} var dockerIgnoreMatcher *patternmatcher.PatternMatcher if opt.Client != nil { dockerIgnorePatterns, err := opt.Client.DockerIgnorePatterns(ctx) if err != nil { return nil, err } if len(dockerIgnorePatterns) > 0 { dockerIgnoreMatcher, err = patternmatcher.New(dockerIgnorePatterns) if err != nil { return nil, err } } } for _, d := range allDispatchStates.states { if !opt.AllStages { if _, ok := allReachable[d]; !ok || d.dispatched { continue } } d.init() d.dispatched = true // Ensure platform is set. if d.platform == nil { d.platform = &d.opt.targetPlatform } // make sure that PATH is always set if _, ok := shell.EnvsFromSlice(d.image.Config.Env).Get("PATH"); !ok { var osName string if d.platform != nil { osName = d.platform.OS } // except for Windows, leave that to the OS. #5445 if osName != "windows" { d.image.Config.Env = append(d.image.Config.Env, "PATH="+system.DefaultPathEnv(osName)) } } // initialize base metadata from image conf for _, env := range d.image.Config.Env { k, v := parseKeyValue(env) d.state = d.state.AddEnv(k, v) } if opt.Hostname != "" { d.state = d.state.Hostname(opt.Hostname) } if d.image.Config.WorkingDir != "" { if err = dispatchWorkdir(d, &instructions.WorkdirCommand{Path: d.image.Config.WorkingDir}, false, nil); err != nil { return nil, parser.WithLocation(err, d.stage.Location) } } if d.image.Config.User != "" { if err = dispatchUser(d, &instructions.UserCommand{User: d.image.Config.User}, false); err != nil { return nil, parser.WithLocation(err, d.stage.Location) } } d.state = d.state.Network(opt.NetworkMode) opt := dispatchOpt{ allDispatchStates: allDispatchStates, globalArgs: globalArgs, buildArgValues: opt.BuildArgs, shlex: shlex, buildContext: llb.NewState(buildContext), proxyEnv: proxyEnv, cacheIDNamespace: opt.CacheIDNamespace, buildPlatforms: platformOpt.buildPlatforms, targetPlatform: platformOpt.targetPlatform, extraHosts: opt.ExtraHosts, shmSize: opt.ShmSize, ulimit: opt.Ulimits, devices: opt.Devices, cgroupParent: opt.CgroupParent, llbCaps: opt.LLBCaps, sourceMap: opt.SourceMap, lint: lint, dockerIgnoreMatcher: dockerIgnoreMatcher, } for _, cmd := range d.commands { if err := dispatch(d, cmd, opt); err != nil { return nil, parser.WithLocation(err, cmd.Location()) } } d.opt = opt for p := range d.ctxPaths { ctxPaths[p] = struct{}{} } for _, name := range []string{sbomScanContext, sbomScanStage} { var b bool if v, ok := d.opt.globalArgs.Get(name); ok { b = isEnabledForStage(d.stageName, v) } for _, kv := range d.buildArgs { if kv.Key == name && kv.Value != nil { b = isEnabledForStage(d.stageName, *kv.Value) } } if b { if name == sbomScanContext { d.scanContext = true } else { d.scanStage = true } } } } // Ensure the entirety of the target state is marked as used. // This is done after we've already evaluated every stage to ensure // the paths attribute is set correctly. target.paths["/"] = struct{}{} if len(opt.Labels) != 0 && target.image.Config.Labels == nil { target.image.Config.Labels = make(map[string]string, len(opt.Labels)) } maps.Copy(target.image.Config.Labels, opt.Labels) // If lint.Error() returns an error, it means that // there were warnings, and that our linter has been // configured to return an error on warnings, // so we appropriately return that error here. if err := lint.Error(); err != nil { return nil, err } opts := filterPaths(ctxPaths) bctx := opt.MainContext if opt.Client != nil { bctx, err = opt.Client.MainContext(ctx, opts...) if err != nil { return nil, err } } else if bctx == nil { bctx = dockerui.DefaultMainContext(opts...) } buildContext.Output = bctx.Output() defaults := []llb.ConstraintsOpt{ llb.Platform(platformOpt.targetPlatform), } if opt.LLBCaps != nil { defaults = append(defaults, llb.WithCaps(*opt.LLBCaps)) } target.state = target.state.SetMarshalDefaults(defaults...) if !platformOpt.implicitTarget { sameOsArch := platformOpt.targetPlatform.OS == target.image.OS && platformOpt.targetPlatform.Architecture == target.image.Architecture target.image.OS = platformOpt.targetPlatform.OS target.image.Architecture = platformOpt.targetPlatform.Architecture if platformOpt.targetPlatform.Variant != "" || !sameOsArch { target.image.Variant = platformOpt.targetPlatform.Variant } if platformOpt.targetPlatform.OSVersion != "" || !sameOsArch { target.image.OSVersion = platformOpt.targetPlatform.OSVersion } if platformOpt.targetPlatform.OSFeatures != nil { target.image.OSFeatures = slices.Clone(platformOpt.targetPlatform.OSFeatures) } } target.image.Platform = platforms.Normalize(target.image.Platform) return target, nil } func toCommand(ic instructions.Command, allDispatchStates *dispatchStates, shlex *shell.Lex) (command, error) { cmd := command{Command: ic} if c, ok := ic.(*instructions.CopyCommand); ok { if c.From != "" { res, err := shlex.ProcessWordWithMatches(c.From, shell.EnvsFromSlice(nil)) if err != nil { return command{}, err } if res.Result != c.From { return command{}, errors.Errorf("variable expansion is not supported for --from, define a new stage with FROM using ARG from global scope as a workaround") } var stn *dispatchState index, err := strconv.Atoi(c.From) if err != nil { stn, ok = allDispatchStates.findStateByName(c.From) if !ok { stn = &dispatchState{ stage: instructions.Stage{BaseName: c.From, Location: c.Location()}, deps: make(map[*dispatchState]instructions.Command), paths: make(map[string]struct{}), unregistered: true, } } } else { stn, err = allDispatchStates.findStateByIndex(index) if err != nil { return command{}, err } } cmd.sources = []*dispatchState{stn} } } if ok := detectRunMount(&cmd, allDispatchStates); ok { return cmd, nil } return cmd, nil } type dispatchOpt struct { allDispatchStates *dispatchStates globalArgs shell.EnvGetter buildArgValues map[string]string shlex *shell.Lex buildContext llb.State proxyEnv *llb.ProxyEnv cacheIDNamespace string targetPlatform ocispecs.Platform buildPlatforms []ocispecs.Platform extraHosts []llb.HostIP shmSize int64 ulimit []*pb.Ulimit devices []*pb.CDIDevice cgroupParent string llbCaps *apicaps.CapSet sourceMap *llb.SourceMap lint *linter.Linter dockerIgnoreMatcher *patternmatcher.PatternMatcher } func getEnv(state llb.State) shell.EnvGetter { return &envsFromState{state: &state} } type envsFromState struct { state *llb.State once sync.Once env shell.EnvGetter } func (e *envsFromState) init() { env, err := e.state.Env(context.TODO()) if err != nil { return } e.env = env } func (e *envsFromState) Get(key string) (string, bool) { e.once.Do(e.init) return e.env.Get(key) } func (e *envsFromState) Keys() []string { e.once.Do(e.init) return e.env.Keys() } func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error { d.cmdIsOnBuild = cmd.isOnBuild var err error // ARG command value could be ignored, so defer handling the expansion error _, isArg := cmd.Command.(*instructions.ArgCommand) if ex, ok := cmd.Command.(instructions.SupportsSingleWordExpansion); ok && !isArg { err := ex.Expand(func(word string) (string, error) { env := getEnv(d.state) newword, unmatched, err := opt.shlex.ProcessWord(word, env) reportUnmatchedVariables(cmd, d.buildArgs, env, unmatched, &opt) return newword, err }) if err != nil { return err } } if ex, ok := cmd.Command.(instructions.SupportsSingleWordExpansionRaw); ok { err := ex.ExpandRaw(func(word string) (string, error) { lex := shell.NewLex('\\') lex.SkipProcessQuotes = true env := getEnv(d.state) newword, unmatched, err := lex.ProcessWord(word, env) reportUnmatchedVariables(cmd, d.buildArgs, env, unmatched, &opt) return newword, err }) if err != nil { return err } } switch c := cmd.Command.(type) { case *instructions.MaintainerCommand: err = dispatchMaintainer(d, c) case *instructions.EnvCommand: err = dispatchEnv(d, c, opt.lint) case *instructions.RunCommand: err = dispatchRun(d, c, opt.proxyEnv, cmd.sources, opt) case *instructions.WorkdirCommand: err = dispatchWorkdir(d, c, true, &opt) case *instructions.AddCommand: err = dispatchCopy(d, copyConfig{ params: c.SourcesAndDest, excludePatterns: c.ExcludePatterns, source: opt.buildContext, isAddCommand: true, cmdToPrint: c, chown: c.Chown, chmod: c.Chmod, link: c.Link, keepGitDir: c.KeepGitDir, checksum: c.Checksum, unpack: c.Unpack, location: c.Location(), ignoreMatcher: opt.dockerIgnoreMatcher, opt: opt, }) if err == nil { for _, src := range c.SourcePaths { if !strings.HasPrefix(src, "http://") && !strings.HasPrefix(src, "https://") { d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{} } } } case *instructions.LabelCommand: err = dispatchLabel(d, c, opt.lint) case *instructions.OnbuildCommand: err = dispatchOnbuild(d, c) case *instructions.CmdCommand: err = dispatchCmd(d, c, opt.lint) case *instructions.EntrypointCommand: err = dispatchEntrypoint(d, c, opt.lint) case *instructions.HealthCheckCommand: err = dispatchHealthcheck(d, c, opt.lint) case *instructions.ExposeCommand: err = dispatchExpose(d, c, &opt) case *instructions.UserCommand: err = dispatchUser(d, c, true) case *instructions.VolumeCommand: err = dispatchVolume(d, c) case *instructions.StopSignalCommand: err = dispatchStopSignal(d, c) case *instructions.ShellCommand: err = dispatchShell(d, c) case *instructions.ArgCommand: err = dispatchArg(d, c, &opt) case *instructions.CopyCommand: l := opt.buildContext var ignoreMatcher *patternmatcher.PatternMatcher if len(cmd.sources) != 0 { src := cmd.sources[0] if !src.dispatched { return errors.Errorf("cannot copy from stage %q, it needs to be defined before current stage %q", c.From, d.stageName) } l = src.state } else { ignoreMatcher = opt.dockerIgnoreMatcher } err = dispatchCopy(d, copyConfig{ params: c.SourcesAndDest, excludePatterns: c.ExcludePatterns, source: l, isAddCommand: false, cmdToPrint: c, chown: c.Chown, chmod: c.Chmod, link: c.Link, parents: c.Parents, location: c.Location(), ignoreMatcher: ignoreMatcher, opt: opt, }) if err == nil { if len(cmd.sources) == 0 { for _, src := range c.SourcePaths { d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{} } } else { source := cmd.sources[0] if source.paths == nil { source.paths = make(map[string]struct{}) } for _, src := range c.SourcePaths { source.paths[path.Join("/", filepath.ToSlash(src))] = struct{}{} } } } default: } return err } type dispatchState struct { opt dispatchOpt state llb.State image dockerspec.DockerOCIImage namedContext *dockerui.NamedContext platform *ocispecs.Platform stage instructions.Stage base *dispatchState baseImg *dockerspec.DockerOCIImage // immutable, unlike image dispatched bool resolved bool // resolved is set to true if base image has been resolved onBuildInit bool deps map[*dispatchState]instructions.Command buildArgs []instructions.KeyValuePairOptional commands []command // ctxPaths marks the paths this dispatchState uses from the build context. ctxPaths map[string]struct{} // paths marks the paths that are used by this dispatchState. paths map[string]struct{} ignoreCache bool unregistered bool stageName string cmdIndex int cmdIsOnBuild bool cmdTotal int prefixPlatform bool outline outlineCapture epoch *time.Time scanStage bool scanContext bool // workdirSet is set to true if a workdir has been set // within the current dockerfile. workdirSet bool entrypoint instructionTracker cmd instructionTracker healthcheck instructionTracker } func (ds *dispatchState) asyncLocalOpts() []llb.LocalOption { return filterPaths(ds.paths) } // init is invoked when the dispatch state inherits its attributes // from the base image. func (ds *dispatchState) init() { // mark as initialized, used to determine states that have not been dispatched yet if ds.base == nil { return } ds.state = ds.base.state ds.platform = ds.base.platform ds.image = clone(ds.base.image) // onbuild triggers to not carry over from base stage ds.image.Config.OnBuild = nil ds.baseImg = cloneX(ds.base.baseImg) // Utilize the same path index as our base image so we propagate // the paths we use back to the base image. ds.paths = ds.base.paths ds.workdirSet = ds.base.workdirSet ds.buildArgs = append(ds.buildArgs, ds.base.buildArgs...) } type dispatchStates struct { states []*dispatchState statesByName map[string]*dispatchState } func newDispatchStates() *dispatchStates { return &dispatchStates{statesByName: map[string]*dispatchState{}} } func (dss *dispatchStates) names() []string { names := make([]string, 0, len(dss.states)) for _, s := range dss.states { if s.stageName != "" { names = append(names, s.stageName) } } return names } func (dss *dispatchStates) addState(ds *dispatchState) { dss.states = append(dss.states, ds) if d, ok := dss.statesByName[ds.stage.BaseName]; ok { ds.base = d ds.outline = d.outline.clone() } if ds.stage.Name != "" { dss.statesByName[strings.ToLower(ds.stage.Name)] = ds } } func (dss *dispatchStates) findStateByName(name string) (*dispatchState, bool) { ds, ok := dss.statesByName[strings.ToLower(name)] return ds, ok } func (dss *dispatchStates) findStateByIndex(index int) (*dispatchState, error) { if index < 0 || index >= len(dss.states) { return nil, errors.Errorf("invalid stage index %d", index) } return dss.states[index], nil } func (dss *dispatchStates) lastTarget() *dispatchState { return dss.states[len(dss.states)-1] } type command struct { instructions.Command sources []*dispatchState isOnBuild bool } // initOnBuildTriggers initializes the onbuild triggers and creates the commands and dependecies for them. // It returns true if there were any new dependencies added that need to be resolved. func initOnBuildTriggers(d *dispatchState, triggers []string, allDispatchStates *dispatchStates, shlex *shell.Lex) (bool, error) { hasNewDeps := false commands := make([]command, 0, len(triggers)) for _, trigger := range triggers { ast, err := parser.Parse(strings.NewReader(trigger)) if err != nil { return false, err } if len(ast.AST.Children) != 1 { return false, errors.New("onbuild trigger should be a single expression") } node := ast.AST.Children[0] // reset the location to the onbuild trigger node.StartLine, node.EndLine = rangeStartEnd(d.stage.Location) ic, err := instructions.ParseCommand(ast.AST.Children[0]) if err != nil { return false, err } cmd, err := toCommand(ic, allDispatchStates, shlex) if err != nil { return false, err } cmd.isOnBuild = true if len(cmd.sources) > 0 { hasNewDeps = true } commands = append(commands, cmd) for _, src := range cmd.sources { if src != nil { d.deps[src] = cmd if src.unregistered { allDispatchStates.addState(src) } } } } d.commands = append(commands, d.commands...) d.cmdTotal += len(commands) return hasNewDeps, nil } func dispatchEnv(d *dispatchState, c *instructions.EnvCommand, lint *linter.Linter) error { commitMessage := bytes.NewBufferString("ENV") for _, e := range c.Env { if e.NoDelim { msg := linter.RuleLegacyKeyValueFormat.Format(c.Name()) lint.Run(&linter.RuleLegacyKeyValueFormat, c.Location(), msg) } validateNoSecretKey("ENV", e.Key, c.Location(), lint) commitMessage.WriteString(" " + e.String()) d.state = d.state.AddEnv(e.Key, e.Value) d.image.Config.Env = addEnv(d.image.Config.Env, e.Key, e.Value) } return commitToHistory(&d.image, commitMessage.String(), false, nil, d.epoch) } func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyEnv, sources []*dispatchState, dopt dispatchOpt) error { var opt []llb.RunOption customname := c.String() // Run command can potentially access any file. Mark the full filesystem as used. d.paths["/"] = struct{}{} var args = c.CmdLine if len(c.Files) > 0 { if len(args) != 1 || !c.PrependShell { return errors.Errorf("parsing produced an invalid run command: %v", args) } if heredoc := parser.MustParseHeredoc(args[0]); heredoc != nil { if d.image.OS != "windows" && strings.HasPrefix(c.Files[0].Data, "#!") { // This is a single heredoc with a shebang, so create a file // and run it. // NOTE: choosing to expand doesn't really make sense here, so // we silently ignore that option if it was provided. sourcePath := "/" destPath := "/dev/pipes/" f := c.Files[0].Name data := c.Files[0].Data if c.Files[0].Chomp { data = parser.ChompHeredocContent(data) } st := llb.Scratch().Dir(sourcePath).File( llb.Mkfile(f, 0755, []byte(data)), dockerui.WithInternalName("preparing inline document"), llb.Platform(*d.platform), ) mount := llb.AddMount(destPath, st, llb.SourcePath(sourcePath), llb.Readonly) opt = append(opt, mount) args = []string{path.Join(destPath, f)} } else { // Just a simple heredoc, so just run the contents in the // shell: this creates the effect of a "fake"-heredoc, so that // the syntax can still be used for shells that don't support // heredocs directly. // NOTE: like above, we ignore the expand option. data := c.Files[0].Data if c.Files[0].Chomp { data = parser.ChompHeredocContent(data) } args = []string{data} } customname += fmt.Sprintf(" (%s)", summarizeHeredoc(c.Files[0].Data)) } else { // More complex heredoc, so reconstitute it, and pass it to the // shell to handle. full := args[0] for _, file := range c.Files { full += "\n" + file.Data + file.Name } args = []string{full} } } if c.PrependShell { // Don't pass the linter function because we do not report a warning for // shell usage on run commands. args = withShell(d.image, args) } opt = append(opt, llb.Args(args), dfCmd(c), location(dopt.sourceMap, c.Location())) if d.ignoreCache { opt = append(opt, llb.IgnoreCache) } if proxy != nil { opt = append(opt, llb.WithProxy(*proxy)) } runMounts, err := dispatchRunMounts(d, c, sources, dopt) if err != nil { return err } opt = append(opt, runMounts...) securityOpt, err := dispatchRunSecurity(c) if err != nil { return err } if securityOpt != nil { opt = append(opt, securityOpt) } networkOpt, err := dispatchRunNetwork(c) if err != nil { return err } if networkOpt != nil { opt = append(opt, networkOpt) } if dopt.llbCaps != nil && dopt.llbCaps.Supports(pb.CapExecMetaUlimit) == nil { for _, u := range dopt.ulimit { opt = append(opt, llb.AddUlimit(llb.UlimitName(u.Name), u.Soft, u.Hard)) } } if dopt.llbCaps != nil && dopt.llbCaps.Supports(pb.CapExecMetaCDI) == nil { for _, device := range dopt.devices { deviceOpts := []llb.CDIDeviceOption{ llb.CDIDeviceName(device.Name), } if device.Optional { deviceOpts = append(deviceOpts, llb.CDIDeviceOptional) } opt = append(opt, llb.AddCDIDevice(deviceOpts...)) } runDevices, err := dispatchRunDevices(c) if err != nil { return err } opt = append(opt, runDevices...) } shlex := *dopt.shlex shlex.RawQuotes = true shlex.SkipUnsetEnv = true pl, err := d.state.GetPlatform(context.TODO()) if err != nil { return err } env := getEnv(d.state) opt = append(opt, llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(&shlex, customname, withSecretEnvMask(c, env))), d.prefixPlatform, pl, env))) for _, h := range dopt.extraHosts { opt = append(opt, llb.AddExtraHost(h.Host, h.IP)) } if dopt.llbCaps != nil && dopt.llbCaps.Supports(pb.CapExecMountTmpfsSize) == nil { if dopt.shmSize > 0 { opt = append(opt, llb.AddMount("/dev/shm", llb.Scratch(), llb.Tmpfs(llb.TmpfsSize(dopt.shmSize)))) } } if dopt.llbCaps != nil && dopt.llbCaps.Supports(pb.CapExecMetaCgroupParent) == nil { if len(dopt.cgroupParent) > 0 { opt = append(opt, llb.WithCgroupParent(dopt.cgroupParent)) } } d.state = d.state.Run(opt...).Root() return commitToHistory(&d.image, "RUN "+runCommandString(args, d.buildArgs, env), true, &d.state, d.epoch) } func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bool, opt *dispatchOpt) error { if commit { // This linter rule checks if workdir has been set to an absolute value locally // within the current dockerfile. Absolute paths in base images are ignored // because they might change and it is not advised to rely on them. // // We only run this check when commit is true. Commit is true when we are performing // this operation on a local call to workdir rather than one coming from // the base image. We only check the first instance of workdir being set // so successive relative paths are ignored because every instance is fixed // by fixing the first one. if !d.workdirSet && !system.IsAbs(c.Path, d.platform.OS) { msg := linter.RuleWorkdirRelativePath.Format(c.Path) opt.lint.Run(&linter.RuleWorkdirRelativePath, c.Location(), msg) } d.workdirSet = true } wd, err := system.NormalizeWorkdir(d.image.Config.WorkingDir, c.Path, d.platform.OS) if err != nil { return errors.Wrap(err, "normalizing workdir") } // NormalizeWorkdir returns paths with platform specific separators. For Windows // this will be of the form: \some\path, which is needed later when we pass it to // HCS. d.image.Config.WorkingDir = wd // From this point forward, we can use UNIX style paths. wd = system.ToSlash(wd, d.platform.OS) d.state = d.state.Dir(wd) if commit { withLayer := false if wd != "/" { mkdirOpt := []llb.MkdirOption{llb.WithParents(true)} if user := d.image.Config.User; user != "" { mkdirOpt = append(mkdirOpt, llb.WithUser(user)) } if d.epoch != nil { mkdirOpt = append(mkdirOpt, llb.WithCreatedTime(*d.epoch)) } platform := opt.targetPlatform if d.platform != nil { platform = *d.platform } env := getEnv(d.state) d.state = d.state.File(llb.Mkdir(wd, 0755, mkdirOpt...), llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(opt.shlex, c.String(), env)), d.prefixPlatform, &platform, env)), location(opt.sourceMap, c.Location()), llb.Platform(*d.platform), ) withLayer = true } return commitToHistory(&d.image, "WORKDIR "+wd, withLayer, nil, d.epoch) } return nil } func dispatchCopy(d *dispatchState, cfg copyConfig) error { dest, err := pathRelativeToWorkingDir(d.state, cfg.params.DestPath, *d.platform) if err != nil { return err } var copyOpt []llb.CopyOption if cfg.chown != "" { copyOpt = append(copyOpt, llb.WithUser(cfg.chown)) } if len(cfg.excludePatterns) > 0 { // in theory we don't need to check whether there are any exclude patterns, // as an empty list is a no-op. However, performing the check makes // the code easier to understand and costs virtually nothing. copyOpt = append(copyOpt, llb.WithExcludePatterns(cfg.excludePatterns)) } var chopt *llb.ChmodOpt if cfg.chmod != "" { chopt = &llb.ChmodOpt{} p, err := strconv.ParseUint(cfg.chmod, 8, 32) nonOctalErr := errors.Errorf("invalid chmod parameter: '%v'. it should be octal string and between 0 and 07777", cfg.chmod) if err == nil { if p > 0o7777 { return nonOctalErr } chopt.Mode = os.FileMode(p) } else { if _, err := mode.Parse(cfg.chmod); err != nil { var ne *strconv.NumError if errors.As(err, &ne) { return nonOctalErr // return nonOctalErr for compatibility if the value looks numeric } return err } chopt.ModeStr = cfg.chmod } } if cfg.checksum != "" { if !cfg.isAddCommand { return errors.New("checksum can't be specified for COPY") } if len(cfg.params.SourcePaths) != 1 { return errors.New("checksum can't be specified for multiple sources") } if !isHTTPSource(cfg.params.SourcePaths[0]) && !isGitSource(cfg.params.SourcePaths[0]) { return errors.New("checksum requires HTTP(S) or Git sources") } } commitMessage := bytes.NewBufferString("") if cfg.isAddCommand { commitMessage.WriteString("ADD") } else { commitMessage.WriteString("COPY") } if cfg.parents { commitMessage.WriteString(" " + "--parents") } if cfg.chown != "" { commitMessage.WriteString(" " + "--chown=" + cfg.chown) } if cfg.chmod != "" { commitMessage.WriteString(" " + "--chmod=" + cfg.chmod) } platform := cfg.opt.targetPlatform if d.platform != nil { platform = *d.platform } env := getEnv(d.state) name := uppercaseCmd(processCmdEnv(cfg.opt.shlex, cfg.cmdToPrint.String(), env)) pgName := prefixCommand(d, name, d.prefixPlatform, &platform, env) var a *llb.FileAction for _, src := range cfg.params.SourcePaths { commitMessage.WriteString(" " + src) gitRef, isGit, gitRefErr := dfgitutil.ParseGitRef(src) if gitRefErr != nil && isGit { return gitRefErr } if gitRefErr == nil && !gitRef.IndistinguishableFromLocal { if !cfg.isAddCommand { return errors.New("source can't be a git ref for COPY") } // TODO: print a warning (not an error) if gitRef.UnencryptedTCP is true gitOptions := []llb.GitOption{ llb.WithCustomName(pgName), llb.GitRef(gitRef.Ref), } if cfg.keepGitDir != nil && gitRef.KeepGitDir != nil { if *cfg.keepGitDir != *gitRef.KeepGitDir { return errors.New("inconsistent keep-git-dir configuration") } } if gitRef.KeepGitDir != nil { cfg.keepGitDir = gitRef.KeepGitDir } if cfg.keepGitDir != nil && *cfg.keepGitDir { gitOptions = append(gitOptions, llb.KeepGitDir()) } if cfg.checksum != "" && gitRef.Checksum != "" { if cfg.checksum != gitRef.Checksum { return errors.Errorf("checksum mismatch %q != %q", cfg.checksum, gitRef.Checksum) } } if gitRef.Checksum != "" { cfg.checksum = gitRef.Checksum } if cfg.checksum != "" { gitOptions = append(gitOptions, llb.GitChecksum(cfg.checksum)) } if gitRef.SubDir != "" { gitOptions = append(gitOptions, llb.GitSubDir(gitRef.SubDir)) } if gitRef.Submodules != nil && !*gitRef.Submodules { gitOptions = append(gitOptions, llb.GitSkipSubmodules()) } st := llb.Git(gitRef.Remote, "", gitOptions...) opts := append([]llb.CopyOption{&llb.CopyInfo{ Mode: chopt, CreateDestPath: true, }}, copyOpt...) if a == nil { a = llb.Copy(st, "/", dest, opts...) } else { a = a.Copy(st, "/", dest, opts...) } } else if isHTTPSource(src) { if !cfg.isAddCommand { return errors.New("source can't be a URL for COPY") } // Resources from remote URLs are not decompressed. // https://docs.docker.com/engine/reference/builder/#add // // Note: mixing up remote archives and local archives in a single ADD instruction // would result in undefined behavior: https://github.com/moby/buildkit/pull/387#discussion_r189494717 u, err := url.Parse(src) f := "__unnamed__" if err == nil { if base := path.Base(u.Path); base != "." && base != "/" { f = base } } var checksum digest.Digest if cfg.checksum != "" { checksum, err = digest.Parse(cfg.checksum) if err != nil { return err } } st := llb.HTTP(src, llb.Filename(f), llb.WithCustomName(pgName), llb.Checksum(checksum), dfCmd(cfg.params)) var unpack bool if cfg.unpack != nil { unpack = *cfg.unpack } opts := append([]llb.CopyOption{&llb.CopyInfo{ Mode: chopt, CreateDestPath: true, AttemptUnpack: unpack, }}, copyOpt...) if a == nil { a = llb.Copy(st, f, dest, opts...) } else { a = a.Copy(st, f, dest, opts...) } } else { validateCopySourcePath(src, &cfg) var patterns []string if cfg.parents { // detect optional pivot point parent, pattern, ok := strings.Cut(src, "/./") if !ok { pattern = src src = "/" } else { src = parent } pattern, err = system.NormalizePath("/", pattern, d.platform.OS, false) if err != nil { return errors.Wrap(err, "removing drive letter") } patterns = []string{strings.TrimPrefix(pattern, "/")} } src, err = system.NormalizePath("/", src, d.platform.OS, false) if err != nil { return errors.Wrap(err, "removing drive letter") } unpack := cfg.isAddCommand if cfg.unpack != nil { unpack = *cfg.unpack } opts := append([]llb.CopyOption{&llb.CopyInfo{ Mode: chopt, FollowSymlinks: true, CopyDirContentsOnly: true, IncludePatterns: patterns, AttemptUnpack: unpack, CreateDestPath: true, AllowWildcard: true, AllowEmptyWildcard: true, }}, copyOpt...) if a == nil { a = llb.Copy(cfg.source, src, dest, opts...) } else { a = a.Copy(cfg.source, src, dest, opts...) } } } for _, src := range cfg.params.SourceContents { commitMessage.WriteString(" <<" + src.Path) data := src.Data f, err := system.CheckSystemDriveAndRemoveDriveLetter(src.Path, d.platform.OS, false) if err != nil { return errors.Wrap(err, "removing drive letter") } st := llb.Scratch().File( llb.Mkfile(f, 0644, []byte(data)), dockerui.WithInternalName("preparing inline document"), llb.Platform(*d.platform), ) opts := append([]llb.CopyOption{&llb.CopyInfo{ Mode: chopt, CreateDestPath: true, }}, copyOpt...) if a == nil { a = llb.Copy(st, system.ToSlash(f, d.platform.OS), dest, opts...) } else { a = a.Copy(st, filepath.ToSlash(f), dest, opts...) } } commitMessage.WriteString(" " + cfg.params.DestPath) fileOpt := []llb.ConstraintsOpt{ llb.WithCustomName(pgName), location(cfg.opt.sourceMap, cfg.location), } if d.ignoreCache { fileOpt = append(fileOpt, llb.IgnoreCache) } // cfg.opt.llbCaps can be nil in unit tests if cfg.opt.llbCaps != nil && cfg.opt.llbCaps.Supports(pb.CapMergeOp) == nil && cfg.link && cfg.chmod == "" { pgID := identity.NewID() d.cmdIndex-- // prefixCommand increases it pgName := prefixCommand(d, name, d.prefixPlatform, &platform, env) copyOpts := []llb.ConstraintsOpt{ llb.Platform(*d.platform), } copyOpts = append(copyOpts, fileOpt...) copyOpts = append(copyOpts, llb.ProgressGroup(pgID, pgName, true)) mergeOpts := slices.Clone(fileOpt) d.cmdIndex-- mergeOpts = append(mergeOpts, llb.ProgressGroup(pgID, pgName, false), llb.WithCustomName(prefixCommand(d, "LINK "+name, d.prefixPlatform, &platform, env))) d.state = d.state.WithOutput(llb.Merge([]llb.State{d.state, llb.Scratch().File(a, copyOpts...)}, mergeOpts...).Output()) } else { d.state = d.state.File(a, fileOpt...) } return commitToHistory(&d.image, commitMessage.String(), true, &d.state, d.epoch) } type copyConfig struct { params instructions.SourcesAndDest excludePatterns []string source llb.State isAddCommand bool cmdToPrint fmt.Stringer chown string chmod string link bool keepGitDir *bool checksum string parents bool location []parser.Range ignoreMatcher *patternmatcher.PatternMatcher opt dispatchOpt unpack *bool } func dispatchMaintainer(d *dispatchState, c *instructions.MaintainerCommand) error { d.image.Author = c.Maintainer return commitToHistory(&d.image, fmt.Sprintf("MAINTAINER %v", c.Maintainer), false, nil, d.epoch) } func dispatchLabel(d *dispatchState, c *instructions.LabelCommand, lint *linter.Linter) error { commitMessage := bytes.NewBufferString("LABEL") if d.image.Config.Labels == nil { d.image.Config.Labels = make(map[string]string, len(c.Labels)) } for _, v := range c.Labels { if v.NoDelim { msg := linter.RuleLegacyKeyValueFormat.Format(c.Name()) lint.Run(&linter.RuleLegacyKeyValueFormat, c.Location(), msg) } d.image.Config.Labels[v.Key] = v.Value commitMessage.WriteString(" " + v.String()) } return commitToHistory(&d.image, commitMessage.String(), false, nil, d.epoch) } func dispatchOnbuild(d *dispatchState, c *instructions.OnbuildCommand) error { d.image.Config.OnBuild = append(d.image.Config.OnBuild, c.Expression) return nil } func dispatchCmd(d *dispatchState, c *instructions.CmdCommand, lint *linter.Linter) error { validateUsedOnce(c, &d.cmd, lint) var args = c.CmdLine if c.PrependShell { if len(d.image.Config.Shell) == 0 { msg := linter.RuleJSONArgsRecommended.Format(c.Name()) lint.Run(&linter.RuleJSONArgsRecommended, c.Location(), msg) } args = withShell(d.image, args) } d.image.Config.Cmd = args d.image.Config.ArgsEscaped = true //nolint:staticcheck // ignore SA1019: field is deprecated in OCI Image spec, but used for backward-compatibility with Docker image spec. return commitToHistory(&d.image, fmt.Sprintf("CMD %q", args), false, nil, d.epoch) } func dispatchEntrypoint(d *dispatchState, c *instructions.EntrypointCommand, lint *linter.Linter) error { validateUsedOnce(c, &d.entrypoint, lint) var args = c.CmdLine if c.PrependShell { if len(d.image.Config.Shell) == 0 { msg := linter.RuleJSONArgsRecommended.Format(c.Name()) lint.Run(&linter.RuleJSONArgsRecommended, c.Location(), msg) } args = withShell(d.image, args) } d.image.Config.Entrypoint = args if !d.cmd.IsSet { d.image.Config.Cmd = nil } return commitToHistory(&d.image, fmt.Sprintf("ENTRYPOINT %q", args), false, nil, d.epoch) } func dispatchHealthcheck(d *dispatchState, c *instructions.HealthCheckCommand, lint *linter.Linter) error { validateUsedOnce(c, &d.healthcheck, lint) d.image.Config.Healthcheck = &dockerspec.HealthcheckConfig{ Test: c.Health.Test, Interval: c.Health.Interval, Timeout: c.Health.Timeout, StartPeriod: c.Health.StartPeriod, StartInterval: c.Health.StartInterval, Retries: c.Health.Retries, } return commitToHistory(&d.image, fmt.Sprintf("HEALTHCHECK %q", d.image.Config.Healthcheck), false, nil, d.epoch) } func dispatchUser(d *dispatchState, c *instructions.UserCommand, commit bool) error { d.state = d.state.User(c.User) d.image.Config.User = c.User if commit { return commitToHistory(&d.image, fmt.Sprintf("USER %v", c.User), false, nil, d.epoch) } return nil } func dispatchVolume(d *dispatchState, c *instructions.VolumeCommand) error { if d.image.Config.Volumes == nil { d.image.Config.Volumes = map[string]struct{}{} } for _, v := range c.Volumes { if v == "" { return errors.New("VOLUME specified can not be an empty string") } d.image.Config.Volumes[v] = struct{}{} } return commitToHistory(&d.image, fmt.Sprintf("VOLUME %v", c.Volumes), false, nil, d.epoch) } func dispatchStopSignal(d *dispatchState, c *instructions.StopSignalCommand) error { if _, err := signal.ParseSignal(c.Signal); err != nil { return err } d.image.Config.StopSignal = c.Signal return commitToHistory(&d.image, fmt.Sprintf("STOPSIGNAL %v", c.Signal), false, nil, d.epoch) } func dispatchShell(d *dispatchState, c *instructions.ShellCommand) error { d.image.Config.Shell = c.Shell return commitToHistory(&d.image, fmt.Sprintf("SHELL %v", c.Shell), false, nil, d.epoch) } func dispatchArg(d *dispatchState, c *instructions.ArgCommand, opt *dispatchOpt) error { commitStrs := make([]string, 0, len(c.Args)) for _, arg := range c.Args { validateNoSecretKey("ARG", arg.Key, c.Location(), opt.lint) _, hasValue := opt.buildArgValues[arg.Key] hasDefault := arg.Value != nil skipArgInfo := false // skip the arg info if the arg is inherited from global scope if !hasDefault && !hasValue { if v, ok := opt.globalArgs.Get(arg.Key); ok { arg.Value = &v skipArgInfo = true hasDefault = false } } if hasValue { v := opt.buildArgValues[arg.Key] arg.Value = &v } else if hasDefault { env := getEnv(d.state) v, unmatched, err := opt.shlex.ProcessWord(*arg.Value, env) reportUnmatchedVariables(c, d.buildArgs, env, unmatched, opt) if err != nil { return err } arg.Value = &v } ai := argInfo{definition: arg, location: c.Location()} if arg.Value != nil { if _, ok := nonEnvArgs[arg.Key]; !ok { d.state = d.state.AddEnv(arg.Key, *arg.Value) } ai.value = *arg.Value } if !skipArgInfo { d.outline.allArgs[arg.Key] = ai } d.outline.usedArgs[arg.Key] = struct{}{} d.buildArgs = append(d.buildArgs, arg) commitStr := arg.Key if arg.Value != nil { commitStr += "=" + *arg.Value } commitStrs = append(commitStrs, commitStr) } return commitToHistory(&d.image, "ARG "+strings.Join(commitStrs, " "), false, nil, d.epoch) } func pathRelativeToWorkingDir(s llb.State, p string, platform ocispecs.Platform) (string, error) { dir, err := s.GetDir(context.TODO(), llb.Platform(platform)) if err != nil { return "", err } p, err = system.CheckSystemDriveAndRemoveDriveLetter(p, platform.OS, true) if err != nil { return "", errors.Wrap(err, "removing drive letter") } if system.IsAbs(p, platform.OS) { return system.NormalizePath("/", p, platform.OS, true) } // add slashes for "" and "." paths // "" is treated as current directory and not necessariy root if p == "." || p == "" { p = "./" } return system.NormalizePath(dir, p, platform.OS, true) } func addEnv(env []string, k, v string) []string { gotOne := false for i, envVar := range env { key, _ := parseKeyValue(envVar) if shell.EqualEnvKeys(key, k) { env[i] = k + "=" + v gotOne = true break } } if !gotOne { env = append(env, k+"="+v) } return env } func parseKeyValue(env string) (string, string) { parts := strings.SplitN(env, "=", 2) v := "" if len(parts) > 1 { v = parts[1] } return parts[0], v } func dfCmd(cmd any) llb.ConstraintsOpt { // TODO: add fmt.Stringer to instructions.Command to remove interface{} var cmdStr string if cmd, ok := cmd.(fmt.Stringer); ok { cmdStr = cmd.String() } if cmd, ok := cmd.(string); ok { cmdStr = cmd } return llb.WithDescription(map[string]string{ "com.docker.dockerfile.v1.command": cmdStr, }) } func runCommandString(args []string, buildArgs []instructions.KeyValuePairOptional, env shell.EnvGetter) string { var tmpBuildEnv []string tmpIdx := map[string]int{} for _, arg := range buildArgs { v, ok := env.Get(arg.Key) if !ok { v = arg.ValueString() } if idx, ok := tmpIdx[arg.Key]; ok { tmpBuildEnv[idx] = arg.Key + "=" + v } else { tmpIdx[arg.Key] = len(tmpBuildEnv) tmpBuildEnv = append(tmpBuildEnv, arg.Key+"="+v) } } if len(tmpBuildEnv) > 0 { tmpBuildEnv = append([]string{fmt.Sprintf("|%d", len(tmpBuildEnv))}, tmpBuildEnv...) } return strings.Join(append(tmpBuildEnv, args...), " ") } func commitToHistory(img *dockerspec.DockerOCIImage, msg string, withLayer bool, st *llb.State, tm *time.Time) error { if st != nil { msg += " # buildkit" } img.History = append(img.History, ocispecs.History{ CreatedBy: msg, Comment: historyComment, EmptyLayer: !withLayer, Created: tm, }) return nil } func allReachableStages(s *dispatchState) map[*dispatchState]struct{} { stages := make(map[*dispatchState]struct{}) addReachableStages(s, stages) return stages } func addReachableStages(s *dispatchState, stages map[*dispatchState]struct{}) { if _, ok := stages[s]; ok { return } stages[s] = struct{}{} if s.base != nil { addReachableStages(s.base, stages) } for d := range s.deps { addReachableStages(d, stages) } } func validateCopySourcePath(src string, cfg *copyConfig) error { if cfg.ignoreMatcher == nil { return nil } cmd := "Copy" if cfg.isAddCommand { cmd = "Add" } ok, err := cfg.ignoreMatcher.MatchesOrParentMatches(src) if err != nil { return err } if ok { msg := linter.RuleCopyIgnoredFile.Format(cmd, src) cfg.opt.lint.Run(&linter.RuleCopyIgnoredFile, cfg.location, msg) } return nil } func validateCircularDependency(states []*dispatchState) error { var visit func(*dispatchState, []instructions.Command) []instructions.Command if states == nil { return nil } visited := make(map[*dispatchState]struct{}) path := make(map[*dispatchState]struct{}) visit = func(state *dispatchState, current []instructions.Command) []instructions.Command { _, ok := visited[state] if ok { return nil } visited[state] = struct{}{} path[state] = struct{}{} for dep, c := range state.deps { next := append(current, c) if _, ok := path[dep]; ok { return next } if c := visit(dep, next); c != nil { return c } } delete(path, state) return nil } for _, state := range states { if cmds := visit(state, nil); cmds != nil { err := errors.Errorf("circular dependency detected on stage: %s", state.stageName) for _, c := range cmds { err = parser.WithLocation(err, c.Location()) } return err } } return nil } func normalizeContextPaths(paths map[string]struct{}) []string { // Avoid a useless allocation if the set of paths is empty. if len(paths) == 0 { return nil } pathSlice := make([]string, 0, len(paths)) for p := range paths { if p == "/" { return nil } pathSlice = append(pathSlice, path.Join(".", p)) } slices.Sort(pathSlice) return pathSlice } // filterPaths returns the local options required to filter an llb.Local // to only the required paths. func filterPaths(paths map[string]struct{}) []llb.LocalOption { if includePaths := normalizeContextPaths(paths); len(includePaths) > 0 { return []llb.LocalOption{llb.FollowPaths(includePaths)} } return nil } func proxyEnvFromBuildArgs(args map[string]string) *llb.ProxyEnv { pe := &llb.ProxyEnv{} isNil := true for k, v := range args { if strings.EqualFold(k, "http_proxy") { pe.HTTPProxy = v isNil = false } if strings.EqualFold(k, "https_proxy") { pe.HTTPSProxy = v isNil = false } if strings.EqualFold(k, "ftp_proxy") { pe.FTPProxy = v isNil = false } if strings.EqualFold(k, "no_proxy") { pe.NoProxy = v isNil = false } if strings.EqualFold(k, "all_proxy") { pe.AllProxy = v isNil = false } } if isNil { return nil } return pe } type mutableOutput struct { llb.Output } func withShell(img dockerspec.DockerOCIImage, args []string) []string { var shell []string if len(img.Config.Shell) > 0 { shell = slices.Clone(img.Config.Shell) } else { shell = defaultShell(img.OS) } return append(shell, strings.Join(args, " ")) } func autoDetectPlatform(img dockerspec.DockerOCIImage, target ocispecs.Platform, supported []ocispecs.Platform) ocispecs.Platform { os := img.OS arch := img.Architecture if target.OS == os && target.Architecture == arch { return target } for _, p := range supported { if p.OS == os && p.Architecture == arch { return p } } return target } func uppercaseCmd(str string) string { p := strings.SplitN(str, " ", 2) p[0] = strings.ToUpper(p[0]) return strings.Join(p, " ") } func processCmdEnv(shlex *shell.Lex, cmd string, env shell.EnvGetter) string { w, _, err := shlex.ProcessWord(cmd, env) if err != nil { return cmd } return w } func prefixCommand(ds *dispatchState, str string, prefixPlatform bool, platform *ocispecs.Platform, env shell.EnvGetter) string { if ds.cmdTotal == 0 { return str } out := "[" if prefixPlatform && platform != nil { out += platforms.FormatAll(*platform) + formatTargetPlatform(*platform, platformFromEnv(env)) + " " } if ds.stageName != "" { out += ds.stageName + " " } ds.cmdIndex++ out += fmt.Sprintf("%*d/%d] ", int(1+math.Log10(float64(ds.cmdTotal))), ds.cmdIndex, ds.cmdTotal) if ds.cmdIsOnBuild { out += "ONBUILD " } return out + str } // formatTargetPlatform formats a secondary platform string for cross compilation cases func formatTargetPlatform(base ocispecs.Platform, target *ocispecs.Platform) string { if target == nil { return "" } if target.OS == "" { target.OS = base.OS } if target.Architecture == "" { target.Architecture = base.Architecture } p := platforms.Normalize(*target) if p.OS == base.OS && p.Architecture != base.Architecture { archVariant := p.Architecture if p.Variant != "" { archVariant += "/" + p.Variant } return "->" + archVariant } if p.OS != base.OS { return "->" + platforms.FormatAll(p) } return "" } // platformFromEnv returns defined platforms based on TARGET* environment variables func platformFromEnv(env shell.EnvGetter) *ocispecs.Platform { var p ocispecs.Platform var set bool for _, key := range env.Keys() { switch key { case "TARGETPLATFORM": v, _ := env.Get(key) p, err := platforms.Parse(v) if err != nil { continue } return &p case "TARGETOS": p.OS, _ = env.Get(key) set = true case "TARGETARCH": p.Architecture, _ = env.Get(key) set = true case "TARGETVARIANT": p.Variant, _ = env.Get(key) set = true } } if !set { return nil } return &p } func location(sm *llb.SourceMap, locations []parser.Range) llb.ConstraintsOpt { loc := make([]*pb.Range, 0, len(locations)) for _, l := range locations { loc = append(loc, &pb.Range{ Start: &pb.Position{ Line: int32(l.Start.Line), Character: int32(l.Start.Character), }, End: &pb.Position{ Line: int32(l.End.Line), Character: int32(l.End.Character), }, }) } return sm.Location(loc) } func summarizeHeredoc(doc string) string { doc = strings.TrimSpace(doc) lines := strings.Split(strings.ReplaceAll(doc, "\r\n", "\n"), "\n") summary := lines[0] if len(lines) > 1 { summary += "..." } return summary } func commonImageNames() []string { repos := []string{ "alpine", "busybox", "centos", "debian", "golang", "ubuntu", "fedora", } out := make([]string, 0, len(repos)*4) for _, name := range repos { out = append(out, name, "docker.io/library"+name, name+":latest", "docker.io/library"+name+":latest") } return out } func isHTTPSource(src string) bool { if !strings.HasPrefix(src, "http://") && !strings.HasPrefix(src, "https://") { return false } return !isGitSource(src) } func isGitSource(src string) bool { // https://github.com/ORG/REPO.git is a git source, not an http source if gitRef, isGit, _ := dfgitutil.ParseGitRef(src); gitRef != nil && isGit { return true } return false } func isEnabledForStage(stage string, value string) bool { if enabled, err := strconv.ParseBool(value); err == nil { return enabled } vv := strings.Split(value, ",") return slices.Contains(vv, stage) } func isSelfConsistentCasing(s string) bool { return s == strings.ToLower(s) || s == strings.ToUpper(s) } func validateCaseMatch(name string, isMajorityLower bool, location []parser.Range, lint *linter.Linter) { var correctCasing string if isMajorityLower && strings.ToLower(name) != name { correctCasing = "lowercase" } else if !isMajorityLower && strings.ToUpper(name) != name { correctCasing = "uppercase" } if correctCasing != "" { msg := linter.RuleConsistentInstructionCasing.Format(name, correctCasing) lint.Run(&linter.RuleConsistentInstructionCasing, location, msg) } } func validateCommandCasing(stages []instructions.Stage, lint *linter.Linter) { var lowerCount, upperCount int for _, stage := range stages { if isSelfConsistentCasing(stage.OrigCmd) { if strings.ToLower(stage.OrigCmd) == stage.OrigCmd { lowerCount++ } else { upperCount++ } } for _, cmd := range stage.Commands { cmdName := cmd.Name() if isSelfConsistentCasing(cmdName) { if strings.ToLower(cmdName) == cmdName { lowerCount++ } else { upperCount++ } } } } isMajorityLower := lowerCount > upperCount for _, stage := range stages { // Here, we check both if the command is consistent per command (ie, "CMD" or "cmd", not "Cmd") // as well as ensuring that the casing is consistent throughout the dockerfile by comparing the // command to the casing of the majority of commands. validateCaseMatch(stage.OrigCmd, isMajorityLower, stage.Location, lint) for _, cmd := range stage.Commands { validateCaseMatch(cmd.Name(), isMajorityLower, cmd.Location(), lint) } } } var reservedStageNames = map[string]struct{}{ "context": {}, "scratch": {}, } func validateStageNames(stages []instructions.Stage, lint *linter.Linter) { stageNames := make(map[string]struct{}) for _, stage := range stages { if stage.Name != "" { if _, ok := reservedStageNames[stage.Name]; ok { msg := linter.RuleReservedStageName.Format(stage.Name) lint.Run(&linter.RuleReservedStageName, stage.Location, msg) } if _, ok := stageNames[stage.Name]; ok { msg := linter.RuleDuplicateStageName.Format(stage.Name) lint.Run(&linter.RuleDuplicateStageName, stage.Location, msg) } stageNames[stage.Name] = struct{}{} } } } func reportUnmatchedVariables(cmd instructions.Command, buildArgs []instructions.KeyValuePairOptional, env shell.EnvGetter, unmatched map[string]struct{}, opt *dispatchOpt) { if len(unmatched) == 0 { return } for _, buildArg := range buildArgs { delete(unmatched, buildArg.Key) } if len(unmatched) == 0 { return } options := env.Keys() for cmdVar := range unmatched { if _, nonEnvOk := nonEnvArgs[cmdVar]; nonEnvOk { continue } match, _ := suggest.Search(cmdVar, options, runtime.GOOS != "windows") msg := linter.RuleUndefinedVar.Format(cmdVar, match) opt.lint.Run(&linter.RuleUndefinedVar, cmd.Location(), msg) } } func mergeLocations(locations ...[]parser.Range) []parser.Range { allRanges := []parser.Range{} for _, ranges := range locations { allRanges = append(allRanges, ranges...) } if len(allRanges) == 0 { return []parser.Range{} } if len(allRanges) == 1 { return allRanges } slices.SortFunc(allRanges, func(a, b parser.Range) int { return a.Start.Line - b.Start.Line }) location := []parser.Range{} currentRange := allRanges[0] for _, r := range allRanges[1:] { if r.Start.Line <= currentRange.End.Line { currentRange.End.Line = max(currentRange.End.Line, r.End.Line) } else { location = append(location, currentRange) currentRange = r } } location = append(location, currentRange) return location } func toPBLocation(sourceIndex int, location []parser.Range) pb.Location { loc := make([]*pb.Range, 0, len(location)) for _, l := range location { loc = append(loc, &pb.Range{ Start: &pb.Position{ Line: int32(l.Start.Line), Character: int32(l.Start.Character), }, End: &pb.Position{ Line: int32(l.End.Line), Character: int32(l.End.Character), }, }) } return pb.Location{ SourceIndex: int32(sourceIndex), Ranges: loc, } } func unusedFromArgsCheckKeys(env shell.EnvGetter, args map[string]argInfo) map[string]struct{} { matched := make(map[string]struct{}) for _, arg := range args { matched[arg.definition.Key] = struct{}{} } for _, k := range env.Keys() { matched[k] = struct{}{} } return matched } func reportUnusedFromArgs(testArgKeys map[string]struct{}, unmatched map[string]struct{}, location []parser.Range, lint *linter.Linter) { var argKeys []string for arg := range testArgKeys { argKeys = append(argKeys, arg) } for arg := range unmatched { if _, ok := testArgKeys[arg]; ok { continue } suggest, _ := suggest.Search(arg, argKeys, true) msg := linter.RuleUndefinedArgInFrom.Format(arg, suggest) lint.Run(&linter.RuleUndefinedArgInFrom, location, msg) } } func reportRedundantTargetPlatform(platformVar string, nameMatch shell.ProcessWordResult, location []parser.Range, env shell.EnvGetter, lint *linter.Linter) { // Only match this rule if there was only one matched name. // It's psosible there were multiple args and that one of them expanded to an empty // string and we don't want to report a warning when that happens. if len(nameMatch.Matched) == 1 && len(nameMatch.Unmatched) == 0 { const targetPlatform = "TARGETPLATFORM" // If target platform is the only environment variable that was substituted and the result // matches the target platform exactly, we can infer that the input was ${TARGETPLATFORM} or // $TARGETPLATFORM. if _, ok := nameMatch.Matched[targetPlatform]; !ok { return } if result, _ := env.Get(targetPlatform); nameMatch.Result == result { msg := linter.RuleRedundantTargetPlatform.Format(platformVar) lint.Run(&linter.RuleRedundantTargetPlatform, location, msg) } } } func reportConstPlatformDisallowed(stageName string, nameMatch shell.ProcessWordResult, location []parser.Range, lint *linter.Linter) { if len(nameMatch.Matched) > 0 || len(nameMatch.Unmatched) > 0 { // Some substitution happened so the platform was not a constant. // Disable checking for this warning. return } // Attempt to parse the platform result. If this fails, then it will fail // later so just ignore. p, err := platforms.Parse(nameMatch.Result) if err != nil { return } // Check if the platform os or architecture is used in the stage name // at all. If it is, then disable this warning. if strings.Contains(stageName, p.OS) || strings.Contains(stageName, p.Architecture) { return } // Report the linter warning. msg := linter.RuleFromPlatformFlagConstDisallowed.Format(nameMatch.Result) lint.Run(&linter.RuleFromPlatformFlagConstDisallowed, location, msg) } type instructionTracker struct { Loc []parser.Range IsSet bool } func (v *instructionTracker) MarkUsed(loc []parser.Range) { v.Loc = loc v.IsSet = true } func validateUsedOnce(c instructions.Command, loc *instructionTracker, lint *linter.Linter) { if loc.IsSet { msg := linter.RuleMultipleInstructionsDisallowed.Format(c.Name()) // Report the location of the previous invocation because it is the one // that will be ignored. lint.Run(&linter.RuleMultipleInstructionsDisallowed, loc.Loc, msg) } loc.MarkUsed(c.Location()) } func wrapSuggestAny(err error, keys map[string]struct{}, options []string) error { for k := range keys { var ok bool ok, err = suggest.WrapErrorMaybe(err, k, options, true) if ok { break } } return err } func validateBaseImagePlatform(name string, expected, actual ocispecs.Platform, location []parser.Range, lint *linter.Linter) { if expected.OS != actual.OS || expected.Architecture != actual.Architecture { expectedStr := platforms.FormatAll(platforms.Normalize(expected)) actualStr := platforms.FormatAll(platforms.Normalize(actual)) msg := linter.RuleInvalidBaseImagePlatform.Format(name, expectedStr, actualStr) lint.Run(&linter.RuleInvalidBaseImagePlatform, location, msg) } } func getSecretsRegex() (*regexp.Regexp, *regexp.Regexp) { // Check for either full value or first/last word. // Examples: api_key, DATABASE_PASSWORD, GITHUB_TOKEN, secret_MESSAGE, AUTH // Case insensitive. secretsRegexpOnce.Do(func() { secretTokens := []string{ "apikey", "auth", "credential", "credentials", "key", "password", "pword", "passwd", "secret", "token", } pattern := `(?i)(?:_|^)(?:` + strings.Join(secretTokens, "|") + `)(?:_|$)` secretsRegexp = regexp.MustCompile(pattern) allowTokens := []string{ "public", } allowPattern := `(?i)(?:_|^)(?:` + strings.Join(allowTokens, "|") + `)(?:_|$)` secretsAllowRegexp = regexp.MustCompile(allowPattern) }) return secretsRegexp, secretsAllowRegexp } func validateNoSecretKey(instruction, key string, location []parser.Range, lint *linter.Linter) { deny, allow := getSecretsRegex() if deny.MatchString(key) && !allow.MatchString(key) { msg := linter.RuleSecretsUsedInArgOrEnv.Format(instruction, key) lint.Run(&linter.RuleSecretsUsedInArgOrEnv, location, msg) } } func validateBaseImagesWithDefaultArgs(stages []instructions.Stage, shlex *shell.Lex, env *llb.EnvList, argCmds []instructions.ArgCommand, lint *linter.Linter) { // Build the arguments as if no build options were given // and using only defaults. args, _, err := buildMetaArgs(env, shlex, argCmds, nil) if err != nil { // Abandon running the linter. We'll likely fail after this point // with the same error but we shouldn't error here inside // of the linting check. return } for _, st := range stages { nameMatch, err := shlex.ProcessWordWithMatches(st.BaseName, args) if err != nil { return } // Verify the image spec is potentially valid. if _, err := reference.ParseNormalizedNamed(nameMatch.Result); err != nil { msg := linter.RuleInvalidDefaultArgInFrom.Format(st.BaseName) lint.Run(&linter.RuleInvalidDefaultArgInFrom, st.Location, msg) } } } func buildMetaArgs(args *llb.EnvList, shlex *shell.Lex, argCommands []instructions.ArgCommand, buildArgs map[string]string) (*llb.EnvList, map[string]argInfo, error) { allArgs := make(map[string]argInfo) for _, cmd := range argCommands { for _, kp := range cmd.Args { info := argInfo{definition: kp, location: cmd.Location()} if v, ok := buildArgs[kp.Key]; !ok { if kp.Value != nil { result, err := shlex.ProcessWordWithMatches(*kp.Value, args) if err != nil { return nil, nil, parser.WithLocation(err, cmd.Location()) } kp.Value = &result.Result info.deps = result.Matched if _, ok := result.Matched[kp.Key]; ok { delete(info.deps, kp.Key) if old, ok := allArgs[kp.Key]; ok { for k := range old.deps { if info.deps == nil { info.deps = make(map[string]struct{}) } info.deps[k] = struct{}{} } } } } } else { kp.Value = &v } if kp.Value != nil { args = args.AddOrReplace(kp.Key, *kp.Value) info.value = *kp.Value } allArgs[kp.Key] = info } } return args, allArgs, nil } func rangeStartEnd(r []parser.Range) (int, int) { if len(r) == 0 { return 0, 0 } start := math.MaxInt32 end := 0 for _, rng := range r { if rng.Start.Line < start { start = rng.Start.Line } if rng.End.Line > end { end = rng.End.Line } } return start, end } type emptyEnvs struct{} func (emptyEnvs) Get(string) (string, bool) { return "", false } func (emptyEnvs) Keys() []string { return nil }