Skip to content

Commit 3aed658

Browse files
committed
buildflags: marshal attestations into json with extra attributes correctly
`MarshalJSON` would not include the extra attributes because it iterated over the target map rather than the source map. Also fixes JSON unmarshaling for SSH and secrets. The intention was to unmarshal into the struct, but `UnmarshalText` takes priority over the default struct unmarshaling so it didn't work as intended. Tests have been added for all marshaling and unmarshaling methods. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
1 parent b4a0dee commit 3aed658

7 files changed

Lines changed: 353 additions & 1 deletion

File tree

util/buildflags/attests.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func (a *Attest) ToPB() *controllerapi.Attest {
9191

9292
func (a *Attest) MarshalJSON() ([]byte, error) {
9393
m := make(map[string]interface{}, len(a.Attrs)+2)
94-
for k, v := range m {
94+
for k, v := range a.Attrs {
9595
m[k] = v
9696
}
9797
m["type"] = a.Type

util/buildflags/attests_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package buildflags
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
"github.com/zclconf/go-cty/cty"
9+
)
10+
11+
func TestAttests(t *testing.T) {
12+
t.Run("MarshalJSON", func(t *testing.T) {
13+
attests := Attests{
14+
{Type: "provenance", Attrs: map[string]string{"mode": "max"}},
15+
{Type: "sbom", Disabled: true},
16+
}
17+
18+
expected := `[{"type":"provenance","mode":"max"},{"type":"sbom","disabled":true}]`
19+
actual, err := json.Marshal(attests)
20+
require.NoError(t, err)
21+
require.JSONEq(t, expected, string(actual))
22+
})
23+
24+
t.Run("UnmarshalJSON", func(t *testing.T) {
25+
in := `[{"type":"provenance","mode":"max"},{"type":"sbom","disabled":true}]`
26+
27+
var actual Attests
28+
err := json.Unmarshal([]byte(in), &actual)
29+
require.NoError(t, err)
30+
31+
expected := Attests{
32+
{Type: "provenance", Attrs: map[string]string{"mode": "max"}},
33+
{Type: "sbom", Disabled: true, Attrs: map[string]string{}},
34+
}
35+
require.Equal(t, expected, actual)
36+
})
37+
38+
t.Run("FromCtyValue", func(t *testing.T) {
39+
in := cty.TupleVal([]cty.Value{
40+
cty.ObjectVal(map[string]cty.Value{
41+
"type": cty.StringVal("provenance"),
42+
"mode": cty.StringVal("max"),
43+
}),
44+
cty.StringVal("type=sbom,disabled=true"),
45+
})
46+
47+
var actual Attests
48+
err := actual.FromCtyValue(in, nil)
49+
require.NoError(t, err)
50+
51+
expected := Attests{
52+
{Type: "provenance", Attrs: map[string]string{"mode": "max"}},
53+
{Type: "sbom", Disabled: true, Attrs: map[string]string{}},
54+
}
55+
require.Equal(t, expected, actual)
56+
})
57+
58+
t.Run("ToCtyValue", func(t *testing.T) {
59+
attests := Attests{
60+
{Type: "provenance", Attrs: map[string]string{"mode": "max"}},
61+
{Type: "sbom", Disabled: true},
62+
}
63+
64+
actual := attests.ToCtyValue()
65+
expected := cty.ListVal([]cty.Value{
66+
cty.MapVal(map[string]cty.Value{
67+
"type": cty.StringVal("provenance"),
68+
"mode": cty.StringVal("max"),
69+
}),
70+
cty.MapVal(map[string]cty.Value{
71+
"type": cty.StringVal("sbom"),
72+
"disabled": cty.StringVal("true"),
73+
}),
74+
})
75+
76+
result := actual.Equals(expected)
77+
require.True(t, result.True())
78+
})
79+
}

util/buildflags/cache_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package buildflags
22

33
import (
4+
"encoding/json"
45
"testing"
56

67
"github.com/docker/buildx/controller/pb"
78
"github.com/stretchr/testify/require"
9+
"github.com/zclconf/go-cty/cty"
810
)
911

1012
func TestCacheOptions_DerivedVars(t *testing.T) {
@@ -37,3 +39,73 @@ func TestCacheOptions_DerivedVars(t *testing.T) {
3739
},
3840
}, cacheFrom)
3941
}
42+
43+
func TestCacheOptions(t *testing.T) {
44+
t.Run("MarshalJSON", func(t *testing.T) {
45+
cache := CacheOptions{
46+
{Type: "registry", Attrs: map[string]string{"ref": "user/app:cache"}},
47+
{Type: "local", Attrs: map[string]string{"src": "path/to/cache"}},
48+
}
49+
50+
expected := `[{"type":"registry","ref":"user/app:cache"},{"type":"local","src":"path/to/cache"}]`
51+
actual, err := json.Marshal(cache)
52+
require.NoError(t, err)
53+
require.JSONEq(t, expected, string(actual))
54+
})
55+
56+
t.Run("UnmarshalJSON", func(t *testing.T) {
57+
in := `[{"type":"registry","ref":"user/app:cache"},{"type":"local","src":"path/to/cache"}]`
58+
59+
var actual CacheOptions
60+
err := json.Unmarshal([]byte(in), &actual)
61+
require.NoError(t, err)
62+
63+
expected := CacheOptions{
64+
{Type: "registry", Attrs: map[string]string{"ref": "user/app:cache"}},
65+
{Type: "local", Attrs: map[string]string{"src": "path/to/cache"}},
66+
}
67+
require.Equal(t, expected, actual)
68+
})
69+
70+
t.Run("FromCtyValue", func(t *testing.T) {
71+
in := cty.TupleVal([]cty.Value{
72+
cty.ObjectVal(map[string]cty.Value{
73+
"type": cty.StringVal("registry"),
74+
"ref": cty.StringVal("user/app:cache"),
75+
}),
76+
cty.StringVal("type=local,src=path/to/cache"),
77+
})
78+
79+
var actual CacheOptions
80+
err := actual.FromCtyValue(in, nil)
81+
require.NoError(t, err)
82+
83+
expected := CacheOptions{
84+
{Type: "registry", Attrs: map[string]string{"ref": "user/app:cache"}},
85+
{Type: "local", Attrs: map[string]string{"src": "path/to/cache"}},
86+
}
87+
require.Equal(t, expected, actual)
88+
})
89+
90+
t.Run("ToCtyValue", func(t *testing.T) {
91+
attests := CacheOptions{
92+
{Type: "registry", Attrs: map[string]string{"ref": "user/app:cache"}},
93+
{Type: "local", Attrs: map[string]string{"src": "path/to/cache"}},
94+
}
95+
96+
actual := attests.ToCtyValue()
97+
expected := cty.ListVal([]cty.Value{
98+
cty.MapVal(map[string]cty.Value{
99+
"type": cty.StringVal("registry"),
100+
"ref": cty.StringVal("user/app:cache"),
101+
}),
102+
cty.MapVal(map[string]cty.Value{
103+
"type": cty.StringVal("local"),
104+
"src": cty.StringVal("path/to/cache"),
105+
}),
106+
})
107+
108+
result := actual.Equals(expected)
109+
require.True(t, result.True())
110+
})
111+
}

