Skip to content

Commit 24f3a1d

Browse files
jsternbergcrazy-max
authored andcommitted
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 <[email protected]>
1 parent 8e30c46 commit 24f3a1d

File tree

7 files changed

+353
-1
lines changed

7 files changed

+353
-1
lines changed

Diff for: util/buildflags/attests.go

+1-1
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

Diff for: util/buildflags/attests_test.go

+79
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+
}

Diff for: util/buildflags/cache_test.go

+72
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+
}

Diff for: util/buildflags/secrets.go

+17
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)

Diff for: util/buildflags/secrets_test.go

+84
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+
}

Diff for: util/buildflags/ssh.go

+15
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)