diff --git a/bake/bake.go b/bake/bake.go index e56980e7d52..22be647197b 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -2,6 +2,7 @@ package bake import ( "context" + "encoding" "io" "os" "path" @@ -496,7 +497,9 @@ func (c Config) loadLinks(name string, t *Target, m map[string]*Target, o map[st if err != nil { return err } - t2.Outputs = []string{"type=cacheonly"} + t2.Outputs = []*buildflags.ExportEntry{ + {Type: "cacheonly"}, + } t2.linked = true m[target] = t2 } @@ -695,59 +698,61 @@ type Target struct { // Inherits is the only field that cannot be overridden with --set Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"` - Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"` - Attest []string `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"` - Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"` - Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"` - Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"` - DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"` - Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"` - Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"` - Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"` - CacheFrom []string `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"` - CacheTo []string `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"` - Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"` - Secrets []string `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"` - SSH []string `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"` - Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"` - Outputs []string `json:"output,omitempty" hcl:"output,optional" cty:"output"` - Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"` - NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"` - NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"` - NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"` - ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"` - Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"` - Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"` - Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"` + Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"` + Attest []string `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"` + Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"` + Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"` + Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"` + DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"` + Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"` + Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"` + Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"` + CacheFrom []string `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"` + CacheTo []string `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"` + Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"` + Secrets []string `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"` + SSH []string `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"` + Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"` + Outputs []*buildflags.ExportEntry `json:"output,omitempty" hcl:"output,optional" cty:"output"` + Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"` + NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"` + NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"` + NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"` + ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"` + Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"` + Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"` + Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"` // IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md. // linked is a private field to mark a target used as a linked one linked bool } -var _ hclparser.WithEvalContexts = &Target{} -var _ hclparser.WithGetName = &Target{} -var _ hclparser.WithEvalContexts = &Group{} -var _ hclparser.WithGetName = &Group{} +var ( + _ hclparser.WithEvalContexts = &Target{} + _ hclparser.WithGetName = &Target{} + _ hclparser.WithEvalContexts = &Group{} + _ hclparser.WithGetName = &Group{} +) func (t *Target) normalize() { - t.Annotations = removeDupes(t.Annotations) + t.Annotations = removeDupesStr(t.Annotations) t.Attest = removeAttestDupes(t.Attest) - t.Tags = removeDupes(t.Tags) - t.Secrets = removeDupes(t.Secrets) - t.SSH = removeDupes(t.SSH) - t.Platforms = removeDupes(t.Platforms) - t.CacheFrom = removeDupes(t.CacheFrom) - t.CacheTo = removeDupes(t.CacheTo) + t.Tags = removeDupesStr(t.Tags) + t.Secrets = removeDupesStr(t.Secrets) + t.SSH = removeDupesStr(t.SSH) + t.Platforms = removeDupesStr(t.Platforms) + t.CacheFrom = removeDupesStr(t.CacheFrom) + t.CacheTo = removeDupesStr(t.CacheTo) t.Outputs = removeDupes(t.Outputs) - t.NoCacheFilter = removeDupes(t.NoCacheFilter) - t.Ulimits = removeDupes(t.Ulimits) + t.NoCacheFilter = removeDupesStr(t.NoCacheFilter) + t.Ulimits = removeDupesStr(t.Ulimits) if t.NetworkMode != nil && *t.NetworkMode == "host" { t.Entitlements = append(t.Entitlements, "network.host") } - t.Entitlements = removeDupes(t.Entitlements) + t.Entitlements = removeDupesStr(t.Entitlements) for k, v := range t.Contexts { if v == "" { @@ -904,7 +909,11 @@ func (t *Target) AddOverrides(overrides map[string]Override) error { case "platform": t.Platforms = o.ArrValue case "output": - t.Outputs = o.ArrValue + outputs, err := parseArrValue[buildflags.ExportEntry](o.ArrValue) + if err != nil { + return errors.Wrap(err, "invalid value for outputs") + } + t.Outputs = outputs case "entitlements": t.Entitlements = append(t.Entitlements, o.ArrValue...) case "annotations": @@ -1354,10 +1363,11 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { } bo.CacheTo = controllerapi.CreateCaches(cacheExports) - outputs, err := buildflags.ParseExports(t.Outputs) - if err != nil { - return nil, err + outputs := make([]*controllerapi.ExportEntry, len(t.Outputs)) + for i, output := range t.Outputs { + outputs[i] = output.ToPB() } + bo.Exports, err = controllerapi.CreateExports(outputs) if err != nil { return nil, err @@ -1403,7 +1413,38 @@ func defaultTarget() *Target { return &Target{} } -func removeDupes(s []string) []string { +type hasEqual[E any] interface { + Equal(other E) bool +} + +func removeDupes[E hasEqual[E]](s []E) []E { + // Move backwards through the slice. + // For each element, any elements after + // the current element are unique. + // If we find our current element conflicts + // with an existing element, then we swap + // the offender with the end of the slice + // and chop it off. + + // Start at the second to last element. + // The last element is always unique. + for i := len(s) - 2; i > 0; i-- { + elem := s[i] + // Check for duplicates after our current element. + for j := i + 1; j < len(s); j++ { + if elem.Equal(s[j]) { + // Found a duplicate, exchange the + // duplicate with the last element. + s[j], s[len(s)-1] = s[len(s)-1], s[j] + s = s[:len(s)-1] + break + } + } + } + return s +} + +func removeDupesStr(s []string) []string { i := 0 seen := make(map[string]struct{}, len(s)) for _, v := range s { @@ -1464,61 +1505,75 @@ func parseOutputType(str string) string { return "" } -func setPushOverride(outputs []string, push bool) []string { - var out []string +func setPushOverride(outputs []*buildflags.ExportEntry, push bool) []*buildflags.ExportEntry { + if !push { + // Disable push for any relevant export types + for i := 0; i < len(outputs); i++ { + output := outputs[i] + if output.Type == "registry" { + // Filter out registry output type + outputs[i], outputs[len(outputs)-1] = outputs[len(outputs)-1], outputs[i] + outputs = outputs[:len(outputs)-1] + } else if output.Type == "image" { + // Override push attribute + output.Attrs["push"] = "false" + } + } + return outputs + } + + // Force push to be enabled setPush := true for _, output := range outputs { - typ := parseOutputType(output) - if typ == "image" || typ == "registry" { - // no need to set push if image or registry types already defined + if output.Type != "docker" { + // If there is an output type that is not docker, don't set "push" setPush = false - if typ == "registry" { - if !push { - // don't set registry output if "push" is false - continue - } - // no need to set "push" attribute to true for registry - out = append(out, output) - continue - } - out = append(out, output+",push="+strconv.FormatBool(push)) - } else { - if typ != "docker" { - // if there is any output that is not docker, don't set "push" - setPush = false - } - out = append(out, output) + } + + // Set push attribute for image + if output.Type == "image" { + output.Attrs["push"] = "true" } } - if push && setPush { - out = append(out, "type=image,push=true") + + if setPush { + // No existing output that pushes so add one + outputs = append(outputs, &buildflags.ExportEntry{ + Type: "image", + Attrs: map[string]string{ + "push": "true", + }, + }) } - return out + return outputs } -func setLoadOverride(outputs []string, load bool) []string { +func setLoadOverride(outputs []*buildflags.ExportEntry, load bool) []*buildflags.ExportEntry { if !load { return outputs } + setLoad := true for _, output := range outputs { - if typ := parseOutputType(output); typ == "docker" { - if v := parseOutput(output); v != nil { - // dest set means we want to output as tar so don't set load - if _, ok := v["dest"]; !ok { - setLoad = false - break - } + switch output.Type { + case "docker": + // if dest is not set, we can reuse this entry and do not need to set load + if output.Destination == "" { + setLoad = false } - } else if typ != "image" && typ != "registry" && typ != "oci" { + case "image", "registry", "oci": + // Ignore + default: // if there is any output that is not an image, registry // or oci, don't set "load" similar to push override setLoad = false - break } } + if setLoad { - outputs = append(outputs, "type=docker") + outputs = append(outputs, &buildflags.ExportEntry{ + Type: "docker", + }) } return outputs } @@ -1558,3 +1613,20 @@ func toNamedContexts(m map[string]string) map[string]build.NamedContext { } return m2 } + +type arrValue[B any] interface { + encoding.TextUnmarshaler + *B +} + +func parseArrValue[T any, PT arrValue[T]](s []string) ([]*T, error) { + outputs := make([]*T, 0, len(s)) + for _, text := range s { + output := new(T) + if err := PT(output).UnmarshalText([]byte(text)); err != nil { + return nil, err + } + outputs = append(outputs, output) + } + return outputs, nil +} diff --git a/bake/compose.go b/bake/compose.go index 6036e53473a..ecd08eee028 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -12,6 +12,7 @@ import ( "github.com/compose-spec/compose-go/v2/dotenv" "github.com/compose-spec/compose-go/v2/loader" composetypes "github.com/compose-spec/compose-go/v2/types" + "github.com/docker/buildx/util/buildflags" dockeropts "github.com/docker/cli/opts" "github.com/docker/go-units" "github.com/pkg/errors" @@ -292,8 +293,10 @@ type xbake struct { // https://github.com/docker/docs/blob/main/content/build/bake/compose-file.md#extension-field-with-x-bake } -type stringMap map[string]string -type stringArray []string +type ( + stringMap map[string]string + stringArray []string +) func (sa *stringArray) UnmarshalYAML(unmarshal func(interface{}) error) error { var multi []string @@ -345,7 +348,11 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error { t.Platforms = dedupSlice(append(t.Platforms, xb.Platforms...)) } if len(xb.Outputs) > 0 { - t.Outputs = dedupSlice(append(t.Outputs, xb.Outputs...)) + outputs, err := parseArrValue[buildflags.ExportEntry](xb.Outputs) + if err != nil { + return err + } + t.Outputs = removeDupes(append(t.Outputs, outputs...)) } if xb.Pull != nil { t.Pull = xb.Pull diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go index fe7dc772dd7..5aebb34205d 100644 --- a/bake/hclparser/hclparser.go +++ b/bake/hclparser/hclparser.go @@ -448,7 +448,7 @@ func (p *parser) resolveBlock(block *hcl.Block, target *hcl.BodySchema) (err err } // decode! - diag = gohcl.DecodeBody(body(), ectx, output.Interface()) + diag = decodeBody(body(), ectx, output.Interface()) if diag.HasErrors() { return diag } @@ -470,7 +470,7 @@ func (p *parser) resolveBlock(block *hcl.Block, target *hcl.BodySchema) (err err } // store the result into the evaluation context (so it can be referenced) - outputType, err := gocty.ImpliedType(output.Interface()) + outputType, err := ImpliedType(output.Interface()) if err != nil { return err } @@ -947,3 +947,8 @@ func key(ks ...any) uint64 { } return hash.Sum64() } + +func decodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { + dec := gohcl.DecodeOptions{ImpliedType: ImpliedType} + return dec.DecodeBody(body, ctx, val) +} diff --git a/bake/hclparser/type_implied.go b/bake/hclparser/type_implied.go new file mode 100644 index 00000000000..1bc6bb11f44 --- /dev/null +++ b/bake/hclparser/type_implied.go @@ -0,0 +1,174 @@ +// MIT License +// +// Copyright (c) 2017-2018 Martin Atkins +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package hclparser + +import ( + "math/big" + "reflect" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/set" +) + +// ImpliedType takes an arbitrary Go value (as an interface{}) and attempts +// to find a suitable cty.Type instance that could be used for a conversion +// with ToCtyValue. +// +// This allows -- for simple situations at least -- types to be defined just +// once in Go and the cty types derived from the Go types, but in the process +// it makes some assumptions that may be undesirable so applications are +// encouraged to build their cty types directly if exacting control is +// required. +// +// Not all Go types can be represented as cty types, so an error may be +// returned which is usually considered to be a bug in the calling program. +// In particular, ImpliedType will never use capsule types in its returned +// type, because it cannot know the capsule types supported by the calling +// program. +func ImpliedType(gv interface{}) (cty.Type, error) { + rt := reflect.TypeOf(gv) + var path cty.Path + return impliedType(rt, path) +} + +func impliedType(rt reflect.Type, path cty.Path) (cty.Type, error) { + if ety, err := impliedTypeExt(rt, path); err == nil { + return ety, nil + } + + switch rt.Kind() { + + case reflect.Ptr: + return impliedType(rt.Elem(), path) + + // Primitive types + case reflect.Bool: + return cty.Bool, nil + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return cty.Number, nil + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return cty.Number, nil + case reflect.Float32, reflect.Float64: + return cty.Number, nil + case reflect.String: + return cty.String, nil + + // Collection types + case reflect.Slice: + path := append(path, cty.IndexStep{Key: cty.UnknownVal(cty.Number)}) + ety, err := impliedType(rt.Elem(), path) + if err != nil { + return cty.NilType, err + } + return cty.List(ety), nil + case reflect.Map: + if !stringType.AssignableTo(rt.Key()) { + return cty.NilType, path.NewErrorf("no cty.Type for %s (must have string keys)", rt) + } + path := append(path, cty.IndexStep{Key: cty.UnknownVal(cty.String)}) + ety, err := impliedType(rt.Elem(), path) + if err != nil { + return cty.NilType, err + } + return cty.Map(ety), nil + + // Structural types + case reflect.Struct: + return impliedStructType(rt, path) + + default: + return cty.NilType, path.NewErrorf("no cty.Type for %s", rt) + } +} + +func impliedStructType(rt reflect.Type, path cty.Path) (cty.Type, error) { + if valueType.AssignableTo(rt) { + // Special case: cty.Value represents cty.DynamicPseudoType, for + // type conformance checking. + return cty.DynamicPseudoType, nil + } + + fieldIdxs := structTagIndices(rt) + if len(fieldIdxs) == 0 { + return cty.NilType, path.NewErrorf("no cty.Type for %s (no cty field tags)", rt) + } + + atys := make(map[string]cty.Type, len(fieldIdxs)) + + { + // Temporary extension of path for attributes + path := append(path, nil) + + for k, fi := range fieldIdxs { + path[len(path)-1] = cty.GetAttrStep{Name: k} + + ft := rt.Field(fi).Type + aty, err := impliedType(ft, path) + if err != nil { + return cty.NilType, err + } + + atys[k] = aty + } + } + + return cty.Object(atys), nil +} + +var ( + valueType = reflect.TypeOf(cty.Value{}) + typeType = reflect.TypeOf(cty.Type{}) +) + +var setType = reflect.TypeOf(set.Set[interface{}]{}) + +var ( + bigFloatType = reflect.TypeOf(big.Float{}) + bigIntType = reflect.TypeOf(big.Int{}) +) + +var emptyInterfaceType = reflect.TypeOf(interface{}(nil)) + +var stringType = reflect.TypeOf("") + +// structTagIndices interrogates the fields of the given type (which must +// be a struct type, or we'll panic) and returns a map from the cty +// attribute names declared via struct tags to the indices of the +// fields holding those tags. +// +// This function will panic if two fields within the struct are tagged with +// the same cty attribute name. +func structTagIndices(st reflect.Type) map[string]int { + ct := st.NumField() + ret := make(map[string]int, ct) + + for i := 0; i < ct; i++ { + field := st.Field(i) + attrName := field.Tag.Get("cty") + if attrName != "" { + ret[attrName] = i + } + } + + return ret +} diff --git a/bake/hclparser/type_implied_ext.go b/bake/hclparser/type_implied_ext.go new file mode 100644 index 00000000000..ecb73b7a6c6 --- /dev/null +++ b/bake/hclparser/type_implied_ext.go @@ -0,0 +1,49 @@ +package hclparser + +import ( + "reflect" + "sync" + + "github.com/containerd/errdefs" + "github.com/zclconf/go-cty/cty" +) + +type FromCtyValue interface { + FromCtyValue(in cty.Value, path cty.Path) error +} + +func impliedTypeExt(rt reflect.Type, _ cty.Path) (cty.Type, error) { + if rt.AssignableTo(fromCtyValueType) { + return fromCtyValueCapsuleType(rt), nil + } + return cty.NilType, errdefs.ErrNotImplemented +} + +var ( + fromCtyValueType = reflect.TypeFor[FromCtyValue]() + fromCtyValueTypes sync.Map +) + +func fromCtyValueCapsuleType(rt reflect.Type) cty.Type { + if val, loaded := fromCtyValueTypes.Load(rt); loaded { + return val.(cty.Type) + } + + // First time used. + ety := cty.CapsuleWithOps(rt.Name(), rt.Elem(), &cty.CapsuleOps{ + ConversionTo: func(_ cty.Type) func(cty.Value, cty.Path) (interface{}, error) { + return func(in cty.Value, p cty.Path) (interface{}, error) { + rv := reflect.New(rt.Elem()).Interface() + if err := rv.(FromCtyValue).FromCtyValue(in, p); err != nil { + return nil, err + } + return rv, nil + } + }, + }) + + // Attempt to store the new type. Use whichever was loaded first + // in the case of a race condition. + val, _ := fromCtyValueTypes.LoadOrStore(rt, ety) + return val.(cty.Type) +} diff --git a/go.mod b/go.mod index 8ec28adf452..c4391f79ccf 100644 --- a/go.mod +++ b/go.mod @@ -181,3 +181,5 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) + +replace github.com/hashicorp/hcl/v2 => github.com/jsternberg/hcl/v2 v2.22.1-0.20241028205650-d84f8d55e419 diff --git a/go.sum b/go.sum index cb59d3b4452..075716cd6e8 100644 --- a/go.sum +++ b/go.sum @@ -239,8 +239,6 @@ github.com/hashicorp/go-cty-funcs v0.0.0-20230405223818-a090f58aa992 h1:fYOrSfO5 github.com/hashicorp/go-cty-funcs v0.0.0-20230405223818-a090f58aa992/go.mod h1:Abjk0jbRkDaNCzsRhOv2iDCofYpX1eVsjozoiK63qLA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc= -github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= @@ -260,6 +258,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jsternberg/hcl/v2 v2.22.1-0.20241028205650-d84f8d55e419 h1:mSM5rQQ+D4AUzULSkIa53l80PLphZcXic/d+VwSEvvs= +github.com/jsternberg/hcl/v2 v2.22.1-0.20241028205650-d84f8d55e419/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -464,8 +464,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/zclconf/go-cty v1.4.0/go.mod h1:nHzOclRkoj++EU9ZjSrZvRG0BXIWt8c7loYc0qXAFGQ= github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= diff --git a/util/buildflags/cty.go b/util/buildflags/cty.go new file mode 100644 index 00000000000..39ad4b110ff --- /dev/null +++ b/util/buildflags/cty.go @@ -0,0 +1,44 @@ +package buildflags + +import ( + "encoding" + "encoding/json" + + "github.com/moby/buildkit/errdefs" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" +) + +func (e *ExportEntry) FromCtyValue(in cty.Value, p cty.Path) error { + return fromCtyValue(in, p, e) +} + +func fromCtyValue[V encoding.TextUnmarshaler](in cty.Value, p cty.Path, v V) error { + // Attempt to read as a cty.Map(cty.String) first as that's our primary form. + if conv, err := convert.Convert(in, cty.Map(cty.String)); err == nil { + m := make(map[string]string, conv.LengthInt()) + for name, val := range conv.AsValueMap() { + m[name] = val.AsString() + } + + data, err := json.Marshal(v) + if err != nil { + return errdefs.Internal(err) + } + + if err := json.Unmarshal(data, v); err != nil { + return errdefs.Internal(err) + } + return nil + } + + // Also supports a string input. + if in, err := convert.Convert(in, cty.String); err == nil { + return v.UnmarshalText([]byte(in.AsString())) + } + + // Return a type mismatch. We want to use the map[string]string + // type since that's the primary type. + msg := convert.MismatchMessage(in.Type(), cty.Map(cty.String)) + return p.NewErrorf("%s", msg) +} diff --git a/util/buildflags/export.go b/util/buildflags/export.go index 37f3c274c30..c0619b86b3a 100644 --- a/util/buildflags/export.go +++ b/util/buildflags/export.go @@ -1,6 +1,8 @@ package buildflags import ( + "encoding/json" + "maps" "regexp" "strings" @@ -13,67 +15,131 @@ import ( "github.com/tonistiigi/go-csvvalue" ) -func ParseExports(inp []string) ([]*controllerapi.ExportEntry, error) { - var outs []*controllerapi.ExportEntry - if len(inp) == 0 { - return nil, nil +type ExportEntry struct { + Type string `json:"type"` + Attrs map[string]string `json:"attrs,omitempty"` + Destination string `json:"dest,omitempty"` +} + +func (e *ExportEntry) Equal(other *ExportEntry) bool { + if e.Type != other.Type || e.Destination != other.Destination { + return false + } + return maps.Equal(e.Attrs, other.Attrs) +} + +func (e *ExportEntry) ToPB() *controllerapi.ExportEntry { + attrs := make(map[string]string, len(e.Attrs)) + for k, v := range attrs { + attrs[k] = v + } + return &controllerapi.ExportEntry{ + Type: e.Type, + Attrs: e.Attrs, + Destination: e.Destination, + } +} + +func (e *ExportEntry) MarshalJSON() ([]byte, error) { + m := make(map[string]string, len(e.Attrs)+2) + for k, v := range e.Attrs { + m[k] = v + } + m["type"] = e.Type + if e.Destination != "" { + m["dest"] = e.Destination + } + return json.Marshal(m) +} + +func (e *ExportEntry) UnmarshalJSON(data []byte) error { + var m map[string]string + if err := json.Unmarshal(data, &m); err != nil { + return err + } + + e.Type = m["type"] + delete(m, "type") + + e.Destination = m["dest"] + delete(m, "dest") + + e.Attrs = m + return e.Validate() +} + +func (e *ExportEntry) UnmarshalText(text []byte) error { + s := string(text) + fields, err := csvvalue.Fields(s, nil) + if err != nil { + return err } - for _, s := range inp { - fields, err := csvvalue.Fields(s, nil) - if err != nil { - return nil, err - } - out := controllerapi.ExportEntry{ - Attrs: map[string]string{}, + // Clear the target entry. + e.Type = "" + e.Attrs = map[string]string{} + e.Destination = "" + + if len(fields) == 1 && fields[0] == s && !strings.HasPrefix(s, "type=") { + if s != "-" { + e.Type = client.ExporterLocal + e.Destination = s + return nil } - if len(fields) == 1 && fields[0] == s && !strings.HasPrefix(s, "type=") { - if s != "-" { - outs = append(outs, &controllerapi.ExportEntry{ - Type: client.ExporterLocal, - Destination: s, - }) - continue + + e.Type = client.ExporterTar + e.Destination = s + } + + if e.Type == "" { + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + if len(parts) != 2 { + return errors.Errorf("invalid value %s", field) } - out = controllerapi.ExportEntry{ - Type: client.ExporterTar, - Destination: s, + key := strings.TrimSpace(strings.ToLower(parts[0])) + value := parts[1] + switch key { + case "type": + e.Type = value + default: + e.Attrs[key] = value } } + } - if out.Type == "" { - for _, field := range fields { - parts := strings.SplitN(field, "=", 2) - if len(parts) != 2 { - return nil, errors.Errorf("invalid value %s", field) - } - key := strings.TrimSpace(strings.ToLower(parts[0])) - value := parts[1] - switch key { - case "type": - out.Type = value - default: - out.Attrs[key] = value - } - } - } - if out.Type == "" { - return nil, errors.Errorf("type is required for output") + if e.Type == "registry" { + e.Type = client.ExporterImage + if _, ok := e.Attrs["push"]; !ok { + e.Attrs["push"] = "true" } + } - if out.Type == "registry" { - out.Type = client.ExporterImage - if _, ok := out.Attrs["push"]; !ok { - out.Attrs["push"] = "true" - } - } + if dest, ok := e.Attrs["dest"]; ok { + e.Destination = dest + delete(e.Attrs, "dest") + } + return e.Validate() +} - if dest, ok := out.Attrs["dest"]; ok { - out.Destination = dest - delete(out.Attrs, "dest") - } +func (e *ExportEntry) Validate() error { + if e.Type == "" { + return errors.Errorf("type is required for output") + } + return nil +} - outs = append(outs, &out) +func ParseExports(inp []string) ([]*controllerapi.ExportEntry, error) { + var outs []*controllerapi.ExportEntry + if len(inp) == 0 { + return nil, nil + } + for _, s := range inp { + var out ExportEntry + if err := out.UnmarshalText([]byte(s)); err != nil { + return nil, err + } + outs = append(outs, out.ToPB()) } return outs, nil } diff --git a/vendor/github.com/hashicorp/hcl/v2/CHANGELOG.md b/vendor/github.com/hashicorp/hcl/v2/CHANGELOG.md index 2eebedbc76f..b2ff2631d2c 100644 --- a/vendor/github.com/hashicorp/hcl/v2/CHANGELOG.md +++ b/vendor/github.com/hashicorp/hcl/v2/CHANGELOG.md @@ -1,5 +1,22 @@ # HCL Changelog +## v2.22.0 (August 26, 2024) + +### Enhancements + +* feat: return an ExprSyntaxError for invalid references that end in a dot ([#692](https://github.com/hashicorp/hcl/pull/692)) + +## v2.21.0 (June 19, 2024) + +### Enhancements + +* Introduce `ParseTraversalPartial`, which allows traversals that include the splat (`[*]`) index operator. ([#673](https://github.com/hashicorp/hcl/pull/673)) +* ext/dynblock: Now accepts marked values in `for_each`, and will transfer those marks (as much as technically possible) to values in the generated blocks. ([#679](https://github.com/hashicorp/hcl/pull/679)) + +### Bugs Fixed + +* Expression evaluation will no longer panic if the splat operator is applied to an unknown value that has cty marks. ([#678](https://github.com/hashicorp/hcl/pull/678)) + ## v2.20.1 (March 26, 2024) ### Bugs Fixed diff --git a/vendor/github.com/hashicorp/hcl/v2/gohcl/decode.go b/vendor/github.com/hashicorp/hcl/v2/gohcl/decode.go index 2d1776a36c6..d5139a7940a 100644 --- a/vendor/github.com/hashicorp/hcl/v2/gohcl/decode.go +++ b/vendor/github.com/hashicorp/hcl/v2/gohcl/decode.go @@ -7,13 +7,29 @@ import ( "fmt" "reflect" - "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/gocty" ) +// DecodeOptions allows customizing sections of the decoding process. +type DecodeOptions struct { + ImpliedType func(gv interface{}) (cty.Type, error) + Convert func(in cty.Value, want cty.Type) (cty.Value, error) +} + +func (o DecodeOptions) DecodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { + o = o.withDefaults() + + rv := reflect.ValueOf(val) + if rv.Kind() != reflect.Ptr { + panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String())) + } + + return o.decodeBodyToValue(body, ctx, rv.Elem()) +} + // DecodeBody extracts the configuration within the given body into the given // value. This value must be a non-nil pointer to either a struct or // a map, where in the former case the configuration will be decoded using @@ -31,27 +47,22 @@ import ( // may still be accessed by a careful caller for static analysis and editor // integration use-cases. func DecodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { - rv := reflect.ValueOf(val) - if rv.Kind() != reflect.Ptr { - panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String())) - } - - return decodeBodyToValue(body, ctx, rv.Elem()) + return DecodeOptions{}.DecodeBody(body, ctx, val) } -func decodeBodyToValue(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { +func (o DecodeOptions) decodeBodyToValue(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { et := val.Type() switch et.Kind() { case reflect.Struct: - return decodeBodyToStruct(body, ctx, val) + return o.decodeBodyToStruct(body, ctx, val) case reflect.Map: - return decodeBodyToMap(body, ctx, val) + return o.decodeBodyToMap(body, ctx, val) default: panic(fmt.Sprintf("target value must be pointer to struct or map, not %s", et.String())) } } -func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { +func (o DecodeOptions) decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { schema, partial := ImpliedBodySchema(val.Interface()) var content *hcl.BodyContent @@ -77,7 +88,7 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) fieldV.Set(reflect.ValueOf(body)) default: - diags = append(diags, decodeBodyToValue(body, ctx, fieldV)...) + diags = append(diags, o.decodeBodyToValue(body, ctx, fieldV)...) } } @@ -95,7 +106,7 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) } fieldV.Set(reflect.ValueOf(attrs)) default: - diags = append(diags, decodeBodyToValue(leftovers, ctx, fieldV)...) + diags = append(diags, o.decodeBodyToValue(leftovers, ctx, fieldV)...) } } @@ -124,7 +135,7 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) case exprType.AssignableTo(field.Type): fieldV.Set(reflect.ValueOf(attr.Expr)) default: - diags = append(diags, DecodeExpression( + diags = append(diags, o.DecodeExpression( attr.Expr, ctx, fieldV.Addr().Interface(), )...) } @@ -198,13 +209,13 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) if v.IsNil() { v = reflect.New(ty) } - diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) + diags = append(diags, o.decodeBlockToValue(block, ctx, v.Elem())...) sli.Index(i).Set(v) } else { if i >= sli.Len() { sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty))) } - diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...) + diags = append(diags, o.decodeBlockToValue(block, ctx, sli.Index(i))...) } } @@ -221,10 +232,10 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) if v.IsNil() { v = reflect.New(ty) } - diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) + diags = append(diags, o.decodeBlockToValue(block, ctx, v.Elem())...) val.Field(fieldIdx).Set(v) } else { - diags = append(diags, decodeBlockToValue(block, ctx, val.Field(fieldIdx))...) + diags = append(diags, o.decodeBlockToValue(block, ctx, val.Field(fieldIdx))...) } } @@ -234,7 +245,7 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) return diags } -func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { +func (o DecodeOptions) decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { attrs, diags := body.JustAttributes() if attrs == nil { return diags @@ -250,7 +261,7 @@ func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.D mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr.Expr)) default: ev := reflect.New(v.Type().Elem()) - diags = append(diags, DecodeExpression(attr.Expr, ctx, ev.Interface())...) + diags = append(diags, o.DecodeExpression(attr.Expr, ctx, ev.Interface())...) mv.SetMapIndex(reflect.ValueOf(k), ev.Elem()) } } @@ -260,8 +271,8 @@ func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.D return diags } -func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { - diags := decodeBodyToValue(block.Body, ctx, v) +func (o DecodeOptions) decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { + diags := o.decodeBodyToValue(block.Body, ctx, v) if len(block.Labels) > 0 { blockTags := getFieldTags(v.Type()) @@ -274,29 +285,17 @@ func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) return diags } -// DecodeExpression extracts the value of the given expression into the given -// value. This value must be something that gocty is able to decode into, -// since the final decoding is delegated to that package. -// -// The given EvalContext is used to resolve any variables or functions in -// expressions encountered while decoding. This may be nil to require only -// constant values, for simple applications that do not support variables or -// functions. -// -// The returned diagnostics should be inspected with its HasErrors method to -// determine if the populated value is valid and complete. If error diagnostics -// are returned then the given value may have been partially-populated but -// may still be accessed by a careful caller for static analysis and editor -// integration use-cases. -func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { +func (o DecodeOptions) DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { + o = o.withDefaults() + srcVal, diags := expr.Value(ctx) - convTy, err := gocty.ImpliedType(val) + convTy, err := o.ImpliedType(val) if err != nil { panic(fmt.Sprintf("unsuitable DecodeExpression target: %s", err)) } - srcVal, err = convert.Convert(srcVal, convTy) + srcVal, err = o.Convert(srcVal, convTy) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -321,3 +320,32 @@ func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{} return diags } + +// DecodeExpression extracts the value of the given expression into the given +// value. This value must be something that gocty is able to decode into, +// since the final decoding is delegated to that package. +// +// The given EvalContext is used to resolve any variables or functions in +// expressions encountered while decoding. This may be nil to require only +// constant values, for simple applications that do not support variables or +// functions. +// +// The returned diagnostics should be inspected with its HasErrors method to +// determine if the populated value is valid and complete. If error diagnostics +// are returned then the given value may have been partially-populated but +// may still be accessed by a careful caller for static analysis and editor +// integration use-cases. +func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { + return DecodeOptions{}.DecodeExpression(expr, ctx, val) +} + +func (o DecodeOptions) withDefaults() DecodeOptions { + if o.ImpliedType == nil { + o.ImpliedType = gocty.ImpliedType + } + + if o.Convert == nil { + o.Convert = convert.Convert + } + return o +} diff --git a/vendor/github.com/hashicorp/hcl/v2/hclsyntax/expression.go b/vendor/github.com/hashicorp/hcl/v2/hclsyntax/expression.go index 815973996bb..577a50fa3b9 100644 --- a/vendor/github.com/hashicorp/hcl/v2/hclsyntax/expression.go +++ b/vendor/github.com/hashicorp/hcl/v2/hclsyntax/expression.go @@ -1780,7 +1780,7 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { if sourceVal.IsNull() { if autoUpgrade { - return cty.EmptyTupleVal, diags + return cty.EmptyTupleVal.WithSameMarks(sourceVal), diags } diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -1798,7 +1798,7 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { // If we don't even know the _type_ of our source value yet then // we'll need to defer all processing, since we can't decide our // result type either. - return cty.DynamicVal, diags + return cty.DynamicVal.WithSameMarks(sourceVal), diags } upgradedUnknown := false @@ -1813,13 +1813,14 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { // list of a single attribute, but we still need to check if that // attribute actually exists. if !sourceVal.IsKnown() { - sourceRng := sourceVal.Range() + unmarkedVal, _ := sourceVal.Unmark() + sourceRng := unmarkedVal.Range() if sourceRng.CouldBeNull() { upgradedUnknown = true } } - sourceVal = cty.TupleVal([]cty.Value{sourceVal}) + sourceVal = cty.TupleVal([]cty.Value{sourceVal}).WithSameMarks(sourceVal) sourceTy = sourceVal.Type() } @@ -1900,14 +1901,14 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { e.Item.clearValue(ctx) // clean up our temporary value if upgradedUnknown { - return cty.DynamicVal, diags + return cty.DynamicVal.WithMarks(marks), diags } if !isKnown { // We'll ingore the resultTy diagnostics in this case since they // will just be the same errors we saw while iterating above. ty, _ := resultTy() - return cty.UnknownVal(ty), diags + return cty.UnknownVal(ty).WithMarks(marks), diags } switch { @@ -1915,7 +1916,7 @@ func (e *SplatExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { if len(vals) == 0 { ty, tyDiags := resultTy() diags = append(diags, tyDiags...) - return cty.ListValEmpty(ty.ElementType()), diags + return cty.ListValEmpty(ty.ElementType()).WithMarks(marks), diags } return cty.ListVal(vals).WithMarks(marks), diags default: diff --git a/vendor/github.com/hashicorp/hcl/v2/hclsyntax/parser.go b/vendor/github.com/hashicorp/hcl/v2/hclsyntax/parser.go index ce96ae35b4c..fec7861a29f 100644 --- a/vendor/github.com/hashicorp/hcl/v2/hclsyntax/parser.go +++ b/vendor/github.com/hashicorp/hcl/v2/hclsyntax/parser.go @@ -811,9 +811,16 @@ Traversal: // will probably be misparsed until we hit something that // allows us to re-sync. // - // We will probably need to do something better here eventually - // in order to support autocomplete triggered by typing a - // period. + // Returning an ExprSyntaxError allows us to pass more information + // about the invalid expression to the caller, which can then + // use this for example for completions that happen after typing + // a dot in an editor. + ret = &ExprSyntaxError{ + Placeholder: cty.DynamicVal, + ParseDiags: diags, + SrcRange: hcl.RangeBetween(from.Range(), dot.Range), + } + p.setRecovery() } @@ -1516,6 +1523,16 @@ func (p *parser) parseObjectCons() (Expression, hcl.Diagnostics) { diags = append(diags, valueDiags...) if p.recovery && valueDiags.HasErrors() { + // If the value is an ExprSyntaxError, we can add an item with it, even though we will recover afterwards + // This allows downstream consumers to still retrieve this first invalid item, even though following items + // won't be parsed. This is useful for supplying completions. + if exprSyntaxError, ok := value.(*ExprSyntaxError); ok { + items = append(items, ObjectConsItem{ + KeyExpr: key, + ValueExpr: exprSyntaxError, + }) + } + // If expression parsing failed then we are probably in a strange // place in the token stream, so we'll bail out and try to reset // to after our closing brace to allow parsing to continue. diff --git a/vendor/github.com/hashicorp/hcl/v2/hclsyntax/parser_traversal.go b/vendor/github.com/hashicorp/hcl/v2/hclsyntax/parser_traversal.go index 3afa6ab0645..f7d4062f09e 100644 --- a/vendor/github.com/hashicorp/hcl/v2/hclsyntax/parser_traversal.go +++ b/vendor/github.com/hashicorp/hcl/v2/hclsyntax/parser_traversal.go @@ -4,8 +4,9 @@ package hclsyntax import ( - "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/hcl/v2" ) // ParseTraversalAbs parses an absolute traversal that is assumed to consume @@ -13,6 +14,26 @@ import ( // behavior is not supported here because traversals are not expected to // be parsed as part of a larger program. func (p *parser) ParseTraversalAbs() (hcl.Traversal, hcl.Diagnostics) { + return p.parseTraversal(false) +} + +// ParseTraversalPartial parses an absolute traversal that is permitted +// to contain splat ([*]) expressions. Only splat expressions within square +// brackets are permitted ([*]); splat expressions within attribute names are +// not permitted (.*). +// +// The meaning of partial here is that the traversal may be incomplete, in that +// any splat expression indicates reference to a potentially unknown number of +// elements. +// +// Traversals that include splats cannot be automatically traversed by HCL using +// the TraversalAbs or TraversalRel methods. Instead, the caller must handle +// the traversals manually. +func (p *parser) ParseTraversalPartial() (hcl.Traversal, hcl.Diagnostics) { + return p.parseTraversal(true) +} + +func (p *parser) parseTraversal(allowSplats bool) (hcl.Traversal, hcl.Diagnostics) { var ret hcl.Traversal var diags hcl.Diagnostics @@ -127,6 +148,34 @@ func (p *parser) ParseTraversalAbs() (hcl.Traversal, hcl.Diagnostics) { return ret, diags } + case TokenStar: + if allowSplats { + + p.Read() // Eat the star. + close := p.Read() + if close.Type != TokenCBrack { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unclosed index brackets", + Detail: "Index key must be followed by a closing bracket.", + Subject: &close.Range, + Context: hcl.RangeBetween(open.Range, close.Range).Ptr(), + }) + } + + ret = append(ret, hcl.TraverseSplat{ + SrcRange: hcl.RangeBetween(open.Range, close.Range), + }) + + if diags.HasErrors() { + return ret, diags + } + + continue + } + + // Otherwise, return the error below for the star. + fallthrough default: if next.Type == TokenStar { diags = append(diags, &hcl.Diagnostic{ diff --git a/vendor/github.com/hashicorp/hcl/v2/hclsyntax/public.go b/vendor/github.com/hashicorp/hcl/v2/hclsyntax/public.go index d56f8e50be5..17dc1ed419a 100644 --- a/vendor/github.com/hashicorp/hcl/v2/hclsyntax/public.go +++ b/vendor/github.com/hashicorp/hcl/v2/hclsyntax/public.go @@ -118,6 +118,37 @@ func ParseTraversalAbs(src []byte, filename string, start hcl.Pos) (hcl.Traversa return expr, diags } +// ParseTraversalPartial matches the behavior of ParseTraversalAbs except +// that it allows splat expressions ([*]) to appear in the traversal. +// +// The returned traversals are "partial" in that the splat expression indicates +// an unknown value for the index. +// +// Traversals that include splats cannot be automatically traversed by HCL using +// the TraversalAbs or TraversalRel methods. Instead, the caller must handle +// the traversals manually. +func ParseTraversalPartial(src []byte, filename string, start hcl.Pos) (hcl.Traversal, hcl.Diagnostics) { + tokens, diags := LexExpression(src, filename, start) + peeker := newPeeker(tokens, false) + parser := &parser{peeker: peeker} + + // Bare traverals are always parsed in "ignore newlines" mode, as if + // they were wrapped in parentheses. + parser.PushIncludeNewlines(false) + + expr, parseDiags := parser.ParseTraversalPartial() + diags = append(diags, parseDiags...) + + parser.PopIncludeNewlines() + + // Panic if the parser uses incorrect stack discipline with the peeker's + // newlines stack, since otherwise it will produce confusing downstream + // errors. + peeker.AssertEmptyIncludeNewlinesStack() + + return expr, diags +} + // LexConfig performs lexical analysis on the given buffer, treating it as a // whole HCL config file, and returns the resulting tokens. // diff --git a/vendor/github.com/hashicorp/hcl/v2/ops.go b/vendor/github.com/hashicorp/hcl/v2/ops.go index bdf23614d67..3cd7b205eff 100644 --- a/vendor/github.com/hashicorp/hcl/v2/ops.go +++ b/vendor/github.com/hashicorp/hcl/v2/ops.go @@ -49,7 +49,7 @@ func Index(collection, key cty.Value, srcRange *Range) (cty.Value, Diagnostics) ty := collection.Type() kty := key.Type() if kty == cty.DynamicPseudoType || ty == cty.DynamicPseudoType { - return cty.DynamicVal, nil + return cty.DynamicVal.WithSameMarks(collection), nil } switch { @@ -87,9 +87,9 @@ func Index(collection, key cty.Value, srcRange *Range) (cty.Value, Diagnostics) has, _ := collection.HasIndex(key).Unmark() if !has.IsKnown() { if ty.IsTupleType() { - return cty.DynamicVal, nil + return cty.DynamicVal.WithSameMarks(collection), nil } else { - return cty.UnknownVal(ty.ElementType()), nil + return cty.UnknownVal(ty.ElementType()).WithSameMarks(collection), nil } } if has.False() { @@ -196,10 +196,10 @@ func Index(collection, key cty.Value, srcRange *Range) (cty.Value, Diagnostics) } } if !collection.IsKnown() { - return cty.DynamicVal, nil + return cty.DynamicVal.WithSameMarks(collection), nil } if !key.IsKnown() { - return cty.DynamicVal, nil + return cty.DynamicVal.WithSameMarks(collection), nil } key, _ = key.Unmark() @@ -291,13 +291,13 @@ func GetAttr(obj cty.Value, attrName string, srcRange *Range) (cty.Value, Diagno } if !obj.IsKnown() { - return cty.UnknownVal(ty.AttributeType(attrName)), nil + return cty.UnknownVal(ty.AttributeType(attrName)).WithSameMarks(obj), nil } return obj.GetAttr(attrName), nil case ty.IsMapType(): if !obj.IsKnown() { - return cty.UnknownVal(ty.ElementType()), nil + return cty.UnknownVal(ty.ElementType()).WithSameMarks(obj), nil } idx := cty.StringVal(attrName) @@ -319,7 +319,7 @@ func GetAttr(obj cty.Value, attrName string, srcRange *Range) (cty.Value, Diagno return obj.Index(idx), nil case ty == cty.DynamicPseudoType: - return cty.DynamicVal, nil + return cty.DynamicVal.WithSameMarks(obj), nil case ty.IsListType() && ty.ElementType().IsObjectType(): // It seems a common mistake to try to access attributes on a whole // list of objects rather than on a specific individual element, so diff --git a/vendor/github.com/hashicorp/hcl/v2/spec.md b/vendor/github.com/hashicorp/hcl/v2/spec.md index 97ef613182f..d52ed70bb52 100644 --- a/vendor/github.com/hashicorp/hcl/v2/spec.md +++ b/vendor/github.com/hashicorp/hcl/v2/spec.md @@ -96,7 +96,7 @@ of the implementation language. ### _Dynamic Attributes_ Processing The _schema-driven_ processing model is useful when the expected structure -of a body is known a priori by the calling application. Some blocks are +of a body is known by the calling application. Some blocks are instead more free-form, such as a user-provided set of arbitrary key/value pairs. diff --git a/vendor/modules.txt b/vendor/modules.txt index 8e9b076a238..1783ada34f5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -419,7 +419,7 @@ github.com/hashicorp/go-cty-funcs/uuid # github.com/hashicorp/go-multierror v1.1.1 ## explicit; go 1.13 github.com/hashicorp/go-multierror -# github.com/hashicorp/hcl/v2 v2.20.1 +# github.com/hashicorp/hcl/v2 v2.20.1 => github.com/jsternberg/hcl/v2 v2.22.1-0.20241028205650-d84f8d55e419 ## explicit; go 1.18 github.com/hashicorp/hcl/v2 github.com/hashicorp/hcl/v2/ext/customdecode @@ -1334,3 +1334,4 @@ sigs.k8s.io/structured-merge-diff/v4/value # sigs.k8s.io/yaml v1.3.0 ## explicit; go 1.12 sigs.k8s.io/yaml +# github.com/hashicorp/hcl/v2 => github.com/jsternberg/hcl/v2 v2.22.1-0.20241028205650-d84f8d55e419