diff --git a/bake/bake.go b/bake/bake.go index 6f72fb88f07..571cf2cb9b8 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -698,30 +698,30 @@ 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 []*buildflags.CacheOptionsEntry `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"` - CacheTo []*buildflags.CacheOptionsEntry `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"` - Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"` - Secrets []*buildflags.Secret `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"` - SSH []*buildflags.SSH `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"` + 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 buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"` + CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"` + Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"` + Secrets []*buildflags.Secret `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"` + SSH []*buildflags.SSH `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 @@ -742,8 +742,8 @@ func (t *Target) normalize() { t.Secrets = removeDupes(t.Secrets) t.SSH = removeDupes(t.SSH) t.Platforms = removeDupesStr(t.Platforms) - t.CacheFrom = removeDupes(t.CacheFrom) - t.CacheTo = removeDupes(t.CacheTo) + t.CacheFrom = t.CacheFrom.Normalize() + t.CacheTo = t.CacheTo.Normalize() t.Outputs = removeDupes(t.Outputs) t.NoCacheFilter = removeDupesStr(t.NoCacheFilter) t.Ulimits = removeDupesStr(t.Ulimits) @@ -824,7 +824,7 @@ func (t *Target) Merge(t2 *Target) { t.Platforms = t2.Platforms } if t2.CacheFrom != nil { // merge - t.CacheFrom = append(t.CacheFrom, t2.CacheFrom...) + t.CacheFrom = t.CacheFrom.Merge(t2.CacheFrom) } if t2.CacheTo != nil { // no merge t.CacheTo = t2.CacheTo @@ -1372,17 +1372,12 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { } } - cacheImports := make([]*controllerapi.CacheOptionsEntry, len(t.CacheFrom)) - for i, ci := range t.CacheFrom { - cacheImports[i] = ci.ToPB() + if t.CacheFrom != nil { + bo.CacheFrom = controllerapi.CreateCaches(t.CacheFrom.ToPB()) } - bo.CacheFrom = controllerapi.CreateCaches(cacheImports) - - cacheExports := make([]*controllerapi.CacheOptionsEntry, len(t.CacheTo)) - for i, ce := range t.CacheTo { - cacheExports[i] = ce.ToPB() + if t.CacheTo != nil { + bo.CacheTo = controllerapi.CreateCaches(t.CacheTo.ToPB()) } - bo.CacheTo = controllerapi.CreateCaches(cacheExports) outputs := make([]*controllerapi.ExportEntry, len(t.Outputs)) for i, output := range t.Outputs { @@ -1625,9 +1620,13 @@ func parseArrValue[T any, PT arrValue[T]](s []string) ([]*T, error) { return outputs, nil } -func parseCacheArrValues(s []string) ([]*buildflags.CacheOptionsEntry, error) { - outs := make([]*buildflags.CacheOptionsEntry, 0, len(s)) +func parseCacheArrValues(s []string) (buildflags.CacheOptions, error) { + var outs buildflags.CacheOptions for _, in := range s { + if in == "" { + continue + } + if !strings.Contains(in, "=") { // This is ref only format. Each field in the CSV is its own entry. fields, err := csvvalue.Fields(in, nil) diff --git a/bake/compose.go b/bake/compose.go index 4a4facb159f..4cb4bbc26ba 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -353,14 +353,14 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error { if err != nil { return err } - t.CacheFrom = removeDupes(append(t.CacheFrom, cacheFrom...)) + t.CacheFrom = t.CacheFrom.Merge(cacheFrom) } if len(xb.CacheTo) > 0 { cacheTo, err := parseCacheArrValues(xb.CacheTo) if err != nil { return err } - t.CacheTo = removeDupes(append(t.CacheTo, cacheTo...)) + t.CacheTo = t.CacheTo.Merge(cacheTo) } if len(xb.Secrets) > 0 { secrets, err := parseArrValue[buildflags.Secret](xb.Secrets) diff --git a/bake/hcl_test.go b/bake/hcl_test.go index facf7b790e6..63dcea42940 100644 --- a/bake/hcl_test.go +++ b/bake/hcl_test.go @@ -606,7 +606,7 @@ func TestHCLAttrsCapsuleType(t *testing.T) { target "app" { cache-from = [ { type = "registry", ref = "user/app:cache" }, - { type = "local", src = "path/to/cache" }, + "type=local,src=path/to/cache", ] cache-to = [ @@ -649,7 +649,7 @@ func TestHCLAttrsCapsuleTypeVars(t *testing.T) { target "app" { cache-from = [ { type = "registry", ref = "user/app:cache" }, - { type = "local", src = "path/to/cache" }, + "type=local,src=path/to/cache", ] cache-to = [ target.app.cache-from[0] ] @@ -674,7 +674,7 @@ func TestHCLAttrsCapsuleTypeVars(t *testing.T) { output = [ "type=oci,dest=../${foo}.tar" ] secret = [ - { id = target.app.output[0].type, src = "/local/secret" }, + { id = target.app.output[0].type, src = "/${target.app.cache-from[1].type}/secret" }, ] } `) diff --git a/bake/hclparser/type_implied_ext.go b/bake/hclparser/type_implied_ext.go index fad53f7074c..e1fbd340b38 100644 --- a/bake/hclparser/type_implied_ext.go +++ b/bake/hclparser/type_implied_ext.go @@ -10,44 +10,60 @@ import ( "github.com/zclconf/go-cty/cty/gocty" ) -type CapsuleValue interface { - // FromCtyValue will initialize this value using a cty.Value. - FromCtyValue(in cty.Value, path cty.Path) error - +type ToCtyValueConverter interface { // ToCtyValue will convert this capsule value into a native // cty.Value. This should not return a capsule type. ToCtyValue() cty.Value } +type FromCtyValueConverter interface { + // FromCtyValue will initialize this value using a cty.Value. + FromCtyValue(in cty.Value, path cty.Path) error +} + type extensionType int const ( - nativeTypeExtension extensionType = iota + unwrapCapsuleValueExtension extensionType = iota ) func impliedTypeExt(rt reflect.Type, _ cty.Path) (cty.Type, error) { - if rt.AssignableTo(capsuleValueType) { + if rt.Kind() != reflect.Pointer { + rt = reflect.PointerTo(rt) + } + + if isCapsuleType(rt) { return capsuleValueCapsuleType(rt), nil } return cty.NilType, errdefs.ErrNotImplemented } -var ( - capsuleValueType = reflect.TypeFor[CapsuleValue]() - capsuleValueTypes sync.Map -) +func isCapsuleType(rt reflect.Type) bool { + fromCtyValueType := reflect.TypeFor[FromCtyValueConverter]() + toCtyValueType := reflect.TypeFor[ToCtyValueConverter]() + return rt.Implements(fromCtyValueType) && rt.Implements(toCtyValueType) +} + +var capsuleValueTypes sync.Map func capsuleValueCapsuleType(rt reflect.Type) cty.Type { - if val, loaded := capsuleValueTypes.Load(rt); loaded { + if rt.Kind() != reflect.Pointer { + panic("capsule value must be a pointer") + } + + elem := rt.Elem() + if val, loaded := capsuleValueTypes.Load(elem); loaded { return val.(cty.Type) } - // First time used. - ety := cty.CapsuleWithOps(rt.Name(), rt.Elem(), &cty.CapsuleOps{ + toCtyValueType := reflect.TypeFor[ToCtyValueConverter]() + + // First time used. Initialize new capsule ops. + ops := &cty.CapsuleOps{ ConversionTo: func(_ cty.Type) func(cty.Value, cty.Path) (any, error) { return func(in cty.Value, p cty.Path) (any, error) { - rv := reflect.New(rt.Elem()).Interface() - if err := rv.(CapsuleValue).FromCtyValue(in, p); err != nil { + rv := reflect.New(elem).Interface() + if err := rv.(FromCtyValueConverter).FromCtyValue(in, p); err != nil { return nil, err } return rv, nil @@ -55,30 +71,39 @@ func capsuleValueCapsuleType(rt reflect.Type) cty.Type { }, ConversionFrom: func(want cty.Type) func(any, cty.Path) (cty.Value, error) { return func(in any, _ cty.Path) (cty.Value, error) { - v := in.(CapsuleValue).ToCtyValue() + rv := reflect.ValueOf(in).Convert(toCtyValueType) + v := rv.Interface().(ToCtyValueConverter).ToCtyValue() return convert.Convert(v, want) } }, ExtensionData: func(key any) any { switch key { - case nativeTypeExtension: - zero := reflect.Zero(rt).Interface() - return zero.(CapsuleValue).ToCtyValue().Type() - default: - return nil + case unwrapCapsuleValueExtension: + zero := reflect.Zero(elem).Interface() + if conv, ok := zero.(ToCtyValueConverter); ok { + return conv.ToCtyValue().Type() + } + + zero = reflect.Zero(rt).Interface() + if conv, ok := zero.(ToCtyValueConverter); ok { + return conv.ToCtyValue().Type() + } } + return nil }, - }) + } - // Attempt to store the new type. Use whichever was loaded first in the case of a race condition. - val, _ := capsuleValueTypes.LoadOrStore(rt, ety) + // Attempt to store the new type. Use whichever was loaded first in the case + // of a race condition. + ety := cty.CapsuleWithOps(elem.Name(), elem, ops) + val, _ := capsuleValueTypes.LoadOrStore(elem, ety) return val.(cty.Type) } -// ToNativeValue will convert a value to only native cty types which will -// remove capsule types if possible. -func ToNativeValue(in cty.Value) cty.Value { - want := toNativeType(in.Type()) +// UnwrapCtyValue will unwrap capsule type values into their native cty value +// equivalents if possible. +func UnwrapCtyValue(in cty.Value) cty.Value { + want := toCtyValueType(in.Type()) if in.Type().Equals(want) { return in } else if out, err := convert.Convert(in, want); err == nil { @@ -87,17 +112,17 @@ func ToNativeValue(in cty.Value) cty.Value { return cty.NullVal(want) } -func toNativeType(in cty.Type) cty.Type { +func toCtyValueType(in cty.Type) cty.Type { if et := in.MapElementType(); et != nil { - return cty.Map(toNativeType(*et)) + return cty.Map(toCtyValueType(*et)) } if et := in.SetElementType(); et != nil { - return cty.Set(toNativeType(*et)) + return cty.Set(toCtyValueType(*et)) } if et := in.ListElementType(); et != nil { - return cty.List(toNativeType(*et)) + return cty.List(toCtyValueType(*et)) } if in.IsObjectType() { @@ -105,7 +130,7 @@ func toNativeType(in cty.Type) cty.Type { inAttrTypes := in.AttributeTypes() outAttrTypes := make(map[string]cty.Type, len(inAttrTypes)) for name, typ := range inAttrTypes { - outAttrTypes[name] = toNativeType(typ) + outAttrTypes[name] = toCtyValueType(typ) if in.AttributeOptional(name) { optional = append(optional, name) } @@ -117,13 +142,13 @@ func toNativeType(in cty.Type) cty.Type { inTypes := in.TupleElementTypes() outTypes := make([]cty.Type, len(inTypes)) for i, typ := range inTypes { - outTypes[i] = toNativeType(typ) + outTypes[i] = toCtyValueType(typ) } return cty.Tuple(outTypes) } if in.IsCapsuleType() { - if out := in.CapsuleExtensionData(nativeTypeExtension); out != nil { + if out := in.CapsuleExtensionData(unwrapCapsuleValueExtension); out != nil { return out.(cty.Type) } return cty.DynamicPseudoType @@ -137,5 +162,5 @@ func ToCtyValue(val any, ty cty.Type) (cty.Value, error) { if err != nil { return out, err } - return ToNativeValue(out), nil + return UnwrapCtyValue(out), nil } diff --git a/tests/bake.go b/tests/bake.go index 8f92cb8e668..66e4aa04e79 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -32,6 +32,7 @@ func bakeCmd(sb integration.Sandbox, opts ...cmdOpt) (string, error) { var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakePrint, + testBakePrintSensitive, testBakeLocal, testBakeLocalMulti, testBakeRemote, @@ -77,7 +78,8 @@ target "build" { HELLO = "foo" } } -`)}, +`), + }, { "Compose", "compose.yml", @@ -88,7 +90,8 @@ services: context: . args: HELLO: foo -`)}, +`), + }, } for _, tc := range testCases { @@ -149,6 +152,124 @@ RUN echo "Hello ${HELLO}" } } +func testBakePrintSensitive(t *testing.T, sb integration.Sandbox) { + testCases := []struct { + name string + f string + dt []byte + }{ + { + "HCL", + "docker-bake.hcl", + []byte(` +target "build" { + args = { + HELLO = "foo" + } + + cache-from = [ + "type=gha", + "type=s3,region=us-west-2,bucket=my_bucket,name=my_image", + ] +} +`), + }, + { + "Compose", + "compose.yml", + []byte(` +services: + build: + build: + context: . + args: + HELLO: foo + cache_from: + - type=gha + - type=s3,region=us-west-2,bucket=my_bucket,name=my_image +`), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dir := tmpdir( + t, + fstest.CreateFile(tc.f, tc.dt, 0600), + fstest.CreateFile("Dockerfile", []byte(` +FROM busybox +ARG HELLO +RUN echo "Hello ${HELLO}" + `), 0600), + ) + + cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--print", "build"), + withEnv( + "ACTIONS_RUNTIME_TOKEN=sensitive_token", + "ACTIONS_CACHE_URL=https://cache.github.com", + "AWS_ACCESS_KEY_ID=definitely_dont_look_here", + "AWS_SECRET_ACCESS_KEY=hackers_please_dont_steal", + "AWS_SESSION_TOKEN=not_a_mitm_attack", + ), + ) + stdout := bytes.Buffer{} + stderr := bytes.Buffer{} + cmd.Stdout = &stdout + cmd.Stderr = &stderr + require.NoError(t, cmd.Run(), stdout.String(), stderr.String()) + + var def struct { + Group map[string]*bake.Group `json:"group,omitempty"` + Target map[string]*bake.Target `json:"target"` + } + require.NoError(t, json.Unmarshal(stdout.Bytes(), &def)) + + require.Len(t, def.Group, 1) + require.Contains(t, def.Group, "default") + + require.Equal(t, []string{"build"}, def.Group["default"].Targets) + require.Len(t, def.Target, 1) + require.Contains(t, def.Target, "build") + require.Equal(t, ".", *def.Target["build"].Context) + require.Equal(t, "Dockerfile", *def.Target["build"].Dockerfile) + require.Equal(t, map[string]*string{"HELLO": ptrstr("foo")}, def.Target["build"].Args) + require.NotNil(t, def.Target["build"].CacheFrom) + require.Len(t, def.Target["build"].CacheFrom, 2) + + require.JSONEq(t, `{ + "group": { + "default": { + "targets": [ + "build" + ] + } + }, + "target": { + "build": { + "context": ".", + "dockerfile": "Dockerfile", + "args": { + "HELLO": "foo" + }, + "cache-from": [ + { + "type": "gha" + }, + { + "type": "s3", + "region": "us-west-2", + "bucket": "my_bucket", + "name": "my_image" + } + ] + } + } +} +`, stdout.String()) + }) + } +} + func testBakeLocal(t *testing.T, sb integration.Sandbox) { dockerfile := []byte(` FROM scratch diff --git a/util/buildflags/cache.go b/util/buildflags/cache.go index fcd36ff5e04..7ff2d4155d6 100644 --- a/util/buildflags/cache.go +++ b/util/buildflags/cache.go @@ -15,6 +15,41 @@ import ( jsoncty "github.com/zclconf/go-cty/cty/json" ) +type CacheOptions []*CacheOptionsEntry + +func (o CacheOptions) Merge(other CacheOptions) CacheOptions { + if other == nil { + return o.Normalize() + } else if o == nil { + return other.Normalize() + } + + return append(o, other...).Normalize() +} + +func (o CacheOptions) Normalize() CacheOptions { + if len(o) == 0 { + return nil + } + return removeDupes(o) +} + +func (o CacheOptions) ToPB() []*controllerapi.CacheOptionsEntry { + if len(o) == 0 { + return nil + } + + entries := make([]*controllerapi.CacheOptionsEntry, 0, len(o)) + for _, entry := range o { + pb := entry.ToPB() + if !isActive(pb) { + continue + } + entries = append(entries, pb) + } + return entries +} + type CacheOptionsEntry struct { Type string `json:"type"` Attrs map[string]string `json:"attrs,omitempty"` @@ -46,10 +81,13 @@ func (e *CacheOptionsEntry) String() string { } func (e *CacheOptionsEntry) ToPB() *controllerapi.CacheOptionsEntry { - return &controllerapi.CacheOptionsEntry{ + ci := &controllerapi.CacheOptionsEntry{ Type: e.Type, Attrs: maps.Clone(e.Attrs), } + addGithubToken(ci) + addAwsCredentials(ci) + return ci } func (e *CacheOptionsEntry) MarshalJSON() ([]byte, error) { @@ -74,14 +112,6 @@ func (e *CacheOptionsEntry) UnmarshalJSON(data []byte) error { return e.validate(data) } -func (e *CacheOptionsEntry) IsActive() bool { - // Always active if not gha. - if e.Type != "gha" { - return true - } - return e.Attrs["token"] != "" && e.Attrs["url"] != "" -} - func (e *CacheOptionsEntry) UnmarshalText(text []byte) error { in := string(text) fields, err := csvvalue.Fields(in, nil) @@ -116,8 +146,6 @@ func (e *CacheOptionsEntry) UnmarshalText(text []byte) error { if e.Type == "" { return errors.Errorf("type required form> %q", in) } - addGithubToken(e) - addAwsCredentials(e) return e.validate(text) } @@ -140,23 +168,22 @@ func (e *CacheOptionsEntry) validate(gv interface{}) error { } func ParseCacheEntry(in []string) ([]*controllerapi.CacheOptionsEntry, error) { - outs := make([]*controllerapi.CacheOptionsEntry, 0, len(in)) + if len(in) == 0 { + return nil, nil + } + + opts := make(CacheOptions, 0, len(in)) for _, in := range in { var out CacheOptionsEntry if err := out.UnmarshalText([]byte(in)); err != nil { return nil, err } - - if !out.IsActive() { - // Skip inactive cache entries. - continue - } - outs = append(outs, out.ToPB()) + opts = append(opts, &out) } - return outs, nil + return opts.ToPB(), nil } -func addGithubToken(ci *CacheOptionsEntry) { +func addGithubToken(ci *controllerapi.CacheOptionsEntry) { if ci.Type != "gha" { return } @@ -172,7 +199,7 @@ func addGithubToken(ci *CacheOptionsEntry) { } } -func addAwsCredentials(ci *CacheOptionsEntry) { +func addAwsCredentials(ci *controllerapi.CacheOptionsEntry) { if ci.Type != "s3" { return } @@ -201,3 +228,11 @@ func addAwsCredentials(ci *CacheOptionsEntry) { ci.Attrs["session_token"] = credentials.SessionToken } } + +func isActive(pb *controllerapi.CacheOptionsEntry) bool { + // Always active if not gha. + if pb.Type != "gha" { + return true + } + return pb.Attrs["token"] != "" && pb.Attrs["url"] != "" +} diff --git a/util/buildflags/cache_cty.go b/util/buildflags/cache_cty.go new file mode 100644 index 00000000000..3d65b9c8448 --- /dev/null +++ b/util/buildflags/cache_cty.go @@ -0,0 +1,82 @@ +package buildflags + +import ( + "sync" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" +) + +var cacheOptionsEntryType = sync.OnceValue(func() cty.Type { + return cty.Map(cty.String) +}) + +func (o *CacheOptions) FromCtyValue(in cty.Value, p cty.Path) error { + got := in.Type() + if got.IsTupleType() || got.IsListType() { + return o.fromCtyValue(in, p) + } + + want := cty.List(cacheOptionsEntryType()) + return p.NewErrorf("%s", convert.MismatchMessage(got, want)) +} + +func (o *CacheOptions) fromCtyValue(in cty.Value, p cty.Path) error { + *o = make([]*CacheOptionsEntry, 0, in.LengthInt()) + for elem := in.ElementIterator(); elem.Next(); { + _, value := elem.Element() + + entry := &CacheOptionsEntry{} + if err := entry.FromCtyValue(value, p); err != nil { + return err + } + *o = append(*o, entry) + } + return nil +} + +func (o CacheOptions) ToCtyValue() cty.Value { + if len(o) == 0 { + return cty.ListValEmpty(cacheOptionsEntryType()) + } + + vals := make([]cty.Value, len(o)) + for i, entry := range o { + vals[i] = entry.ToCtyValue() + } + return cty.ListVal(vals) +} + +func (o *CacheOptionsEntry) FromCtyValue(in cty.Value, p cty.Path) error { + if in.Type() == cty.String { + if err := o.UnmarshalText([]byte(in.AsString())); err != nil { + return p.NewError(err) + } + return nil + } + + conv, err := convert.Convert(in, cty.Map(cty.String)) + if err != nil { + return err + } + + m := conv.AsValueMap() + if err := getAndDelete(m, "type", &o.Type); err != nil { + return err + } + o.Attrs = asMap(m) + return o.validate(in) +} + +func (o *CacheOptionsEntry) ToCtyValue() cty.Value { + if o == nil { + return cty.NullVal(cty.Map(cty.String)) + } + + vals := make(map[string]cty.Value, len(o.Attrs)+1) + for k, v := range o.Attrs { + vals[k] = cty.StringVal(v) + } + vals["type"] = cty.StringVal(o.Type) + return cty.MapVal(vals) +} diff --git a/util/buildflags/cache_test.go b/util/buildflags/cache_test.go new file mode 100644 index 00000000000..a25544011d7 --- /dev/null +++ b/util/buildflags/cache_test.go @@ -0,0 +1,39 @@ +package buildflags + +import ( + "testing" + + "github.com/docker/buildx/controller/pb" + "github.com/stretchr/testify/require" +) + +func TestCacheOptions_DerivedVars(t *testing.T) { + t.Setenv("ACTIONS_RUNTIME_TOKEN", "sensitive_token") + t.Setenv("ACTIONS_CACHE_URL", "https://cache.github.com") + t.Setenv("AWS_ACCESS_KEY_ID", "definitely_dont_look_here") + t.Setenv("AWS_SECRET_ACCESS_KEY", "hackers_please_dont_steal") + t.Setenv("AWS_SESSION_TOKEN", "not_a_mitm_attack") + + cacheFrom, err := ParseCacheEntry([]string{"type=gha", "type=s3,region=us-west-2,bucket=my_bucket,name=my_image"}) + require.NoError(t, err) + require.Equal(t, []*pb.CacheOptionsEntry{ + { + Type: "gha", + Attrs: map[string]string{ + "token": "sensitive_token", + "url": "https://cache.github.com", + }, + }, + { + Type: "s3", + Attrs: map[string]string{ + "region": "us-west-2", + "bucket": "my_bucket", + "name": "my_image", + "access_key_id": "definitely_dont_look_here", + "secret_access_key": "hackers_please_dont_steal", + "session_token": "not_a_mitm_attack", + }, + }, + }, cacheFrom) +} diff --git a/util/buildflags/cty.go b/util/buildflags/cty.go index d8b6a19a03e..a80b7f763b9 100644 --- a/util/buildflags/cty.go +++ b/util/buildflags/cty.go @@ -9,32 +9,6 @@ import ( "github.com/zclconf/go-cty/cty/gocty" ) -func (e *CacheOptionsEntry) FromCtyValue(in cty.Value, p cty.Path) error { - conv, err := convert.Convert(in, cty.Map(cty.String)) - if err == nil { - m := conv.AsValueMap() - if err := getAndDelete(m, "type", &e.Type); err != nil { - return err - } - e.Attrs = asMap(m) - return e.validate(in) - } - return unmarshalTextFallback(in, e, err) -} - -func (e *CacheOptionsEntry) ToCtyValue() cty.Value { - if e == nil { - return cty.NullVal(cty.Map(cty.String)) - } - - vals := make(map[string]cty.Value, len(e.Attrs)+1) - for k, v := range e.Attrs { - vals[k] = cty.StringVal(v) - } - vals["type"] = cty.StringVal(e.Type) - return cty.MapVal(vals) -} - func (e *ExportEntry) FromCtyValue(in cty.Value, p cty.Path) error { conv, err := convert.Convert(in, cty.Map(cty.String)) if err == nil { diff --git a/util/buildflags/utils.go b/util/buildflags/utils.go new file mode 100644 index 00000000000..6b69f3a81c5 --- /dev/null +++ b/util/buildflags/utils.go @@ -0,0 +1,29 @@ +package buildflags + +type comparable[E any] interface { + Equal(other E) bool +} + +func removeDupes[E comparable[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 +}