util/buildflags/secrets.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package buildflags
22

33
import (
4+
"encoding/json"
45
"strings"
56

67
controllerapi "github.com/docker/buildx/controller/pb"
@@ -73,6 +74,22 @@ func (s *Secret) ToPB() *controllerapi.Secret {
7374
}
7475
}
7576

77+
func (s *Secret) UnmarshalJSON(data []byte) error {
78+
var v struct {
79+
ID string `json:"id,omitempty"`
80+
FilePath string `json:"src,omitempty"`
81+
Env string `json:"env,omitempty"`
82+
}
83+
if err := json.Unmarshal(data, &v); err != nil {
84+
return err
85+
}
86+
87+
s.ID = v.ID
88+
s.FilePath = v.FilePath
89+
s.Env = v.Env
90+
return nil
91+
}
92+
7693
func (s *Secret) UnmarshalText(text []byte) error {
7794
value := string(text)
7895
fields, err := csvvalue.Fields(value, nil)

util/buildflags/secrets_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package buildflags
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
"github.com/zclconf/go-cty/cty"
9+
)
10+
11+
func TestSecrets(t *testing.T) {
12+
t.Run("MarshalJSON", func(t *testing.T) {
13+
secrets := Secrets{
14+
{ID: "mysecret", FilePath: "/local/secret"},
15+
{ID: "mysecret2", Env: "TOKEN"},
16+
}
17+
18+
expected := `[{"id":"mysecret","src":"/local/secret"},{"id":"mysecret2","env":"TOKEN"}]`
19+
actual, err := json.Marshal(secrets)
20+
require.NoError(t, err)
21+
require.JSONEq(t, expected, string(actual))
22+
})
23+
24+
t.Run("UnmarshalJSON", func(t *testing.T) {
25+
in := `[{"id":"mysecret","src":"/local/secret"},{"id":"mysecret2","env":"TOKEN"}]`
26+
27+
var actual Secrets
28+
err := json.Unmarshal([]byte(in), &actual)
29+
require.NoError(t, err)
30+
31+
expected := Secrets{
32+
{ID: "mysecret", FilePath: "/local/secret"},
33+
{ID: "mysecret2", Env: "TOKEN"},
34+
}
35+
require.Equal(t, expected, actual)
36+
})
37+
38+
t.Run("FromCtyValue", func(t *testing.T) {
39+
in := cty.TupleVal([]cty.Value{
40+
cty.ObjectVal(map[string]cty.Value{
41+
"id": cty.StringVal("mysecret"),
42+
"src": cty.StringVal("/local/secret"),
43+
}),
44+
cty.ObjectVal(map[string]cty.Value{
45+
"id": cty.StringVal("mysecret2"),
46+
"env": cty.StringVal("TOKEN"),
47+
}),
48+
})
49+
50+
var actual Secrets
51+
err := actual.FromCtyValue(in, nil)
52+
require.NoError(t, err)
53+
54+
expected := Secrets{
55+
{ID: "mysecret", FilePath: "/local/secret"},
56+
{ID: "mysecret2", Env: "TOKEN"},
57+
}
58+
require.Equal(t, expected, actual)
59+
})
60+
61+
t.Run("ToCtyValue", func(t *testing.T) {
62+
secrets := Secrets{
63+
{ID: "mysecret", FilePath: "/local/secret"},
64+
{ID: "mysecret2", Env: "TOKEN"},
65+
}
66+
67+
actual := secrets.ToCtyValue()
68+
expected := cty.ListVal([]cty.Value{
69+
cty.ObjectVal(map[string]cty.Value{
70+
"id": cty.StringVal("mysecret"),
71+
"src": cty.StringVal("/local/secret"),
72+
"env": cty.StringVal(""),
73+
}),
74+
cty.ObjectVal(map[string]cty.Value{
75+
"id": cty.StringVal("mysecret2"),
76+
"src": cty.StringVal(""),
77+
"env": cty.StringVal("TOKEN"),
78+
}),
79+
})
80+
81+
result := actual.Equals(expected)
82+
require.True(t, result.True())
83+
})
84+
}

util/buildflags/ssh.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package buildflags
22

33
import (
44
"cmp"
5+
"encoding/json"
56
"slices"
67
"strings"
78

@@ -76,6 +77,20 @@ func (s *SSH) ToPB() *controllerapi.SSH {
7677
}
7778
}
7879

80+
func (s *SSH) UnmarshalJSON(data []byte) error {
81+
var v struct {
82+
ID string `json:"id,omitempty"`
83+
Paths []string `json:"paths,omitempty"`
84+
}
85+
if err := json.Unmarshal(data, &v); err != nil {
86+
return err
87+
}
88+
89+
s.ID = v.ID
90+
s.Paths = v.Paths
91+
return nil
92+
}
93+
7994
func (s *SSH) UnmarshalText(text []byte) error {
8095
parts := strings.SplitN(string(text), "=", 2)
8196

0 commit comments

Comments
 (0)