bake: various fixes for composable attributes

This changes how the composable attributes are implemented and provides
various fixes to the first iteration.

Cache-from and cache-to now no longer print sensitive values that are
automatically added. These automatically added attributes are added when
the protobuf is created rather than at the time of parsing so they will
no longer be printed. If they are part of the original configuration
file, they will still be printed.

Empty strings will now be skipped. This was the original behavior and
composable attributes removed this functionality accidentally. This
functionality is now restored.

This also expands the available syntax that works with each of the
composable attributes. 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.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg
2024-12-18 10:26:15 -06:00
parent 567361d494
commit 5dd4ae0335
20 changed files with 927 additions and 352 deletions

View File

@@ -21,7 +21,7 @@ func CanonicalizeAttest(attestType string, in string) string {
}
func ParseAttests(in []string) ([]*controllerapi.Attest, error) {
out := []*controllerapi.Attest{}
var out []*controllerapi.Attest
found := map[string]struct{}{}
for _, in := range in {
in := in

View File

@@ -15,6 +15,41 @@ import (
jsoncty "github.com/zclconf/go-cty/cty/json"
)
type CacheOptions []*CacheOptionsEntry
func (o CacheOptions) Merge(other CacheOptions) CacheOptions {
if other == nil {
return o.Normalize()
} else if o == nil {
return other.Normalize()
}
return append(o, other...).Normalize()
}
func (o CacheOptions) Normalize() CacheOptions {
if len(o) == 0 {
return nil
}
return removeDupes(o)
}
func (o CacheOptions) ToPB() []*controllerapi.CacheOptionsEntry {
if len(o) == 0 {
return nil
}
var outs []*controllerapi.CacheOptionsEntry
for _, entry := range o {
pb := entry.ToPB()
if !isActive(pb) {
continue
}
outs = append(outs, pb)
}
return outs
}
type CacheOptionsEntry struct {
Type string `json:"type"`
Attrs map[string]string `json:"attrs,omitempty"`
@@ -46,10 +81,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 +112,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 +146,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 +168,22 @@ func (e *CacheOptionsEntry) validate(gv interface{}) error {
}
func ParseCacheEntry(in []string) ([]*controllerapi.CacheOptionsEntry, error) {
outs := make([]*controllerapi.CacheOptionsEntry, 0, len(in))
if len(in) == 0 {
return nil, nil
}
opts := make(CacheOptions, 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 = append(opts, &out)
}
return outs, nil
return opts.ToPB(), nil
}
func addGithubToken(ci *CacheOptionsEntry) {
func addGithubToken(ci *controllerapi.CacheOptionsEntry) {
if ci.Type != "gha" {
return
}
@@ -172,7 +199,7 @@ func addGithubToken(ci *CacheOptionsEntry) {
}
}
func addAwsCredentials(ci *CacheOptionsEntry) {
func addAwsCredentials(ci *controllerapi.CacheOptionsEntry) {
if ci.Type != "s3" {
return
}
@@ -201,3 +228,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"] != ""
}

View File

@@ -0,0 +1,86 @@
package buildflags
import (
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
var cacheOptionsEntryType = sync.OnceValue(func() cty.Type {
return cty.Map(cty.String)
})
func (o *CacheOptions) FromCtyValue(in cty.Value, p cty.Path) error {
got := in.Type()
if got.IsTupleType() || got.IsListType() {
return o.fromCtyValue(in, p)
}
want := cty.List(cacheOptionsEntryType())
return p.NewErrorf("%s", convert.MismatchMessage(got, want))
}
func (o *CacheOptions) fromCtyValue(in cty.Value, p cty.Path) error {
*o = make([]*CacheOptionsEntry, 0, in.LengthInt())
for elem := in.ElementIterator(); elem.Next(); {
_, value := elem.Element()
if isEmpty(value) {
continue
}
entry := &CacheOptionsEntry{}
if err := entry.FromCtyValue(value, p); err != nil {
return err
}
*o = append(*o, entry)
}
return nil
}
func (o CacheOptions) ToCtyValue() cty.Value {
if len(o) == 0 {
return cty.ListValEmpty(cacheOptionsEntryType())
}
vals := make([]cty.Value, len(o))
for i, entry := range o {
vals[i] = entry.ToCtyValue()
}
return cty.ListVal(vals)
}
func (o *CacheOptionsEntry) FromCtyValue(in cty.Value, p cty.Path) error {
if in.Type() == cty.String {
if err := o.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", &o.Type); err != nil {
return err
}
o.Attrs = asMap(m)
return o.validate(in)
}
func (o *CacheOptionsEntry) ToCtyValue() cty.Value {
if o == nil {
return cty.NullVal(cty.Map(cty.String))
}
vals := make(map[string]cty.Value, len(o.Attrs)+1)
for k, v := range o.Attrs {
vals[k] = cty.StringVal(v)
}
vals["type"] = cty.StringVal(o.Type)
return cty.MapVal(vals)
}

View File

@@ -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)
}

View File

@@ -11,8 +11,13 @@ func ParseContextNames(values []string) (map[string]string, error) {
if len(values) == 0 {
return nil, nil
}
result := make(map[string]string, len(values))
for _, value := range values {
if value == "" {
continue
}
kv := strings.SplitN(value, "=", 2)
if len(kv) != 2 {
return nil, errors.Errorf("invalid context value: %s, expected key=value", value)

View File

@@ -1,183 +0,0 @@
package buildflags
import (
"encoding"
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"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 {
return err
}
e.Attrs = asMap(m)
return e.validate(in)
}
return unmarshalTextFallback(in, e, err)
}
func (e *CacheOptionsEntry) ToCtyValue() cty.Value {
if e == nil {
return cty.NullVal(cty.Map(cty.String))
}
vals := make(map[string]cty.Value, len(e.Attrs)+1)
for k, v := range e.Attrs {
vals[k] = cty.StringVal(v)
}
vals["type"] = cty.StringVal(e.Type)
return cty.MapVal(vals)
}
func (e *ExportEntry) 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 {
return err
}
if err := getAndDelete(m, "dest", &e.Destination); err != nil {
return err
}
e.Attrs = asMap(m)
return e.validate()
}
return unmarshalTextFallback(in, e, err)
}
func (e *ExportEntry) 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)
vals["dest"] = cty.StringVal(e.Destination)
return cty.MapVal(vals)
}
var secretType = sync.OnceValue(func() cty.Type {
return cty.ObjectWithOptionalAttrs(
map[string]cty.Type{
"id": cty.String,
"src": cty.String,
"env": cty.String,
},
[]string{"id", "src", "env"},
)
})
func (e *Secret) FromCtyValue(in cty.Value, p cty.Path) (err error) {
conv, err := convert.Convert(in, secretType())
if err == nil {
if id := conv.GetAttr("id"); !id.IsNull() {
e.ID = id.AsString()
}
if src := conv.GetAttr("src"); !src.IsNull() {
e.FilePath = src.AsString()
}
if env := conv.GetAttr("env"); !env.IsNull() {
e.Env = env.AsString()
}
return nil
}
return unmarshalTextFallback(in, e, err)
}
func (e *Secret) ToCtyValue() cty.Value {
if e == nil {
return cty.NullVal(secretType())
}
return cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal(e.ID),
"src": cty.StringVal(e.FilePath),
"env": cty.StringVal(e.Env),
})
}
var sshType = sync.OnceValue(func() cty.Type {
return cty.ObjectWithOptionalAttrs(
map[string]cty.Type{
"id": cty.String,
"paths": cty.List(cty.String),
},
[]string{"id", "paths"},
)
})
func (e *SSH) FromCtyValue(in cty.Value, p cty.Path) (err error) {
conv, err := convert.Convert(in, sshType())
if err == nil {
if id := conv.GetAttr("id"); !id.IsNull() {
e.ID = id.AsString()
}
if paths := conv.GetAttr("paths"); !paths.IsNull() {
if err := gocty.FromCtyValue(paths, &e.Paths); err != nil {
return err
}
}
return nil
}
return unmarshalTextFallback(in, e, err)
}
func (e *SSH) ToCtyValue() cty.Value {
if e == nil {
return cty.NullVal(sshType())
}
var ctyPaths cty.Value
if len(e.Paths) > 0 {
paths := make([]cty.Value, len(e.Paths))
for i, path := range e.Paths {
paths[i] = cty.StringVal(path)
}
ctyPaths = cty.ListVal(paths)
} else {
ctyPaths = cty.ListValEmpty(cty.String)
}
return cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal(e.ID),
"paths": ctyPaths,
})
}
func getAndDelete(m map[string]cty.Value, attr string, gv interface{}) error {
if v, ok := m[attr]; ok {
delete(m, attr)
return gocty.FromCtyValue(v, gv)
}
return nil
}
func asMap(m map[string]cty.Value) map[string]string {
out := make(map[string]string, len(m))
for k, v := range m {
out[k] = v.AsString()
}
return out
}
func unmarshalTextFallback[V encoding.TextUnmarshaler](in cty.Value, v V, inErr error) (outErr error) {
// Attempt to convert this type to a string.
conv, err := convert.Convert(in, cty.String)
if err != nil {
// Cannot convert. Do not attempt to convert and return the original error.
return inErr
}
// Conversion was successful. Use UnmarshalText on the string and return any
// errors associated with that.
return v.UnmarshalText([]byte(conv.AsString()))
}

