From 834f6107e01e3465c39a57eb827aa8d652ec8c99 Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Mon, 2 Dec 2024 14:48:24 -0600 Subject: [PATCH] bake: cache-from/cache-to options no longer print sensitive values This refactors how the cache-from/cache-to composable attributes work so they no longer print sensitive values that are automatically added. This also expands the available syntax that works with the cache options. It is now possible to interleave the csv syntax with the object syntax without any problems. The canonical form is still the object syntax and variables are resolved according to that syntax. `cache-from` and `cache-to` now correctly ignore empty string inputs so these can be used with variables. Signed-off-by: Jonathan A. Sternberg --- bake/bake.go | 83 +++++++++++----------- bake/compose.go | 4 +- bake/compose_test.go | 12 ++-- bake/hcl_test.go | 16 ++--- tests/bake.go | 125 +++++++++++++++++++++++++++++++++- util/buildflags/cache.go | 82 ++++++++++++++++------ util/buildflags/cache_test.go | 39 +++++++++++ util/buildflags/cty.go | 66 +++++++++++++++--- util/buildflags/utils.go | 29 ++++++++ 9 files changed, 371 insertions(+), 85 deletions(-) create mode 100644 util/buildflags/cache_test.go create mode 100644 util/buildflags/utils.go diff --git a/bake/bake.go b/bake/bake.go index 8322edbb3ae..8fef3c5bcf9 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,12 @@ 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) + if t.CacheFrom != nil { + t.CacheFrom.Normalize() + } + if t.CacheTo != nil { + t.CacheTo.Normalize() + } t.Outputs = removeDupes(t.Outputs) t.NoCacheFilter = removeDupesStr(t.NoCacheFilter) t.Ulimits = removeDupesStr(t.Ulimits) @@ -824,7 +828,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 @@ -1338,17 +1342,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 { @@ -1591,9 +1590,13 @@ func parseArrValue[T any, PT arrValue[T]](s []string) ([]*T, error) { return outputs, nil } -func parseCacheArrValues(s []string) ([]*buildflags.CacheOptionsEntry, error) { +func parseCacheArrValues(s []string) (*buildflags.CacheOptions, error) { outs := make([]*buildflags.CacheOptionsEntry, 0, len(s)) 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) @@ -1618,5 +1621,9 @@ func parseCacheArrValues(s []string) ([]*buildflags.CacheOptionsEntry, error) { } outs = append(outs, &out) } - return outs, nil + + if len(outs) == 0 { + return nil, nil + } + return &buildflags.CacheOptions{Entries: outs}, 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/compose_test.go b/bake/compose_test.go index 277433a73be..5361fd82184 100644 --- a/bake/compose_test.go +++ b/bake/compose_test.go @@ -74,8 +74,8 @@ secrets: require.Equal(t, "Dockerfile-alternate", *c.Targets[1].Dockerfile) require.Equal(t, 1, len(c.Targets[1].Args)) require.Equal(t, ptrstr("123"), c.Targets[1].Args["buildno"]) - require.Equal(t, []string{"type=local,src=path/to/cache"}, stringify(c.Targets[1].CacheFrom)) - require.Equal(t, []string{"type=local,dest=path/to/cache"}, stringify(c.Targets[1].CacheTo)) + require.Equal(t, []string{"type=local,src=path/to/cache"}, stringify(c.Targets[1].CacheFrom.Entries)) + require.Equal(t, []string{"type=local,dest=path/to/cache"}, stringify(c.Targets[1].CacheTo.Entries)) require.Equal(t, "none", *c.Targets[1].NetworkMode) require.Equal(t, []string{"default", "key=path/to/key"}, stringify(c.Targets[1].SSH)) require.Equal(t, []string{ @@ -336,8 +336,8 @@ services: require.Equal(t, map[string]*string{"CT_ECR": ptrstr("foo"), "CT_TAG": ptrstr("bar")}, c.Targets[0].Args) require.Equal(t, []string{"ct-addon:baz", "ct-addon:foo", "ct-addon:alp"}, c.Targets[0].Tags) require.Equal(t, []string{"linux/amd64", "linux/arm64"}, c.Targets[0].Platforms) - require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom)) - require.Equal(t, []string{"type=local,dest=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheTo)) + require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom.Entries)) + require.Equal(t, []string{"type=local,dest=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheTo.Entries)) require.Equal(t, []string{"default", "key=path/to/key", "other=path/to/otherkey"}, stringify(c.Targets[0].SSH)) require.Equal(t, newBool(true), c.Targets[0].Pull) require.Equal(t, map[string]string{"alpine": "docker-image://alpine:3.13"}, c.Targets[0].Contexts) @@ -383,8 +383,8 @@ services: require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) require.Equal(t, []string{"ct-addon:foo", "ct-addon:baz"}, c.Targets[0].Tags) - require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom)) - require.Equal(t, []string{"type=local,dest=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheTo)) + require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom.Entries)) + require.Equal(t, []string{"type=local,dest=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheTo.Entries)) require.Equal(t, []string{"default", "key=path/to/key"}, stringify(c.Targets[0].SSH)) } diff --git a/bake/hcl_test.go b/bake/hcl_test.go index facf7b790e6..21f88618dc4 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 = [ @@ -634,8 +634,8 @@ func TestHCLAttrsCapsuleType(t *testing.T) { require.Equal(t, 1, len(c.Targets)) require.Equal(t, []string{"type=oci,dest=../out.tar"}, stringify(c.Targets[0].Outputs)) - require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom)) - require.Equal(t, []string{"type=local,dest=path/to/cache"}, stringify(c.Targets[0].CacheTo)) + require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom.Entries)) + require.Equal(t, []string{"type=local,dest=path/to/cache"}, stringify(c.Targets[0].CacheTo.Entries)) require.Equal(t, []string{"id=mysecret,src=/local/secret", "id=mysecret2,env=TOKEN"}, stringify(c.Targets[0].Secrets)) require.Equal(t, []string{"default", "key=path/to/key"}, stringify(c.Targets[0].SSH)) } @@ -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" }, ] } `) @@ -697,14 +697,14 @@ func TestHCLAttrsCapsuleTypeVars(t *testing.T) { app := findTarget(t, "app") require.Equal(t, []string{"type=oci,dest=../out.tar"}, stringify(app.Outputs)) - require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(app.CacheFrom)) - require.Equal(t, []string{"user/app:cache"}, stringify(app.CacheTo)) + require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(app.CacheFrom.Entries)) + require.Equal(t, []string{"user/app:cache"}, stringify(app.CacheTo.Entries)) require.Equal(t, []string{"id=mysecret,src=/local/secret"}, stringify(app.Secrets)) require.Equal(t, []string{"default", "key=path/to/oci"}, stringify(app.SSH)) web := findTarget(t, "web") require.Equal(t, []string{"type=oci,dest=../bar.tar"}, stringify(web.Outputs)) - require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(web.CacheFrom)) + require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(web.CacheFrom.Entries)) require.Equal(t, []string{"id=oci,src=/local/secret"}, stringify(web.Secrets)) } diff --git a/tests/bake.go b/tests/bake.go index 8f92cb8e668..2c2e93aefec 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.Entries, 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..59cfdc8cca5 100644 --- a/util/buildflags/cache.go +++ b/util/buildflags/cache.go @@ -15,6 +15,48 @@ import ( jsoncty "github.com/zclconf/go-cty/cty/json" ) +type CacheOptions struct { + Entries []*CacheOptionsEntry +} + +func (o *CacheOptions) Merge(other *CacheOptions) *CacheOptions { + if other == nil { + o.Normalize() + return o + } else if o == nil { + other.Normalize() + return other + } + + o.Entries = append(o.Entries, other.Entries...) + o.Normalize() + return o +} + +func (o *CacheOptions) Normalize() { + o.Entries = removeDupes(o.Entries) +} + +func (o *CacheOptions) ToPB() []*controllerapi.CacheOptionsEntry { + entries := make([]*controllerapi.CacheOptionsEntry, 0, len(o.Entries)) + for _, entry := range o.Entries { + pb := entry.ToPB() + if !isActive(pb) { + continue + } + entries = append(entries, pb) + } + return entries +} + +func (o *CacheOptions) MarshalJSON() ([]byte, error) { + return json.Marshal(o.Entries) +} + +func (o *CacheOptions) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &o.Entries) +} + type CacheOptionsEntry struct { Type string `json:"type"` Attrs map[string]string `json:"attrs,omitempty"` @@ -46,10 +88,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 +119,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 +153,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 +175,20 @@ func (e *CacheOptionsEntry) validate(gv interface{}) error { } func ParseCacheEntry(in []string) ([]*controllerapi.CacheOptionsEntry, error) { - outs := make([]*controllerapi.CacheOptionsEntry, 0, len(in)) + opts := &CacheOptions{ + Entries: make([]*CacheOptionsEntry, 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.Entries = append(opts.Entries, &out) } - return outs, nil + return opts.ToPB(), nil } -func addGithubToken(ci *CacheOptionsEntry) { +func addGithubToken(ci *controllerapi.CacheOptionsEntry) { if ci.Type != "gha" { return } @@ -172,7 +204,7 @@ func addGithubToken(ci *CacheOptionsEntry) { } } -func addAwsCredentials(ci *CacheOptionsEntry) { +func addAwsCredentials(ci *controllerapi.CacheOptionsEntry) { if ci.Type != "s3" { return } @@ -201,3 +233,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_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..8ce75bea702 100644 --- a/util/buildflags/cty.go +++ b/util/buildflags/cty.go @@ -9,17 +9,67 @@ 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 { +var cacheOptionsEntryType = sync.OnceValue(func() cty.Type { + return cty.Map(cty.String) +}) + +func (e *CacheOptions) FromCtyValue(in cty.Value, p cty.Path) error { + got := in.Type() + if got.IsTupleType() || got.IsListType() { + return e.fromCtyValue(in, p) + } + + want := cty.List(cacheOptionsEntryType()) + return p.NewErrorf("%s", convert.MismatchMessage(got, want)) +} + +func (e *CacheOptions) fromCtyValue(in cty.Value, p cty.Path) error { + e.Entries = 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 } - e.Attrs = asMap(m) - return e.validate(in) + e.Entries = append(e.Entries, entry) } - return unmarshalTextFallback(in, e, err) + return nil +} + +func (e *CacheOptions) ToCtyValue() cty.Value { + if e == nil { + return cty.NullVal(cty.List(cacheOptionsEntryType())) + } else if len(e.Entries) == 0 { + return cty.ListValEmpty(cacheOptionsEntryType()) + } + + vals := make([]cty.Value, len(e.Entries)) + for i, entry := range e.Entries { + vals[i] = entry.ToCtyValue() + } + return cty.ListVal(vals) +} + +func (e *CacheOptionsEntry) FromCtyValue(in cty.Value, p cty.Path) error { + if in.Type() == cty.String { + if err := e.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", &e.Type); err != nil { + return err + } + e.Attrs = asMap(m) + return e.validate(in) } func (e *CacheOptionsEntry) ToCtyValue() cty.Value { 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 +}