From 753705da7dddd62d60fc5e583b6348697dc217c7 Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Mon, 9 Dec 2024 10:04:26 -0600 Subject: [PATCH] bake: migrate other buildflags to allow different types This migrates the other buildflags to use the same method that cache options now uses. Signed-off-by: Jonathan A. Sternberg --- bake/bake.go | 76 ++++++++-------- bake/compose.go | 6 +- bake/hcl_test.go | 6 +- util/buildflags/cty.go | 157 --------------------------------- util/buildflags/export.go | 40 ++++++++- util/buildflags/export_cty.go | 86 ++++++++++++++++++ util/buildflags/secrets.go | 33 +++++++ util/buildflags/secrets_cty.go | 92 +++++++++++++++++++ util/buildflags/ssh.go | 33 +++++++ util/buildflags/ssh_cty.go | 101 +++++++++++++++++++++ util/buildflags/utils.go | 21 +++++ 11 files changed, 445 insertions(+), 206 deletions(-) delete mode 100644 util/buildflags/cty.go create mode 100644 util/buildflags/export_cty.go create mode 100644 util/buildflags/secrets_cty.go create mode 100644 util/buildflags/ssh_cty.go diff --git a/bake/bake.go b/bake/bake.go index 571cf2cb9b8..cd4c1e01b9c 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -698,30 +698,30 @@ type Target struct { // Inherits is the only field that cannot be overridden with --set Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"` - Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"` - Attest []string `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"` - Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"` - Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"` - Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"` - DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"` - Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"` - Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"` - Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"` - CacheFrom buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"` - CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"` - Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"` - Secrets []*buildflags.Secret `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"` - SSH []*buildflags.SSH `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"` - Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"` - Outputs []*buildflags.ExportEntry `json:"output,omitempty" hcl:"output,optional" cty:"output"` - Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"` - NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"` - NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"` - NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"` - ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"` - Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"` - Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"` - Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"` + Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"` + Attest []string `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"` + Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"` + Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"` + Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"` + DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"` + Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"` + Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"` + Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"` + CacheFrom buildflags.CacheOptions `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"` + CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"` + Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"` + Secrets buildflags.Secrets `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"` + SSH buildflags.SSHKeys `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"` + Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"` + Outputs buildflags.Exports `json:"output,omitempty" hcl:"output,optional" cty:"output"` + Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"` + NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"` + NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"` + NoCacheFilter []string `json:"no-cache-filter,omitempty" hcl:"no-cache-filter,optional" cty:"no-cache-filter"` + ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"` + Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"` + Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"` + Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"` // IMPORTANT: if you add more fields here, do not forget to update newOverrides/AddOverrides and docs/bake-reference.md. // linked is a private field to mark a target used as a linked one @@ -739,12 +739,12 @@ func (t *Target) normalize() { t.Annotations = removeDupesStr(t.Annotations) t.Attest = removeAttestDupes(t.Attest) t.Tags = removeDupesStr(t.Tags) - t.Secrets = removeDupes(t.Secrets) + t.Secrets = t.Secrets.Normalize() t.SSH = removeDupes(t.SSH) t.Platforms = removeDupesStr(t.Platforms) t.CacheFrom = t.CacheFrom.Normalize() t.CacheTo = t.CacheTo.Normalize() - t.Outputs = removeDupes(t.Outputs) + t.Outputs = t.Outputs.Normalize() t.NoCacheFilter = removeDupesStr(t.NoCacheFilter) t.Ulimits = removeDupesStr(t.Ulimits) @@ -815,10 +815,10 @@ func (t *Target) Merge(t2 *Target) { t.Attest = removeAttestDupes(t.Attest) } if t2.Secrets != nil { // merge - t.Secrets = append(t.Secrets, t2.Secrets...) + t.Secrets = t.Secrets.Merge(t2.Secrets) } if t2.SSH != nil { // merge - t.SSH = append(t.SSH, t2.SSH...) + t.SSH = t.SSH.Merge(t2.SSH) } if t2.Platforms != nil { // no merge t.Platforms = t2.Platforms @@ -1333,13 +1333,8 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { } bo.Platforms = platforms - secrets := make([]*controllerapi.Secret, len(t.Secrets)) - for i, s := range t.Secrets { - secrets[i] = s.ToPB() - } - bo.SecretSpecs = secrets - - secretAttachment, err := controllerapi.CreateSecrets(secrets) + bo.SecretSpecs = t.Secrets.ToPB() + secretAttachment, err := controllerapi.CreateSecrets(bo.SecretSpecs) if err != nil { return nil, err } @@ -1379,12 +1374,7 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) { bo.CacheTo = controllerapi.CreateCaches(t.CacheTo.ToPB()) } - outputs := make([]*controllerapi.ExportEntry, len(t.Outputs)) - for i, output := range t.Outputs { - outputs[i] = output.ToPB() - } - - bo.Exports, bo.ExportsLocalPathsTemporary, err = controllerapi.CreateExports(outputs) + bo.Exports, bo.ExportsLocalPathsTemporary, err = controllerapi.CreateExports(t.Outputs.ToPB()) if err != nil { return nil, err } @@ -1611,6 +1601,10 @@ type arrValue[B any] interface { func parseArrValue[T any, PT arrValue[T]](s []string) ([]*T, error) { outputs := make([]*T, 0, len(s)) for _, text := range s { + if text == "" { + continue + } + output := new(T) if err := PT(output).UnmarshalText([]byte(text)); err != nil { return nil, err diff --git a/bake/compose.go b/bake/compose.go index 4cb4bbc26ba..58cfc80c953 100644 --- a/bake/compose.go +++ b/bake/compose.go @@ -367,14 +367,14 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error { if err != nil { return err } - t.Secrets = removeDupes(append(t.Secrets, secrets...)) + t.Secrets = t.Secrets.Merge(secrets) } if len(xb.SSH) > 0 { ssh, err := parseArrValue[buildflags.SSH](xb.SSH) if err != nil { return err } - t.SSH = removeDupes(append(t.SSH, ssh...)) + t.SSH = t.SSH.Merge(ssh) slices.SortFunc(t.SSH, func(a, b *buildflags.SSH) int { return a.Less(b) }) @@ -387,7 +387,7 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error { if err != nil { return err } - t.Outputs = removeDupes(append(t.Outputs, outputs...)) + t.Outputs = t.Outputs.Merge(outputs) } if xb.Pull != nil { t.Pull = xb.Pull diff --git a/bake/hcl_test.go b/bake/hcl_test.go index 63dcea42940..a0d37db15e4 100644 --- a/bake/hcl_test.go +++ b/bake/hcl_test.go @@ -615,6 +615,7 @@ func TestHCLAttrsCapsuleType(t *testing.T) { output = [ { type = "oci", dest = "../out.tar" }, + "type=local,dest=../out", ] secret = [ @@ -633,7 +634,7 @@ func TestHCLAttrsCapsuleType(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(c.Targets)) - require.Equal(t, []string{"type=oci,dest=../out.tar"}, stringify(c.Targets[0].Outputs)) + require.Equal(t, []string{"type=local,dest=../out", "type=oci,dest=../out.tar"}, stringify(c.Targets[0].Outputs)) require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(c.Targets[0].CacheFrom)) require.Equal(t, []string{"type=local,dest=path/to/cache"}, stringify(c.Targets[0].CacheTo)) require.Equal(t, []string{"id=mysecret,src=/local/secret", "id=mysecret2,env=TOKEN"}, stringify(c.Targets[0].Secrets)) @@ -656,6 +657,7 @@ func TestHCLAttrsCapsuleTypeVars(t *testing.T) { output = [ { type = "oci", dest = "../out.tar" }, + "type=local,dest=../out", ] secret = [ @@ -696,7 +698,7 @@ func TestHCLAttrsCapsuleTypeVars(t *testing.T) { } app := findTarget(t, "app") - require.Equal(t, []string{"type=oci,dest=../out.tar"}, stringify(app.Outputs)) + require.Equal(t, []string{"type=local,dest=../out", "type=oci,dest=../out.tar"}, stringify(app.Outputs)) require.Equal(t, []string{"type=local,src=path/to/cache", "user/app:cache"}, stringify(app.CacheFrom)) require.Equal(t, []string{"user/app:cache"}, stringify(app.CacheTo)) require.Equal(t, []string{"id=mysecret,src=/local/secret"}, stringify(app.Secrets)) diff --git a/util/buildflags/cty.go b/util/buildflags/cty.go deleted file mode 100644 index a80b7f763b9..00000000000 --- a/util/buildflags/cty.go +++ /dev/null @@ -1,157 +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 *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())) -} diff --git a/util/buildflags/export.go b/util/buildflags/export.go index 90eaa743d7b..6c9d3844fa7 100644 --- a/util/buildflags/export.go +++ b/util/buildflags/export.go @@ -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,19 @@ 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 { 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) { diff --git a/util/buildflags/export_cty.go b/util/buildflags/export_cty.go new file mode 100644 index 00000000000..fd5b2ebaa78 --- /dev/null +++ b/util/buildflags/export_cty.go @@ -0,0 +1,86 @@ +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() + + 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) +} diff --git a/util/buildflags/secrets.go b/util/buildflags/secrets.go index d480e6860a9..4ae76f9878d 100644 --- a/util/buildflags/secrets.go +++ b/util/buildflags/secrets.go @@ -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"` diff --git a/util/buildflags/secrets_cty.go b/util/buildflags/secrets_cty.go new file mode 100644 index 00000000000..2d460b2ad1c --- /dev/null +++ b/util/buildflags/secrets_cty.go @@ -0,0 +1,92 @@ +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() + + 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), + }) +} diff --git a/util/buildflags/ssh.go b/util/buildflags/ssh.go index e7944db8eb3..4ed29b14167 100644 --- a/util/buildflags/ssh.go +++ b/util/buildflags/ssh.go @@ -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"` diff --git a/util/buildflags/ssh_cty.go b/util/buildflags/ssh_cty.go new file mode 100644 index 00000000000..6eabf393995 --- /dev/null +++ b/util/buildflags/ssh_cty.go @@ -0,0 +1,101 @@ +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() + + 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, + }) +} diff --git a/util/buildflags/utils.go b/util/buildflags/utils.go index 6b69f3a81c5..ed636f097af 100644 --- a/util/buildflags/utils.go +++ b/util/buildflags/utils.go @@ -1,5 +1,10 @@ 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 } @@ -27,3 +32,19 @@ func removeDupes[E comparable[E]](s []E) []E { } 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 +}