Merge pull request #2814 from jsternberg/bake-composable-attributes-phase2

bake: various fixes for composable attributes
This commit is contained in:
Tõnis Tiigi 2024-12-18 09:35:35 -08:00 committed by GitHub
commit 3771fe2034
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 927 additions and 352 deletions

View File

@ -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.CacheOptionsEntry `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
CacheTo []*buildflags.CacheOptionsEntry `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.SSH = removeDupes(t.SSH)
t.Secrets = t.Secrets.Normalize()
t.SSH = t.SSH.Normalize()
t.Platforms = removeDupesStr(t.Platforms)
t.CacheFrom = removeDupes(t.CacheFrom)
t.CacheTo = removeDupes(t.CacheTo)
t.Outputs = removeDupes(t.Outputs)
t.CacheFrom = t.CacheFrom.Normalize()
t.CacheTo = t.CacheTo.Normalize()
t.Outputs = t.Outputs.Normalize()
t.NoCacheFilter = removeDupesStr(t.NoCacheFilter)
t.Ulimits = removeDupesStr(t.Ulimits)
@ -815,16 +815,16 @@ 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
}
if t2.CacheFrom != nil { // merge
t.CacheFrom = append(t.CacheFrom, t2.CacheFrom...)
t.CacheFrom = t.CacheFrom.Merge(t2.CacheFrom)
}
if t2.CacheTo != nil { // no merge
t.CacheTo = t2.CacheTo
@ -1333,30 +1333,19 @@ 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
}
bo.Session = append(bo.Session, secretAttachment)
var sshSpecs []*controllerapi.SSH
if len(t.SSH) > 0 {
sshSpecs := make([]*controllerapi.SSH, len(t.SSH))
for i, s := range t.SSH {
sshSpecs[i] = s.ToPB()
}
} else if buildflags.IsGitSSH(bi.ContextPath) || (inp != nil && buildflags.IsGitSSH(inp.URL)) {
sshSpecs = []*controllerapi.SSH{{ID: "default"}}
bo.SSHSpecs = t.SSH.ToPB()
if len(bo.SSHSpecs) == 0 && buildflags.IsGitSSH(bi.ContextPath) || (inp != nil && buildflags.IsGitSSH(inp.URL)) {
bo.SSHSpecs = []*controllerapi.SSH{{ID: "default"}}
}
bo.SSHSpecs = sshSpecs
sshAttachment, err := controllerapi.CreateSSH(sshSpecs)
sshAttachment, err := controllerapi.CreateSSH(bo.SSHSpecs)
if err != nil {
return nil, err
}
@ -1372,24 +1361,14 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
}
}
cacheImports := make([]*controllerapi.CacheOptionsEntry, len(t.CacheFrom))
for i, ci := range t.CacheFrom {
cacheImports[i] = ci.ToPB()
if t.CacheFrom != nil {
bo.CacheFrom = controllerapi.CreateCaches(t.CacheFrom.ToPB())
}
bo.CacheFrom = controllerapi.CreateCaches(cacheImports)
cacheExports := make([]*controllerapi.CacheOptionsEntry, len(t.CacheTo))
for i, ce := range t.CacheTo {
cacheExports[i] = ce.ToPB()
}
bo.CacheTo = controllerapi.CreateCaches(cacheExports)
outputs := make([]*controllerapi.ExportEntry, len(t.Outputs))
for i, output := range t.Outputs {
outputs[i] = output.ToPB()
if t.CacheTo != nil {
bo.CacheTo = controllerapi.CreateCaches(t.CacheTo.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
}
@ -1434,34 +1413,6 @@ func defaultTarget() *Target {
return &Target{}
}
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 removeDupesStr(s []string) []string {
i := 0
seen := make(map[string]struct{}, len(s))
@ -1616,6 +1567,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
@ -1625,9 +1580,13 @@ func parseArrValue[T any, PT arrValue[T]](s []string) ([]*T, error) {
return outputs, nil
}
func parseCacheArrValues(s []string) ([]*buildflags.CacheOptionsEntry, error) {
outs := make([]*buildflags.CacheOptionsEntry, 0, len(s))
func parseCacheArrValues(s []string) (buildflags.CacheOptions, error) {
var outs buildflags.CacheOptions
for _, in := range s {
if in == "" {
continue
}
if !strings.Contains(in, "=") {
// This is ref only format. Each field in the CSV is its own entry.
fields, err := csvvalue.Fields(in, nil)

View File

@ -2019,6 +2019,26 @@ target "app" {
})
}
// https://github.com/docker/buildx/pull/428
// https://github.com/docker/buildx/issues/2822
func TestEmptyAttribute(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
Data: []byte(`
target "app" {
output = [""]
}
`),
}
ctx := context.TODO()
m, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil, &EntitlementConf{})
require.Equal(t, 1, len(m))
require.Len(t, m["app"].Outputs, 0)
require.NoError(t, err)
}
func stringify[V fmt.Stringer](values []V) []string {
s := make([]string, len(values))
for i, v := range values {

View File

@ -353,28 +353,28 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error {
if err != nil {
return err
}
t.CacheFrom = removeDupes(append(t.CacheFrom, cacheFrom...))
t.CacheFrom = t.CacheFrom.Merge(cacheFrom)
}
if len(xb.CacheTo) > 0 {
cacheTo, err := parseCacheArrValues(xb.CacheTo)
if err != nil {
return err
}
t.CacheTo = removeDupes(append(t.CacheTo, cacheTo...))
t.CacheTo = t.CacheTo.Merge(cacheTo)
}
if len(xb.Secrets) > 0 {
secrets, err := parseArrValue[buildflags.Secret](xb.Secrets)
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

View File

@ -606,7 +606,7 @@ func TestHCLAttrsCapsuleType(t *testing.T) {
target "app" {
cache-from = [
{ type = "registry", ref = "user/app:cache" },
{ type = "local", src = "path/to/cache" },
"type=local,src=path/to/cache",
]
cache-to = [
@ -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))
@ -649,13 +650,14 @@ func TestHCLAttrsCapsuleTypeVars(t *testing.T) {
target "app" {
cache-from = [
{ type = "registry", ref = "user/app:cache" },
{ type = "local", src = "path/to/cache" },
"type=local,src=path/to/cache",
]
cache-to = [ target.app.cache-from[0] ]
output = [
{ type = "oci", dest = "../out.tar" },
"type=local,dest=../out",
]
secret = [
@ -674,7 +676,7 @@ func TestHCLAttrsCapsuleTypeVars(t *testing.T) {
output = [ "type=oci,dest=../${foo}.tar" ]
secret = [
{ id = target.app.output[0].type, src = "/local/secret" },
{ id = target.app.output[0].type, src = "/${target.app.cache-from[1].type}/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))

View File

@ -10,44 +10,60 @@ import (
"github.com/zclconf/go-cty/cty/gocty"
)
type CapsuleValue interface {
// FromCtyValue will initialize this value using a cty.Value.
FromCtyValue(in cty.Value, path cty.Path) error
type ToCtyValueConverter interface {
// ToCtyValue will convert this capsule value into a native
// cty.Value. This should not return a capsule type.
ToCtyValue() cty.Value
}
type FromCtyValueConverter interface {
// FromCtyValue will initialize this value using a cty.Value.
FromCtyValue(in cty.Value, path cty.Path) error
}
type extensionType int
const (
nativeTypeExtension extensionType = iota
unwrapCapsuleValueExtension extensionType = iota
)
func impliedTypeExt(rt reflect.Type, _ cty.Path) (cty.Type, error) {
if rt.AssignableTo(capsuleValueType) {
if rt.Kind() != reflect.Pointer {
rt = reflect.PointerTo(rt)
}
if isCapsuleType(rt) {
return capsuleValueCapsuleType(rt), nil
}
return cty.NilType, errdefs.ErrNotImplemented
}
var (
capsuleValueType = reflect.TypeFor[CapsuleValue]()
capsuleValueTypes sync.Map
)
func isCapsuleType(rt reflect.Type) bool {
fromCtyValueType := reflect.TypeFor[FromCtyValueConverter]()
toCtyValueType := reflect.TypeFor[ToCtyValueConverter]()
return rt.Implements(fromCtyValueType) && rt.Implements(toCtyValueType)
}
var capsuleValueTypes sync.Map
func capsuleValueCapsuleType(rt reflect.Type) cty.Type {
if val, loaded := capsuleValueTypes.Load(rt); loaded {
if rt.Kind() != reflect.Pointer {
panic("capsule value must be a pointer")
}
elem := rt.Elem()
if val, loaded := capsuleValueTypes.Load(elem); loaded {
return val.(cty.Type)
}
// First time used.
ety := cty.CapsuleWithOps(rt.Name(), rt.Elem(), &cty.CapsuleOps{
toCtyValueType := reflect.TypeFor[ToCtyValueConverter]()
// First time used. Initialize new capsule ops.
ops := &cty.CapsuleOps{
ConversionTo: func(_ cty.Type) func(cty.Value, cty.Path) (any, error) {
return func(in cty.Value, p cty.Path) (any, error) {
rv := reflect.New(rt.Elem()).Interface()
if err := rv.(CapsuleValue).FromCtyValue(in, p); err != nil {
rv := reflect.New(elem).Interface()
if err := rv.(FromCtyValueConverter).FromCtyValue(in, p); err != nil {
return nil, err
}
return rv, nil
@ -55,30 +71,39 @@ func capsuleValueCapsuleType(rt reflect.Type) cty.Type {
},
ConversionFrom: func(want cty.Type) func(any, cty.Path) (cty.Value, error) {
return func(in any, _ cty.Path) (cty.Value, error) {
v := in.(CapsuleValue).ToCtyValue()
rv := reflect.ValueOf(in).Convert(toCtyValueType)
v := rv.Interface().(ToCtyValueConverter).ToCtyValue()
return convert.Convert(v, want)
}
},
ExtensionData: func(key any) any {
switch key {
case nativeTypeExtension:
zero := reflect.Zero(rt).Interface()
return zero.(CapsuleValue).ToCtyValue().Type()
default:
return nil
}
},
})
case unwrapCapsuleValueExtension:
zero := reflect.Zero(elem).Interface()
if conv, ok := zero.(ToCtyValueConverter); ok {
return conv.ToCtyValue().Type()
}
// Attempt to store the new type. Use whichever was loaded first in the case of a race condition.
val, _ := capsuleValueTypes.LoadOrStore(rt, ety)
zero = reflect.Zero(rt).Interface()
if conv, ok := zero.(ToCtyValueConverter); ok {
return conv.ToCtyValue().Type()
}
}
return nil
},
}
// Attempt to store the new type. Use whichever was loaded first in the case
// of a race condition.
ety := cty.CapsuleWithOps(elem.Name(), elem, ops)
val, _ := capsuleValueTypes.LoadOrStore(elem, ety)
return val.(cty.Type)
}
// ToNativeValue will convert a value to only native cty types which will
// remove capsule types if possible.
func ToNativeValue(in cty.Value) cty.Value {
want := toNativeType(in.Type())
// UnwrapCtyValue will unwrap capsule type values into their native cty value
// equivalents if possible.
func UnwrapCtyValue(in cty.Value) cty.Value {
want := toCtyValueType(in.Type())
if in.Type().Equals(want) {
return in
} else if out, err := convert.Convert(in, want); err == nil {
@ -87,17 +112,17 @@ func ToNativeValue(in cty.Value) cty.Value {
return cty.NullVal(want)
}
func toNativeType(in cty.Type) cty.Type {
func toCtyValueType(in cty.Type) cty.Type {
if et := in.MapElementType(); et != nil {
return cty.Map(toNativeType(*et))
return cty.Map(toCtyValueType(*et))
}
if et := in.SetElementType(); et != nil {
return cty.Set(toNativeType(*et))
return cty.Set(toCtyValueType(*et))
}
if et := in.ListElementType(); et != nil {
return cty.List(toNativeType(*et))
return cty.List(toCtyValueType(*et))
}
if in.IsObjectType() {
@ -105,7 +130,7 @@ func toNativeType(in cty.Type) cty.Type {
inAttrTypes := in.AttributeTypes()
outAttrTypes := make(map[string]cty.Type, len(inAttrTypes))
for name, typ := range inAttrTypes {
outAttrTypes[name] = toNativeType(typ)
outAttrTypes[name] = toCtyValueType(typ)
if in.AttributeOptional(name) {
optional = append(optional, name)
}
@ -117,13 +142,13 @@ func toNativeType(in cty.Type) cty.Type {
inTypes := in.TupleElementTypes()
outTypes := make([]cty.Type, len(inTypes))
for i, typ := range inTypes {
outTypes[i] = toNativeType(typ)
outTypes[i] = toCtyValueType(typ)
}
return cty.Tuple(outTypes)
}
if in.IsCapsuleType() {
if out := in.CapsuleExtensionData(nativeTypeExtension); out != nil {
if out := in.CapsuleExtensionData(unwrapCapsuleValueExtension); out != nil {
return out.(cty.Type)
}
return cty.DynamicPseudoType
@ -137,5 +162,5 @@ func ToCtyValue(val any, ty cty.Type) (cty.Value, error) {
if err != nil {
return out, err
}
return ToNativeValue(out), nil
return UnwrapCtyValue(out), nil
}

View File

@ -33,6 +33,7 @@ func bakeCmd(sb integration.Sandbox, opts ...cmdOpt) (string, error) {
var bakeTests = []func(t *testing.T, sb integration.Sandbox){
testBakePrint,
testBakePrintSensitive,
testBakeLocal,
testBakeLocalMulti,
testBakeRemote,
@ -86,7 +87,8 @@ target "build" {
HELLO = "foo"
}
}
`)},
`),
},
{
"Compose",
"compose.yml",
@ -97,7 +99,8 @@ services:
context: .
args:
HELLO: foo
`)},
`),
},
}
for _, tc := range testCases {
@ -158,6 +161,125 @@ RUN echo "Hello ${HELLO}"
}
}
func testBakePrintSensitive(t *testing.T, sb integration.Sandbox) {
testCases := []struct {
name string
f string
dt []byte
}{
{
"HCL",
"docker-bake.hcl",
[]byte(`
target "build" {
args = {
HELLO = "foo"
}
cache-from = [
"type=gha,token=abc",
"type=s3,region=us-west-2,bucket=my_bucket,name=my_image",
]
}
`),
},
{
"Compose",
"compose.yml",
[]byte(`
services:
build:
build:
context: .
args:
HELLO: foo
cache_from:
- type=gha,token=abc
- type=s3,region=us-west-2,bucket=my_bucket,name=my_image
`),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
dir := tmpdir(
t,
fstest.CreateFile(tc.f, tc.dt, 0600),
fstest.CreateFile("Dockerfile", []byte(`
FROM busybox
ARG HELLO
RUN echo "Hello ${HELLO}"
`), 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs("bake", "--print", "build"),
withEnv(
"ACTIONS_RUNTIME_TOKEN=sensitive_token",
"ACTIONS_CACHE_URL=https://cache.github.com",
"AWS_ACCESS_KEY_ID=definitely_dont_look_here",
"AWS_SECRET_ACCESS_KEY=hackers_please_dont_steal",
"AWS_SESSION_TOKEN=not_a_mitm_attack",
),
)
stdout := bytes.Buffer{}
stderr := bytes.Buffer{}
cmd.Stdout = &stdout
cmd.Stderr = &stderr
require.NoError(t, cmd.Run(), stdout.String(), stderr.String())
var def struct {
Group map[string]*bake.Group `json:"group,omitempty"`
Target map[string]*bake.Target `json:"target"`
}
require.NoError(t, json.Unmarshal(stdout.Bytes(), &def))
require.Len(t, def.Group, 1)
require.Contains(t, def.Group, "default")
require.Equal(t, []string{"build"}, def.Group["default"].Targets)
require.Len(t, def.Target, 1)
require.Contains(t, def.Target, "build")
require.Equal(t, ".", *def.Target["build"].Context)
require.Equal(t, "Dockerfile", *def.Target["build"].Dockerfile)
require.Equal(t, map[string]*string{"HELLO": ptrstr("foo")}, def.Target["build"].Args)
require.NotNil(t, def.Target["build"].CacheFrom)
require.Len(t, def.Target["build"].CacheFrom, 2)
require.JSONEq(t, `{
"group": {
"default": {
"targets": [
"build"
]
}
},
"target": {
"build": {
"context": ".",
"dockerfile": "Dockerfile",
"args": {
"HELLO": "foo"
},
"cache-from": [
{
"type": "gha",
"token": "abc"
},
{
"type": "s3",
"region": "us-west-2",
"bucket": "my_bucket",
"name": "my_image"
}
]
}
}
}
`, stdout.String())
})
}
}
func testBakeLocal(t *testing.T, sb integration.Sandbox) {
dockerfile := []byte(`
FROM scratch

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() == ""
}