diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index 3f0282c21a3fe..301de585b27da 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -291,9 +291,18 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS 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, metaArgsKeys(optMetaArgs)) + return nil, err + } + p, err := platforms.Parse(platMatch.Result) if err != nil { - return nil, parser.WithLocation(errors.Wrapf(err, "failed to parse platform %s", platMatch.Result), st.Location) + err = parser.WithLocation(err, st.Location) + err = wrapSuggestAny(err, platMatch.Unmatched, metaArgsKeys(optMetaArgs)) + return nil, parser.WithLocation(errors.Wrapf(err, "failed to parse platform %s", v), st.Location) } for k := range platMatch.Matched { @@ -687,6 +696,14 @@ func metaArgsToMap(metaArgs []instructions.KeyValuePairOptional) map[string]stri return m } +func metaArgsKeys(metaArgs []instructions.KeyValuePairOptional) []string { + s := make([]string, 0, len(metaArgs)) + for _, arg := range metaArgs { + s = append(s, arg.Key) + } + return s +} + func toCommand(ic instructions.Command, allDispatchStates *dispatchStates) (command, error) { cmd := command{Command: ic} if c, ok := ic.(*instructions.CopyCommand); ok { @@ -2178,3 +2195,14 @@ func validateUsedOnce(c instructions.Command, loc *instructionTracker, warn lint } loc.MarkUsed(c.Location()) } + +func wrapSuggestAny(err error, keys map[string]struct{}, options []string) error { + for k := range keys { + var ok bool + err, ok = suggest.WrapErrorMaybe(err, k, options, true) + if ok { + break + } + } + return err +} diff --git a/frontend/dockerfile/dockerfile_lint_test.go b/frontend/dockerfile/dockerfile_lint_test.go index 186b63ad88e18..2969ed730532f 100644 --- a/frontend/dockerfile/dockerfile_lint_test.go +++ b/frontend/dockerfile/dockerfile_lint_test.go @@ -467,11 +467,33 @@ COPY Dockerfile . Line: 2, }, }, - StreamBuildErr: "failed to solve: failed to parse platform : \"\" is an invalid component of \"\": platform specifier component must match \"^[A-Za-z0-9_-]+$\": invalid argument", - UnmarshalBuildErr: "failed to parse platform : \"\" is an invalid component of \"\": platform specifier component must match \"^[A-Za-z0-9_-]+$\": invalid argument", + StreamBuildErr: "failed to solve: empty platform value from expression $BULIDPLATFORM (did you mean BUILDPLATFORM?)", + UnmarshalBuildErr: "empty platform value from expression $BULIDPLATFORM (did you mean BUILDPLATFORM?)", BuildErrLocation: 2, }) + dockerfile = []byte(` +ARG MY_OS=linux +ARG MY_ARCH=amd64 +FROM --platform=linux/${MYARCH} busybox +COPY Dockerfile . + `) + checkLinterWarnings(t, sb, &lintTestParams{ + Dockerfile: dockerfile, + Warnings: []expectedLintWarning{ + { + RuleName: "UndeclaredArgInFrom", + Description: "FROM command must use declared ARGs", + Detail: "FROM argument 'MYARCH' is not declared", + Level: 1, + Line: 4, + }, + }, + StreamBuildErr: "failed to solve: failed to parse platform linux/: \"\" is an invalid component of \"linux/\": platform specifier component must match \"^[A-Za-z0-9_-]+$\": invalid argument (did you mean MY_ARCH?)", + UnmarshalBuildErr: "failed to parse platform linux/: \"\" is an invalid component of \"linux/\": platform specifier component must match \"^[A-Za-z0-9_-]+$\": invalid argument (did you mean MY_ARCH?)", + BuildErrLocation: 4, + }) + dockerfile = []byte(` ARG tag=latest FROM busybox:${tag}${version} AS b diff --git a/util/suggest/error.go b/util/suggest/error.go index 0ad7b5c8679af..1c3d41e6f9654 100644 --- a/util/suggest/error.go +++ b/util/suggest/error.go @@ -8,8 +8,13 @@ import ( // WrapError wraps error with a suggestion for fixing it func WrapError(err error, val string, options []string, caseSensitive bool) error { + err, _ = WrapErrorMaybe(err, val, options, caseSensitive) + return err +} + +func WrapErrorMaybe(err error, val string, options []string, caseSensitive bool) (error, bool) { if err == nil { - return nil + return nil, false } orig := val if !caseSensitive { @@ -23,7 +28,7 @@ func WrapError(err error, val string, options []string, caseSensitive bool) erro } if val == opt { // exact match means error was unrelated to the value - return err + return err, false } dist := levenshtein.Distance(val, opt, nil) if dist < mindist { @@ -37,13 +42,13 @@ func WrapError(err error, val string, options []string, caseSensitive bool) erro } if match == "" { - return err + return err, false } return &suggestError{ err: err, match: match, - } + }, true } type suggestError struct {