View File

@@ -5,6 +5,10 @@ import "github.com/moby/buildkit/util/entitlements"
func ParseEntitlements(in []string) ([]entitlements.Entitlement, error) {
out := make([]entitlements.Entitlement, 0, len(in))
for _, v := range in {
if v == "" {
continue
}
e, err := entitlements.Parse(v)
if err != nil {
return nil, err

View File

@@ -16,6 +16,39 @@ import (
"github.com/tonistiigi/go-csvvalue"
)
type Exports []*ExportEntry
func (e Exports) Merge(other Exports) Exports {
if other == nil {
e.Normalize()
return e
} else if e == nil {
other.Normalize()
return other
}
return append(e, other...).Normalize()
}
func (e Exports) Normalize() Exports {
if len(e) == 0 {
return nil
}
return removeDupes(e)
}
func (e Exports) ToPB() []*controllerapi.ExportEntry {
if len(e) == 0 {
return nil
}
entries := make([]*controllerapi.ExportEntry, len(e))
for i, entry := range e {
entries[i] = entry.ToPB()
}
return entries
}
type ExportEntry struct {
Type string `json:"type"`
Attrs map[string]string `json:"attrs,omitempty"`
@@ -131,18 +164,23 @@ func (e *ExportEntry) validate() error {
}
func ParseExports(inp []string) ([]*controllerapi.ExportEntry, error) {
var outs []*controllerapi.ExportEntry
if len(inp) == 0 {
return nil, nil
}
export := make(Exports, 0, len(inp))
for _, s := range inp {
if s == "" {
continue
}
var out ExportEntry
if err := out.UnmarshalText([]byte(s)); err != nil {
return nil, err
}
outs = append(outs, out.ToPB())
export = append(export, &out)
}
return outs, nil
return export.ToPB(), nil
}
func ParseAnnotations(inp []string) (map[exptypes.AnnotationKey]string, error) {
@@ -153,6 +191,10 @@ func ParseAnnotations(inp []string) (map[exptypes.AnnotationKey]string, error) {
annotations := make(map[exptypes.AnnotationKey]string)
for _, inp := range inp {
if inp == "" {
continue
}
k, v, ok := strings.Cut(inp, "=")
if !ok {
return nil, errors.Errorf("invalid annotation %q, expected key=value", inp)

View File

@@ -0,0 +1,90 @@
package buildflags
import (
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
var exportEntryType = sync.OnceValue(func() cty.Type {
return cty.Map(cty.String)
})
func (e *Exports) FromCtyValue(in cty.Value, p cty.Path) error {
got := in.Type()
if got.IsTupleType() || got.IsListType() {
return e.fromCtyValue(in, p)
}
want := cty.List(exportEntryType())
return p.NewErrorf("%s", convert.MismatchMessage(got, want))
}
func (e *Exports) fromCtyValue(in cty.Value, p cty.Path) error {
*e = make([]*ExportEntry, 0, in.LengthInt())
for elem := in.ElementIterator(); elem.Next(); {
_, value := elem.Element()
if isEmpty(value) {
continue
}
entry := &ExportEntry{}
if err := entry.FromCtyValue(value, p); err != nil {
return err
}
*e = append(*e, entry)
}
return nil
}
func (e Exports) ToCtyValue() cty.Value {
if len(e) == 0 {
return cty.ListValEmpty(exportEntryType())
}
vals := make([]cty.Value, len(e))
for i, entry := range e {
vals[i] = entry.ToCtyValue()
}
return cty.ListVal(vals)
}
func (e *ExportEntry) 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
}
if err := getAndDelete(m, "dest", &e.Destination); err != nil {
return err
}
e.Attrs = asMap(m)
return e.validate()
}
func (e *ExportEntry) 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)
vals["dest"] = cty.StringVal(e.Destination)
return cty.MapVal(vals)
}

View File

@@ -8,6 +8,39 @@ import (
"github.com/tonistiigi/go-csvvalue"
)
type Secrets []*Secret
func (s Secrets) Merge(other Secrets) Secrets {
if other == nil {
s.Normalize()
return s
} else if s == nil {
other.Normalize()
return other
}
return append(s, other...).Normalize()
}
func (s Secrets) Normalize() Secrets {
if len(s) == 0 {
return nil
}
return removeDupes(s)
}
func (s Secrets) ToPB() []*controllerapi.Secret {
if len(s) == 0 {
return nil
}
entries := make([]*controllerapi.Secret, len(s))
for i, entry := range s {
entries[i] = entry.ToPB()
}
return entries
}
type Secret struct {
ID string `json:"id,omitempty"`
FilePath string `json:"src,omitempty"`
@@ -85,6 +118,10 @@ func (s *Secret) UnmarshalText(text []byte) error {
func ParseSecretSpecs(sl []string) ([]*controllerapi.Secret, error) {
fs := make([]*controllerapi.Secret, 0, len(sl))
for _, v := range sl {
if v == "" {
continue
}
s, err := parseSecret(v)
if err != nil {
return nil, err

View File

@@ -0,0 +1,96 @@
package buildflags
import (
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
var secretType = sync.OnceValue(func() cty.Type {
return cty.ObjectWithOptionalAttrs(
map[string]cty.Type{
"id": cty.String,
"src": cty.String,
"env": cty.String,
},
[]string{"id", "src", "env"},
)
})
func (s *Secrets) FromCtyValue(in cty.Value, p cty.Path) error {
got := in.Type()
if got.IsTupleType() || got.IsListType() {
return s.fromCtyValue(in, p)
}
want := cty.List(secretType())
return p.NewErrorf("%s", convert.MismatchMessage(got, want))
}
func (s *Secrets) fromCtyValue(in cty.Value, p cty.Path) error {
*s = make([]*Secret, 0, in.LengthInt())
for elem := in.ElementIterator(); elem.Next(); {
_, value := elem.Element()
if isEmpty(value) {
continue
}
entry := &Secret{}
if err := entry.FromCtyValue(value, p); err != nil {
return err
}
*s = append(*s, entry)
}
return nil
}
func (s Secrets) ToCtyValue() cty.Value {
if len(s) == 0 {
return cty.ListValEmpty(secretType())
}
vals := make([]cty.Value, len(s))
for i, entry := range s {
vals[i] = entry.ToCtyValue()
}
return cty.ListVal(vals)
}
func (e *Secret) 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, secretType())
if err != nil {
return err
}
if id := conv.GetAttr("id"); !id.IsNull() {
e.ID = id.AsString()
}
if src := conv.GetAttr("src"); !src.IsNull() {
e.FilePath = src.AsString()
}
if env := conv.GetAttr("env"); !env.IsNull() {
e.Env = env.AsString()
}
return nil
}
func (e *Secret) ToCtyValue() cty.Value {
if e == nil {
return cty.NullVal(secretType())
}
return cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal(e.ID),
"src": cty.StringVal(e.FilePath),
"env": cty.StringVal(e.Env),
})
}

View File

@@ -9,6 +9,39 @@ import (
"github.com/moby/buildkit/util/gitutil"
)
type SSHKeys []*SSH
func (s SSHKeys) Merge(other SSHKeys) SSHKeys {
if other == nil {
s.Normalize()
return s
} else if s == nil {
other.Normalize()
return other
}
return append(s, other...).Normalize()
}
func (s SSHKeys) Normalize() SSHKeys {
if len(s) == 0 {
return nil
}
return removeDupes(s)
}
func (s SSHKeys) ToPB() []*controllerapi.SSH {
if len(s) == 0 {
return nil
}
entries := make([]*controllerapi.SSH, len(s))
for i, entry := range s {
entries[i] = entry.ToPB()
}
return entries
}
type SSH struct {
ID string `json:"id,omitempty" cty:"id"`
Paths []string `json:"paths,omitempty" cty:"paths"`
@@ -62,6 +95,10 @@ func ParseSSHSpecs(sl []string) ([]*controllerapi.SSH, error) {
}
for _, s := range sl {
if s == "" {
continue
}
var out SSH
if err := out.UnmarshalText([]byte(s)); err != nil {
return nil, err

105
util/buildflags/ssh_cty.go Normal file
View File

@@ -0,0 +1,105 @@
package buildflags
import (
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/gocty"
)
var sshType = sync.OnceValue(func() cty.Type {
return cty.ObjectWithOptionalAttrs(
map[string]cty.Type{
"id": cty.String,
"paths": cty.List(cty.String),
},
[]string{"id", "paths"},
)
})
func (s *SSHKeys) FromCtyValue(in cty.Value, p cty.Path) error {
got := in.Type()
if got.IsTupleType() || got.IsListType() {
return s.fromCtyValue(in, p)
}
want := cty.List(sshType())
return p.NewErrorf("%s", convert.MismatchMessage(got, want))
}
func (s *SSHKeys) fromCtyValue(in cty.Value, p cty.Path) error {
*s = make([]*SSH, 0, in.LengthInt())
for elem := in.ElementIterator(); elem.Next(); {
_, value := elem.Element()
if isEmpty(value) {
continue
}
entry := &SSH{}
if err := entry.FromCtyValue(value, p); err != nil {
return err
}
*s = append(*s, entry)
}
return nil
}
func (s SSHKeys) ToCtyValue() cty.Value {
if len(s) == 0 {
return cty.ListValEmpty(sshType())
}
vals := make([]cty.Value, len(s))
for i, entry := range s {
vals[i] = entry.ToCtyValue()
}
return cty.ListVal(vals)
}
func (e *SSH) 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, sshType())
if err != nil {
return err
}
if id := conv.GetAttr("id"); !id.IsNull() {
e.ID = id.AsString()
}
if paths := conv.GetAttr("paths"); !paths.IsNull() {
if err := gocty.FromCtyValue(paths, &e.Paths); err != nil {
return err
}
}
return nil
}
func (e *SSH) ToCtyValue() cty.Value {
if e == nil {
return cty.NullVal(sshType())
}
var ctyPaths cty.Value
if len(e.Paths) > 0 {
paths := make([]cty.Value, len(e.Paths))
for i, path := range e.Paths {
paths[i] = cty.StringVal(path)
}
ctyPaths = cty.ListVal(paths)
} else {
ctyPaths = cty.ListValEmpty(cty.String)
}
return cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal(e.ID),
"paths": ctyPaths,
})
}

54
util/buildflags/utils.go Normal file
View File

@@ -0,0 +1,54 @@
package buildflags
import (
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
)
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
}
func getAndDelete(m map[string]cty.Value, attr string, gv interface{}) error {
if v, ok := m[attr]; ok {
delete(m, attr)
return gocty.FromCtyValue(v, gv)
}
return nil
}
func asMap(m map[string]cty.Value) map[string]string {
out := make(map[string]string, len(m))
for k, v := range m {
out[k] = v.AsString()
}
return out
}
func isEmpty(v cty.Value) bool {
return v.Type() == cty.String && v.AsString() == ""
}