Skip to content

Commit

Permalink
buildflags: marshal attestations into json with extra attributes corr…
Browse files Browse the repository at this point in the history
…ectly

`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]>
  • Loading branch information
jsternberg committed Jan 21, 2025
1 parent b4a0dee commit 3aed658
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 1 deletion.
2 changes: 1 addition & 1 deletion util/buildflags/attests.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (a *Attest) ToPB() *controllerapi.Attest {

func (a *Attest) MarshalJSON() ([]byte, error) {
m := make(map[string]interface{}, len(a.Attrs)+2)
for k, v := range m {
for k, v := range a.Attrs {
m[k] = v
}
m["type"] = a.Type
Expand Down
79 changes: 79 additions & 0 deletions util/buildflags/attests_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package buildflags

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty"
)

func TestAttests(t *testing.T) {
t.Run("MarshalJSON", func(t *testing.T) {
attests := Attests{
{Type: "provenance", Attrs: map[string]string{"mode": "max"}},
{Type: "sbom", Disabled: true},
}

expected := `[{"type":"provenance","mode":"max"},{"type":"sbom","disabled":true}]`
actual, err := json.Marshal(attests)
require.NoError(t, err)
require.JSONEq(t, expected, string(actual))
})

t.Run("UnmarshalJSON", func(t *testing.T) {
in := `[{"type":"provenance","mode":"max"},{"type":"sbom","disabled":true}]`

var actual Attests
err := json.Unmarshal([]byte(in), &actual)
require.NoError(t, err)

expected := Attests{
{Type: "provenance", Attrs: map[string]string{"mode": "max"}},
{Type: "sbom", Disabled: true, Attrs: map[string]string{}},
}
require.Equal(t, expected, actual)
})

t.Run("FromCtyValue", func(t *testing.T) {
in := cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("provenance"),
"mode": cty.StringVal("max"),
}),
cty.StringVal("type=sbom,disabled=true"),
})

var actual Attests
err := actual.FromCtyValue(in, nil)
require.NoError(t, err)

expected := Attests{
{Type: "provenance", Attrs: map[string]string{"mode": "max"}},
{Type: "sbom", Disabled: true, Attrs: map[string]string{}},
}
require.Equal(t, expected, actual)
})

t.Run("ToCtyValue", func(t *testing.T) {
attests := Attests{
{Type: "provenance", Attrs: map[string]string{"mode": "max"}},
{Type: "sbom", Disabled: true},
}

actual := attests.ToCtyValue()
expected := cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"type": cty.StringVal("provenance"),
"mode": cty.StringVal("max"),
}),
cty.MapVal(map[string]cty.Value{
"type": cty.StringVal("sbom"),
"disabled": cty.StringVal("true"),
}),
})

result := actual.Equals(expected)
require.True(t, result.True())
})
}
72 changes: 72 additions & 0 deletions util/buildflags/cache_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package buildflags

import (
"encoding/json"
"testing"

"github.com/docker/buildx/controller/pb"
"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty"
)

func TestCacheOptions_DerivedVars(t *testing.T) {
Expand Down Expand Up @@ -37,3 +39,73 @@ func TestCacheOptions_DerivedVars(t *testing.T) {
},
}, cacheFrom)
}

func TestCacheOptions(t *testing.T) {
t.Run("MarshalJSON", func(t *testing.T) {
cache := CacheOptions{
{Type: "registry", Attrs: map[string]string{"ref": "user/app:cache"}},
{Type: "local", Attrs: map[string]string{"src": "path/to/cache"}},
}

expected := `[{"type":"registry","ref":"user/app:cache"},{"type":"local","src":"path/to/cache"}]`
actual, err := json.Marshal(cache)
require.NoError(t, err)
require.JSONEq(t, expected, string(actual))
})

t.Run("UnmarshalJSON", func(t *testing.T) {
in := `[{"type":"registry","ref":"user/app:cache"},{"type":"local","src":"path/to/cache"}]`

var actual CacheOptions
err := json.Unmarshal([]byte(in), &actual)
require.NoError(t, err)

expected := CacheOptions{
{Type: "registry", Attrs: map[string]string{"ref": "user/app:cache"}},
{Type: "local", Attrs: map[string]string{"src": "path/to/cache"}},
}
require.Equal(t, expected, actual)
})

t.Run("FromCtyValue", func(t *testing.T) {
in := cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"type": cty.StringVal("registry"),
"ref": cty.StringVal("user/app:cache"),
}),
cty.StringVal("type=local,src=path/to/cache"),
})

var actual CacheOptions
err := actual.FromCtyValue(in, nil)
require.NoError(t, err)

expected := CacheOptions{
{Type: "registry", Attrs: map[string]string{"ref": "user/app:cache"}},
{Type: "local", Attrs: map[string]string{"src": "path/to/cache"}},
}
require.Equal(t, expected, actual)
})

t.Run("ToCtyValue", func(t *testing.T) {
attests := CacheOptions{
{Type: "registry", Attrs: map[string]string{"ref": "user/app:cache"}},
{Type: "local", Attrs: map[string]string{"src": "path/to/cache"}},
}

actual := attests.ToCtyValue()
expected := cty.ListVal([]cty.Value{
cty.MapVal(map[string]cty.Value{
"type": cty.StringVal("registry"),
"ref": cty.StringVal("user/app:cache"),
}),
cty.MapVal(map[string]cty.Value{
"type": cty.StringVal("local"),
"src": cty.StringVal("path/to/cache"),
}),
})

result := actual.Equals(expected)
require.True(t, result.True())
})
}
17 changes: 17 additions & 0 deletions util/buildflags/secrets.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package buildflags

import (
"encoding/json"
"strings"

controllerapi "github.com/docker/buildx/controller/pb"
Expand Down Expand Up @@ -73,6 +74,22 @@ func (s *Secret) ToPB() *controllerapi.Secret {
}
}

func (s *Secret) UnmarshalJSON(data []byte) error {
var v struct {
ID string `json:"id,omitempty"`
FilePath string `json:"src,omitempty"`
Env string `json:"env,omitempty"`
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}

s.ID = v.ID
s.FilePath = v.FilePath
s.Env = v.Env
return nil
}

func (s *Secret) UnmarshalText(text []byte) error {
value := string(text)
fields, err := csvvalue.Fields(value, nil)
Expand Down
84 changes: 84 additions & 0 deletions util/buildflags/secrets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package buildflags

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty"
)

func TestSecrets(t *testing.T) {
t.Run("MarshalJSON", func(t *testing.T) {
secrets := Secrets{
{ID: "mysecret", FilePath: "/local/secret"},
{ID: "mysecret2", Env: "TOKEN"},
}

expected := `[{"id":"mysecret","src":"/local/secret"},{"id":"mysecret2","env":"TOKEN"}]`
actual, err := json.Marshal(secrets)
require.NoError(t, err)
require.JSONEq(t, expected, string(actual))
})

t.Run("UnmarshalJSON", func(t *testing.T) {
in := `[{"id":"mysecret","src":"/local/secret"},{"id":"mysecret2","env":"TOKEN"}]`

var actual Secrets
err := json.Unmarshal([]byte(in), &actual)
require.NoError(t, err)

expected := Secrets{
{ID: "mysecret", FilePath: "/local/secret"},
{ID: "mysecret2", Env: "TOKEN"},
}
require.Equal(t, expected, actual)
})

t.Run("FromCtyValue", func(t *testing.T) {
in := cty.TupleVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("mysecret"),
"src": cty.StringVal("/local/secret"),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("mysecret2"),
"env": cty.StringVal("TOKEN"),
}),
})

var actual Secrets
err := actual.FromCtyValue(in, nil)
require.NoError(t, err)

expected := Secrets{
{ID: "mysecret", FilePath: "/local/secret"},
{ID: "mysecret2", Env: "TOKEN"},
}
require.Equal(t, expected, actual)
})

t.Run("ToCtyValue", func(t *testing.T) {
secrets := Secrets{
{ID: "mysecret", FilePath: "/local/secret"},
{ID: "mysecret2", Env: "TOKEN"},
}

actual := secrets.ToCtyValue()
expected := cty.ListVal([]cty.Value{
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("mysecret"),
"src": cty.StringVal("/local/secret"),
"env": cty.StringVal(""),
}),
cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("mysecret2"),
"src": cty.StringVal(""),
"env": cty.StringVal("TOKEN"),
}),
})

result := actual.Equals(expected)
require.True(t, result.True())
})
}
15 changes: 15 additions & 0 deletions util/buildflags/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package buildflags

import (
"cmp"
"encoding/json"
"slices"
"strings"

Expand Down Expand Up @@ -76,6 +77,20 @@ func (s *SSH) ToPB() *controllerapi.SSH {
}
}

func (s *SSH) UnmarshalJSON(data []byte) error {
var v struct {
ID string `json:"id,omitempty"`
Paths []string `json:"paths,omitempty"`
}
if err := json.Unmarshal(data, &v); err != nil {
return err
}

s.ID = v.ID
s.Paths = v.Paths
return nil
}

func (s *SSH) UnmarshalText(text []byte) error {
parts := strings.SplitN(string(text), "=", 2)

Expand Down
Loading

0 comments on commit 3aed658

Please sign in to comment.