mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-07-10 05:27:07 +08:00
bake: implement composable attributes for attestations
Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
@ -1,7 +1,9 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -10,6 +12,167 @@ import (
|
||||
"github.com/tonistiigi/go-csvvalue"
|
||||
)
|
||||
|
||||
type Attests []*Attest
|
||||
|
||||
func (a Attests) Merge(other Attests) Attests {
|
||||
if other == nil {
|
||||
a.Normalize()
|
||||
return a
|
||||
} else if a == nil {
|
||||
other.Normalize()
|
||||
return other
|
||||
}
|
||||
|
||||
return append(a, other...).Normalize()
|
||||
}
|
||||
|
||||
func (a Attests) Normalize() Attests {
|
||||
if len(a) == 0 {
|
||||
return nil
|
||||
}
|
||||
return removeAttestDupes(a)
|
||||
}
|
||||
|
||||
func (a Attests) ToPB() []*controllerapi.Attest {
|
||||
if len(a) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
entries := make([]*controllerapi.Attest, len(a))
|
||||
for i, entry := range a {
|
||||
entries[i] = entry.ToPB()
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
type Attest struct {
|
||||
Type string `json:"type"`
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
Attrs map[string]string `json:"attrs,omitempty"`
|
||||
}
|
||||
|
||||
func (a *Attest) Equal(other *Attest) bool {
|
||||
if a.Type != other.Type || a.Disabled != other.Disabled {
|
||||
return false
|
||||
}
|
||||
return maps.Equal(a.Attrs, other.Attrs)
|
||||
}
|
||||
|
||||
func (a *Attest) String() string {
|
||||
var b csvBuilder
|
||||
if a.Type != "" {
|
||||
b.Write("type", a.Type)
|
||||
}
|
||||
if a.Disabled {
|
||||
b.Write("disabled", "true")
|
||||
}
|
||||
if len(a.Attrs) > 0 {
|
||||
b.WriteAttributes(a.Attrs)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (a *Attest) ToPB() *controllerapi.Attest {
|
||||
var b csvBuilder
|
||||
if a.Type != "" {
|
||||
b.Write("type", a.Type)
|
||||
}
|
||||
if a.Disabled {
|
||||
b.Write("disabled", "true")
|
||||
}
|
||||
b.WriteAttributes(a.Attrs)
|
||||
|
||||
return &controllerapi.Attest{
|
||||
Type: a.Type,
|
||||
Disabled: a.Disabled,
|
||||
Attrs: b.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Attest) MarshalJSON() ([]byte, error) {
|
||||
m := make(map[string]interface{}, len(a.Attrs)+2)
|
||||
for k, v := range m {
|
||||
m[k] = v
|
||||
}
|
||||
m["type"] = a.Type
|
||||
if a.Disabled {
|
||||
m["disabled"] = true
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (a *Attest) UnmarshalJSON(data []byte) error {
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if typ, ok := m["type"]; ok {
|
||||
a.Type, ok = typ.(string)
|
||||
if !ok {
|
||||
return errors.Errorf("attest type must be a string")
|
||||
}
|
||||
delete(m, "type")
|
||||
}
|
||||
|
||||
if disabled, ok := m["disabled"]; ok {
|
||||
a.Disabled, ok = disabled.(bool)
|
||||
if !ok {
|
||||
return errors.Errorf("attest disabled attribute must be a boolean")
|
||||
}
|
||||
delete(m, "disabled")
|
||||
}
|
||||
|
||||
attrs := make(map[string]string, len(m))
|
||||
for k, v := range m {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
return errors.Errorf("attest attribute %q must be a string", k)
|
||||
}
|
||||
attrs[k] = s
|
||||
}
|
||||
a.Attrs = attrs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Attest) UnmarshalText(text []byte) error {
|
||||
in := string(text)
|
||||
fields, err := csvvalue.Fields(in, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.Attrs = map[string]string{}
|
||||
for _, field := range fields {
|
||||
key, value, ok := strings.Cut(field, "=")
|
||||
if !ok {
|
||||
return errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
key = strings.TrimSpace(strings.ToLower(key))
|
||||
|
||||
switch key {
|
||||
case "type":
|
||||
a.Type = value
|
||||
case "disabled":
|
||||
disabled, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid value %s", field)
|
||||
}
|
||||
a.Disabled = disabled
|
||||
default:
|
||||
a.Attrs[key] = value
|
||||
}
|
||||
}
|
||||
return a.validate()
|
||||
}
|
||||
|
||||
func (a *Attest) validate() error {
|
||||
if a.Type == "" {
|
||||
return errors.Errorf("attestation type not specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CanonicalizeAttest(attestType string, in string) string {
|
||||
if in == "" {
|
||||
return ""
|
||||
@ -21,21 +184,34 @@ func CanonicalizeAttest(attestType string, in string) string {
|
||||
}
|
||||
|
||||
func ParseAttests(in []string) ([]*controllerapi.Attest, error) {
|
||||
var out []*controllerapi.Attest
|
||||
found := map[string]struct{}{}
|
||||
for _, in := range in {
|
||||
in := in
|
||||
attest, err := ParseAttest(in)
|
||||
if err != nil {
|
||||
var outs []*Attest
|
||||
for _, s := range in {
|
||||
var out Attest
|
||||
if err := out.UnmarshalText([]byte(s)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outs = append(outs, &out)
|
||||
}
|
||||
return ConvertAttests(outs)
|
||||
}
|
||||
|
||||
// ConvertAttests converts Attestations for the controller API from
|
||||
// the ones in this package.
|
||||
//
|
||||
// Attestations of the same type will cause an error. Some tools,
|
||||
// like bake, remove the duplicates before calling this function.
|
||||
func ConvertAttests(in []*Attest) ([]*controllerapi.Attest, error) {
|
||||
out := make([]*controllerapi.Attest, 0, len(in))
|
||||
|
||||
// Check for dupplicate attestations while we convert them
|
||||
// to the controller API.
|
||||
found := map[string]struct{}{}
|
||||
for _, attest := range in {
|
||||
if _, ok := found[attest.Type]; ok {
|
||||
return nil, errors.Errorf("duplicate attestation field %s", attest.Type)
|
||||
}
|
||||
found[attest.Type] = struct{}{}
|
||||
|
||||
out = append(out, attest)
|
||||
out = append(out, attest.ToPB())
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@ -77,3 +253,17 @@ func ParseAttest(in string) (*controllerapi.Attest, error) {
|
||||
|
||||
return &attest, nil
|
||||
}
|
||||
|
||||
func removeAttestDupes(s []*Attest) []*Attest {
|
||||
res := []*Attest{}
|
||||
m := map[string]int{}
|
||||
for _, att := range s {
|
||||
if i, ok := m[att.Type]; ok {
|
||||
res[i] = att
|
||||
} else {
|
||||
m[att.Type] = len(res)
|
||||
res = append(res, att)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
97
util/buildflags/attests_cty.go
Normal file
97
util/buildflags/attests_cty.go
Normal file
@ -0,0 +1,97 @@
|
||||
package buildflags
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
)
|
||||
|
||||
var attestType = sync.OnceValue(func() cty.Type {
|
||||
return cty.Map(cty.String)
|
||||
})
|
||||
|
||||
func (e *Attests) FromCtyValue(in cty.Value, p cty.Path) error {
|
||||
got := in.Type()
|
||||
if got.IsTupleType() || got.IsListType() {
|
||||
return e.fromCtyValue(in, p)
|
||||
}
|
||||
|
||||
want := cty.List(attestType())
|
||||
return p.NewErrorf("%s", convert.MismatchMessage(got, want))
|
||||
}
|
||||
|
||||
func (e *Attests) fromCtyValue(in cty.Value, p cty.Path) error {
|
||||
*e = make([]*Attest, 0, in.LengthInt())
|
||||
for elem := in.ElementIterator(); elem.Next(); {
|
||||
_, value := elem.Element()
|
||||
|
||||
entry := &Attest{}
|
||||
if err := entry.FromCtyValue(value, p); err != nil {
|
||||
return err
|
||||
}
|
||||
*e = append(*e, entry)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e Attests) ToCtyValue() cty.Value {
|
||||
if len(e) == 0 {
|
||||
return cty.ListValEmpty(attestType())
|
||||
}
|
||||
|
||||
vals := make([]cty.Value, len(e))
|
||||
for i, entry := range e {
|
||||
vals[i] = entry.ToCtyValue()
|
||||
}
|
||||
return cty.ListVal(vals)
|
||||
}
|
||||
|
||||
func (e *Attest) 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
|
||||
}
|
||||
|
||||
e.Attrs = map[string]string{}
|
||||
for it := conv.ElementIterator(); it.Next(); {
|
||||
k, v := it.Element()
|
||||
switch key := k.AsString(); key {
|
||||
case "type":
|
||||
e.Type = v.AsString()
|
||||
case "disabled":
|
||||
b, err := strconv.ParseBool(v.AsString())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.Disabled = b
|
||||
default:
|
||||
e.Attrs[key] = v.AsString()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Attest) ToCtyValue() cty.Value {
|
||||
if e == nil {
|
||||
return cty.NullVal(cty.Map(cty.String))
|
||||
}
|
||||
|
||||
vals := make(map[string]cty.Value, len(e.Attrs)+2)
|
||||
for k, v := range e.Attrs {
|
||||
vals[k] = cty.StringVal(v)
|
||||
}
|
||||
vals["type"] = cty.StringVal(e.Type)
|
||||
if e.Disabled {
|
||||
vals["disabled"] = cty.StringVal("true")
|
||||
}
|
||||
return cty.MapVal(vals)
|
||||
}
|
Reference in New Issue
Block a user