mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 09:17:49 +08:00
Merge pull request #2814 from jsternberg/bake-composable-attributes-phase2
bake: various fixes for composable attributes
This commit is contained in:
commit
3771fe2034
147
bake/bake.go
147
bake/bake.go
@ -698,30 +698,30 @@ type Target struct {
|
|||||||
// Inherits is the only field that cannot be overridden with --set
|
// Inherits is the only field that cannot be overridden with --set
|
||||||
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"`
|
Inherits []string `json:"inherits,omitempty" hcl:"inherits,optional" cty:"inherits"`
|
||||||
|
|
||||||
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
|
Annotations []string `json:"annotations,omitempty" hcl:"annotations,optional" cty:"annotations"`
|
||||||
Attest []string `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
|
Attest []string `json:"attest,omitempty" hcl:"attest,optional" cty:"attest"`
|
||||||
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
|
Context *string `json:"context,omitempty" hcl:"context,optional" cty:"context"`
|
||||||
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
|
Contexts map[string]string `json:"contexts,omitempty" hcl:"contexts,optional" cty:"contexts"`
|
||||||
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
|
Dockerfile *string `json:"dockerfile,omitempty" hcl:"dockerfile,optional" cty:"dockerfile"`
|
||||||
DockerfileInline *string `json:"dockerfile-inline,omitempty" hcl:"dockerfile-inline,optional" cty:"dockerfile-inline"`
|
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"`
|
Args map[string]*string `json:"args,omitempty" hcl:"args,optional" cty:"args"`
|
||||||
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
|
Labels map[string]*string `json:"labels,omitempty" hcl:"labels,optional" cty:"labels"`
|
||||||
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
|
Tags []string `json:"tags,omitempty" hcl:"tags,optional" cty:"tags"`
|
||||||
CacheFrom []*buildflags.CacheOptionsEntry `json:"cache-from,omitempty" hcl:"cache-from,optional" cty:"cache-from"`
|
CacheFrom buildflags.CacheOptions `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"`
|
CacheTo buildflags.CacheOptions `json:"cache-to,omitempty" hcl:"cache-to,optional" cty:"cache-to"`
|
||||||
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
|
Target *string `json:"target,omitempty" hcl:"target,optional" cty:"target"`
|
||||||
Secrets []*buildflags.Secret `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
|
Secrets buildflags.Secrets `json:"secret,omitempty" hcl:"secret,optional" cty:"secret"`
|
||||||
SSH []*buildflags.SSH `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
|
SSH buildflags.SSHKeys `json:"ssh,omitempty" hcl:"ssh,optional" cty:"ssh"`
|
||||||
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
|
Platforms []string `json:"platforms,omitempty" hcl:"platforms,optional" cty:"platforms"`
|
||||||
Outputs []*buildflags.ExportEntry `json:"output,omitempty" hcl:"output,optional" cty:"output"`
|
Outputs buildflags.Exports `json:"output,omitempty" hcl:"output,optional" cty:"output"`
|
||||||
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
|
Pull *bool `json:"pull,omitempty" hcl:"pull,optional" cty:"pull"`
|
||||||
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
|
NoCache *bool `json:"no-cache,omitempty" hcl:"no-cache,optional" cty:"no-cache"`
|
||||||
NetworkMode *string `json:"network,omitempty" hcl:"network,optional" cty:"network"`
|
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"`
|
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"`
|
ShmSize *string `json:"shm-size,omitempty" hcl:"shm-size,optional"`
|
||||||
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"`
|
Ulimits []string `json:"ulimits,omitempty" hcl:"ulimits,optional"`
|
||||||
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
|
Call *string `json:"call,omitempty" hcl:"call,optional" cty:"call"`
|
||||||
Entitlements []string `json:"entitlements,omitempty" hcl:"entitlements,optional" cty:"entitlements"`
|
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.
|
// 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
|
// 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.Annotations = removeDupesStr(t.Annotations)
|
||||||
t.Attest = removeAttestDupes(t.Attest)
|
t.Attest = removeAttestDupes(t.Attest)
|
||||||
t.Tags = removeDupesStr(t.Tags)
|
t.Tags = removeDupesStr(t.Tags)
|
||||||
t.Secrets = removeDupes(t.Secrets)
|
t.Secrets = t.Secrets.Normalize()
|
||||||
t.SSH = removeDupes(t.SSH)
|
t.SSH = t.SSH.Normalize()
|
||||||
t.Platforms = removeDupesStr(t.Platforms)
|
t.Platforms = removeDupesStr(t.Platforms)
|
||||||
t.CacheFrom = removeDupes(t.CacheFrom)
|
t.CacheFrom = t.CacheFrom.Normalize()
|
||||||
t.CacheTo = removeDupes(t.CacheTo)
|
t.CacheTo = t.CacheTo.Normalize()
|
||||||
t.Outputs = removeDupes(t.Outputs)
|
t.Outputs = t.Outputs.Normalize()
|
||||||
t.NoCacheFilter = removeDupesStr(t.NoCacheFilter)
|
t.NoCacheFilter = removeDupesStr(t.NoCacheFilter)
|
||||||
t.Ulimits = removeDupesStr(t.Ulimits)
|
t.Ulimits = removeDupesStr(t.Ulimits)
|
||||||
|
|
||||||
@ -815,16 +815,16 @@ func (t *Target) Merge(t2 *Target) {
|
|||||||
t.Attest = removeAttestDupes(t.Attest)
|
t.Attest = removeAttestDupes(t.Attest)
|
||||||
}
|
}
|
||||||
if t2.Secrets != nil { // merge
|
if t2.Secrets != nil { // merge
|
||||||
t.Secrets = append(t.Secrets, t2.Secrets...)
|
t.Secrets = t.Secrets.Merge(t2.Secrets)
|
||||||
}
|
}
|
||||||
if t2.SSH != nil { // merge
|
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
|
if t2.Platforms != nil { // no merge
|
||||||
t.Platforms = t2.Platforms
|
t.Platforms = t2.Platforms
|
||||||
}
|
}
|
||||||
if t2.CacheFrom != nil { // merge
|
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
|
if t2.CacheTo != nil { // no merge
|
||||||
t.CacheTo = t2.CacheTo
|
t.CacheTo = t2.CacheTo
|
||||||
@ -1333,30 +1333,19 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
|||||||
}
|
}
|
||||||
bo.Platforms = platforms
|
bo.Platforms = platforms
|
||||||
|
|
||||||
secrets := make([]*controllerapi.Secret, len(t.Secrets))
|
bo.SecretSpecs = t.Secrets.ToPB()
|
||||||
for i, s := range t.Secrets {
|
secretAttachment, err := controllerapi.CreateSecrets(bo.SecretSpecs)
|
||||||
secrets[i] = s.ToPB()
|
|
||||||
}
|
|
||||||
bo.SecretSpecs = secrets
|
|
||||||
|
|
||||||
secretAttachment, err := controllerapi.CreateSecrets(secrets)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
bo.Session = append(bo.Session, secretAttachment)
|
bo.Session = append(bo.Session, secretAttachment)
|
||||||
|
|
||||||
var sshSpecs []*controllerapi.SSH
|
bo.SSHSpecs = t.SSH.ToPB()
|
||||||
if len(t.SSH) > 0 {
|
if len(bo.SSHSpecs) == 0 && buildflags.IsGitSSH(bi.ContextPath) || (inp != nil && buildflags.IsGitSSH(inp.URL)) {
|
||||||
sshSpecs := make([]*controllerapi.SSH, len(t.SSH))
|
bo.SSHSpecs = []*controllerapi.SSH{{ID: "default"}}
|
||||||
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 = sshSpecs
|
|
||||||
|
|
||||||
sshAttachment, err := controllerapi.CreateSSH(sshSpecs)
|
sshAttachment, err := controllerapi.CreateSSH(bo.SSHSpecs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1372,24 +1361,14 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheImports := make([]*controllerapi.CacheOptionsEntry, len(t.CacheFrom))
|
if t.CacheFrom != nil {
|
||||||
for i, ci := range t.CacheFrom {
|
bo.CacheFrom = controllerapi.CreateCaches(t.CacheFrom.ToPB())
|
||||||
cacheImports[i] = ci.ToPB()
|
|
||||||
}
|
}
|
||||||
bo.CacheFrom = controllerapi.CreateCaches(cacheImports)
|
if t.CacheTo != nil {
|
||||||
|
bo.CacheTo = controllerapi.CreateCaches(t.CacheTo.ToPB())
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bo.Exports, bo.ExportsLocalPathsTemporary, err = controllerapi.CreateExports(outputs)
|
bo.Exports, bo.ExportsLocalPathsTemporary, err = controllerapi.CreateExports(t.Outputs.ToPB())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1434,34 +1413,6 @@ func defaultTarget() *Target {
|
|||||||
return &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 {
|
func removeDupesStr(s []string) []string {
|
||||||
i := 0
|
i := 0
|
||||||
seen := make(map[string]struct{}, len(s))
|
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) {
|
func parseArrValue[T any, PT arrValue[T]](s []string) ([]*T, error) {
|
||||||
outputs := make([]*T, 0, len(s))
|
outputs := make([]*T, 0, len(s))
|
||||||
for _, text := range s {
|
for _, text := range s {
|
||||||
|
if text == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
output := new(T)
|
output := new(T)
|
||||||
if err := PT(output).UnmarshalText([]byte(text)); err != nil {
|
if err := PT(output).UnmarshalText([]byte(text)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -1625,9 +1580,13 @@ func parseArrValue[T any, PT arrValue[T]](s []string) ([]*T, error) {
|
|||||||
return outputs, nil
|
return outputs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCacheArrValues(s []string) ([]*buildflags.CacheOptionsEntry, error) {
|
func parseCacheArrValues(s []string) (buildflags.CacheOptions, error) {
|
||||||
outs := make([]*buildflags.CacheOptionsEntry, 0, len(s))
|
var outs buildflags.CacheOptions
|
||||||
for _, in := range s {
|
for _, in := range s {
|
||||||
|
if in == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.Contains(in, "=") {
|
if !strings.Contains(in, "=") {
|
||||||
// This is ref only format. Each field in the CSV is its own entry.
|
// This is ref only format. Each field in the CSV is its own entry.
|
||||||
fields, err := csvvalue.Fields(in, nil)
|
fields, err := csvvalue.Fields(in, nil)
|
||||||
|
@ -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 {
|
func stringify[V fmt.Stringer](values []V) []string {
|
||||||
s := make([]string, len(values))
|
s := make([]string, len(values))
|
||||||
for i, v := range values {
|
for i, v := range values {
|
||||||
|
@ -353,28 +353,28 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.CacheFrom = removeDupes(append(t.CacheFrom, cacheFrom...))
|
t.CacheFrom = t.CacheFrom.Merge(cacheFrom)
|
||||||
}
|
}
|
||||||
if len(xb.CacheTo) > 0 {
|
if len(xb.CacheTo) > 0 {
|
||||||
cacheTo, err := parseCacheArrValues(xb.CacheTo)
|
cacheTo, err := parseCacheArrValues(xb.CacheTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.CacheTo = removeDupes(append(t.CacheTo, cacheTo...))
|
t.CacheTo = t.CacheTo.Merge(cacheTo)
|
||||||
}
|
}
|
||||||
if len(xb.Secrets) > 0 {
|
if len(xb.Secrets) > 0 {
|
||||||
secrets, err := parseArrValue[buildflags.Secret](xb.Secrets)
|
secrets, err := parseArrValue[buildflags.Secret](xb.Secrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.Secrets = removeDupes(append(t.Secrets, secrets...))
|
t.Secrets = t.Secrets.Merge(secrets)
|
||||||
}
|
}
|
||||||
if len(xb.SSH) > 0 {
|
if len(xb.SSH) > 0 {
|
||||||
ssh, err := parseArrValue[buildflags.SSH](xb.SSH)
|
ssh, err := parseArrValue[buildflags.SSH](xb.SSH)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
slices.SortFunc(t.SSH, func(a, b *buildflags.SSH) int {
|
||||||
return a.Less(b)
|
return a.Less(b)
|
||||||
})
|
})
|
||||||
@ -387,7 +387,7 @@ func (t *Target) composeExtTarget(exts map[string]interface{}) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.Outputs = removeDupes(append(t.Outputs, outputs...))
|
t.Outputs = t.Outputs.Merge(outputs)
|
||||||
}
|
}
|
||||||
if xb.Pull != nil {
|
if xb.Pull != nil {
|
||||||
t.Pull = xb.Pull
|
t.Pull = xb.Pull
|
||||||
|
@ -606,7 +606,7 @@ func TestHCLAttrsCapsuleType(t *testing.T) {
|
|||||||
target "app" {
|
target "app" {
|
||||||
cache-from = [
|
cache-from = [
|
||||||
{ type = "registry", ref = "user/app:cache" },
|
{ type = "registry", ref = "user/app:cache" },
|
||||||
{ type = "local", src = "path/to/cache" },
|
"type=local,src=path/to/cache",
|
||||||
]
|
]
|
||||||
|
|
||||||
cache-to = [
|
cache-to = [
|
||||||
@ -615,6 +615,7 @@ func TestHCLAttrsCapsuleType(t *testing.T) {
|
|||||||
|
|
||||||
output = [
|
output = [
|
||||||
{ type = "oci", dest = "../out.tar" },
|
{ type = "oci", dest = "../out.tar" },
|
||||||
|
"type=local,dest=../out",
|
||||||
]
|
]
|
||||||
|
|
||||||
secret = [
|
secret = [
|
||||||
@ -633,7 +634,7 @@ func TestHCLAttrsCapsuleType(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, 1, len(c.Targets))
|
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,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{"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))
|
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" {
|
target "app" {
|
||||||
cache-from = [
|
cache-from = [
|
||||||
{ type = "registry", ref = "user/app:cache" },
|
{ 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] ]
|
cache-to = [ target.app.cache-from[0] ]
|
||||||
|
|
||||||
output = [
|
output = [
|
||||||
{ type = "oci", dest = "../out.tar" },
|
{ type = "oci", dest = "../out.tar" },
|
||||||
|
"type=local,dest=../out",
|
||||||
]
|
]
|
||||||
|
|
||||||
secret = [
|
secret = [
|
||||||
@ -674,7 +676,7 @@ func TestHCLAttrsCapsuleTypeVars(t *testing.T) {
|
|||||||
output = [ "type=oci,dest=../${foo}.tar" ]
|
output = [ "type=oci,dest=../${foo}.tar" ]
|
||||||
|
|
||||||
secret = [
|
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")
|
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{"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{"user/app:cache"}, stringify(app.CacheTo))
|
||||||
require.Equal(t, []string{"id=mysecret,src=/local/secret"}, stringify(app.Secrets))
|
require.Equal(t, []string{"id=mysecret,src=/local/secret"}, stringify(app.Secrets))
|
||||||
|
@ -10,44 +10,60 @@ import (
|
|||||||
"github.com/zclconf/go-cty/cty/gocty"
|
"github.com/zclconf/go-cty/cty/gocty"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CapsuleValue interface {
|
type ToCtyValueConverter interface {
|
||||||
// FromCtyValue will initialize this value using a cty.Value.
|
|
||||||
FromCtyValue(in cty.Value, path cty.Path) error
|
|
||||||
|
|
||||||
// ToCtyValue will convert this capsule value into a native
|
// ToCtyValue will convert this capsule value into a native
|
||||||
// cty.Value. This should not return a capsule type.
|
// cty.Value. This should not return a capsule type.
|
||||||
ToCtyValue() cty.Value
|
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
|
type extensionType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
nativeTypeExtension extensionType = iota
|
unwrapCapsuleValueExtension extensionType = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
func impliedTypeExt(rt reflect.Type, _ cty.Path) (cty.Type, error) {
|
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 capsuleValueCapsuleType(rt), nil
|
||||||
}
|
}
|
||||||
return cty.NilType, errdefs.ErrNotImplemented
|
return cty.NilType, errdefs.ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func isCapsuleType(rt reflect.Type) bool {
|
||||||
capsuleValueType = reflect.TypeFor[CapsuleValue]()
|
fromCtyValueType := reflect.TypeFor[FromCtyValueConverter]()
|
||||||
capsuleValueTypes sync.Map
|
toCtyValueType := reflect.TypeFor[ToCtyValueConverter]()
|
||||||
)
|
return rt.Implements(fromCtyValueType) && rt.Implements(toCtyValueType)
|
||||||
|
}
|
||||||
|
|
||||||
|
var capsuleValueTypes sync.Map
|
||||||
|
|
||||||
func capsuleValueCapsuleType(rt reflect.Type) cty.Type {
|
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)
|
return val.(cty.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
// First time used.
|
toCtyValueType := reflect.TypeFor[ToCtyValueConverter]()
|
||||||
ety := cty.CapsuleWithOps(rt.Name(), rt.Elem(), &cty.CapsuleOps{
|
|
||||||
|
// First time used. Initialize new capsule ops.
|
||||||
|
ops := &cty.CapsuleOps{
|
||||||
ConversionTo: func(_ cty.Type) func(cty.Value, cty.Path) (any, error) {
|
ConversionTo: func(_ cty.Type) func(cty.Value, cty.Path) (any, error) {
|
||||||
return func(in cty.Value, p cty.Path) (any, error) {
|
return func(in cty.Value, p cty.Path) (any, error) {
|
||||||
rv := reflect.New(rt.Elem()).Interface()
|
rv := reflect.New(elem).Interface()
|
||||||
if err := rv.(CapsuleValue).FromCtyValue(in, p); err != nil {
|
if err := rv.(FromCtyValueConverter).FromCtyValue(in, p); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return rv, nil
|
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) {
|
ConversionFrom: func(want cty.Type) func(any, cty.Path) (cty.Value, error) {
|
||||||
return func(in 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)
|
return convert.Convert(v, want)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ExtensionData: func(key any) any {
|
ExtensionData: func(key any) any {
|
||||||
switch key {
|
switch key {
|
||||||
case nativeTypeExtension:
|
case unwrapCapsuleValueExtension:
|
||||||
zero := reflect.Zero(rt).Interface()
|
zero := reflect.Zero(elem).Interface()
|
||||||
return zero.(CapsuleValue).ToCtyValue().Type()
|
if conv, ok := zero.(ToCtyValueConverter); ok {
|
||||||
default:
|
return conv.ToCtyValue().Type()
|
||||||
return nil
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Attempt to store the new type. Use whichever was loaded first in the case of a race condition.
|
zero = reflect.Zero(rt).Interface()
|
||||||
val, _ := capsuleValueTypes.LoadOrStore(rt, ety)
|
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)
|
return val.(cty.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToNativeValue will convert a value to only native cty types which will
|
// UnwrapCtyValue will unwrap capsule type values into their native cty value
|
||||||
// remove capsule types if possible.
|
// equivalents if possible.
|
||||||
func ToNativeValue(in cty.Value) cty.Value {
|
func UnwrapCtyValue(in cty.Value) cty.Value {
|
||||||
want := toNativeType(in.Type())
|
want := toCtyValueType(in.Type())
|
||||||
if in.Type().Equals(want) {
|
if in.Type().Equals(want) {
|
||||||
return in
|
return in
|
||||||
} else if out, err := convert.Convert(in, want); err == nil {
|
} 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)
|
return cty.NullVal(want)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toNativeType(in cty.Type) cty.Type {
|
func toCtyValueType(in cty.Type) cty.Type {
|
||||||
if et := in.MapElementType(); et != nil {
|
if et := in.MapElementType(); et != nil {
|
||||||
return cty.Map(toNativeType(*et))
|
return cty.Map(toCtyValueType(*et))
|
||||||
}
|
}
|
||||||
|
|
||||||
if et := in.SetElementType(); et != nil {
|
if et := in.SetElementType(); et != nil {
|
||||||
return cty.Set(toNativeType(*et))
|
return cty.Set(toCtyValueType(*et))
|
||||||
}
|
}
|
||||||
|
|
||||||
if et := in.ListElementType(); et != nil {
|
if et := in.ListElementType(); et != nil {
|
||||||
return cty.List(toNativeType(*et))
|
return cty.List(toCtyValueType(*et))
|
||||||
}
|
}
|
||||||
|
|
||||||
if in.IsObjectType() {
|
if in.IsObjectType() {
|
||||||
@ -105,7 +130,7 @@ func toNativeType(in cty.Type) cty.Type {
|
|||||||
inAttrTypes := in.AttributeTypes()
|
inAttrTypes := in.AttributeTypes()
|
||||||
outAttrTypes := make(map[string]cty.Type, len(inAttrTypes))
|
outAttrTypes := make(map[string]cty.Type, len(inAttrTypes))
|
||||||
for name, typ := range inAttrTypes {
|
for name, typ := range inAttrTypes {
|
||||||
outAttrTypes[name] = toNativeType(typ)
|
outAttrTypes[name] = toCtyValueType(typ)
|
||||||
if in.AttributeOptional(name) {
|
if in.AttributeOptional(name) {
|
||||||
optional = append(optional, name)
|
optional = append(optional, name)
|
||||||
}
|
}
|
||||||
@ -117,13 +142,13 @@ func toNativeType(in cty.Type) cty.Type {
|
|||||||
inTypes := in.TupleElementTypes()
|
inTypes := in.TupleElementTypes()
|
||||||
outTypes := make([]cty.Type, len(inTypes))
|
outTypes := make([]cty.Type, len(inTypes))
|
||||||
for i, typ := range inTypes {
|
for i, typ := range inTypes {
|
||||||
outTypes[i] = toNativeType(typ)
|
outTypes[i] = toCtyValueType(typ)
|
||||||
}
|
}
|
||||||
return cty.Tuple(outTypes)
|
return cty.Tuple(outTypes)
|
||||||
}
|
}
|
||||||
|
|
||||||
if in.IsCapsuleType() {
|
if in.IsCapsuleType() {
|
||||||
if out := in.CapsuleExtensionData(nativeTypeExtension); out != nil {
|
if out := in.CapsuleExtensionData(unwrapCapsuleValueExtension); out != nil {
|
||||||
return out.(cty.Type)
|
return out.(cty.Type)
|
||||||
}
|
}
|
||||||
return cty.DynamicPseudoType
|
return cty.DynamicPseudoType
|
||||||
@ -137,5 +162,5 @@ func ToCtyValue(val any, ty cty.Type) (cty.Value, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
return ToNativeValue(out), nil
|
return UnwrapCtyValue(out), nil
|
||||||
}
|
}
|
||||||
|
126
tests/bake.go
126
tests/bake.go
@ -33,6 +33,7 @@ func bakeCmd(sb integration.Sandbox, opts ...cmdOpt) (string, error) {
|
|||||||
|
|
||||||
var bakeTests = []func(t *testing.T, sb integration.Sandbox){
|
var bakeTests = []func(t *testing.T, sb integration.Sandbox){
|
||||||
testBakePrint,
|
testBakePrint,
|
||||||
|
testBakePrintSensitive,
|
||||||
testBakeLocal,
|
testBakeLocal,
|
||||||
testBakeLocalMulti,
|
testBakeLocalMulti,
|
||||||
testBakeRemote,
|
testBakeRemote,
|
||||||
@ -86,7 +87,8 @@ target "build" {
|
|||||||
HELLO = "foo"
|
HELLO = "foo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`)},
|
`),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Compose",
|
"Compose",
|
||||||
"compose.yml",
|
"compose.yml",
|
||||||
@ -97,7 +99,8 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
HELLO: foo
|
HELLO: foo
|
||||||
`)},
|
`),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
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) {
|
func testBakeLocal(t *testing.T, sb integration.Sandbox) {
|
||||||
dockerfile := []byte(`
|
dockerfile := []byte(`
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
@ -21,7 +21,7 @@ func CanonicalizeAttest(attestType string, in string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ParseAttests(in []string) ([]*controllerapi.Attest, error) {
|
func ParseAttests(in []string) ([]*controllerapi.Attest, error) {
|
||||||
out := []*controllerapi.Attest{}
|
var out []*controllerapi.Attest
|
||||||
found := map[string]struct{}{}
|
found := map[string]struct{}{}
|
||||||
for _, in := range in {
|
for _, in := range in {
|
||||||
in := in
|
in := in
|
||||||
|
@ -15,6 +15,41 @@ import (
|
|||||||
jsoncty "github.com/zclconf/go-cty/cty/json"
|
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 CacheOptionsEntry struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Attrs map[string]string `json:"attrs,omitempty"`
|
Attrs map[string]string `json:"attrs,omitempty"`
|
||||||
@ -46,10 +81,13 @@ func (e *CacheOptionsEntry) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *CacheOptionsEntry) ToPB() *controllerapi.CacheOptionsEntry {
|
func (e *CacheOptionsEntry) ToPB() *controllerapi.CacheOptionsEntry {
|
||||||
return &controllerapi.CacheOptionsEntry{
|
ci := &controllerapi.CacheOptionsEntry{
|
||||||
Type: e.Type,
|
Type: e.Type,
|
||||||
Attrs: maps.Clone(e.Attrs),
|
Attrs: maps.Clone(e.Attrs),
|
||||||
}
|
}
|
||||||
|
addGithubToken(ci)
|
||||||
|
addAwsCredentials(ci)
|
||||||
|
return ci
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *CacheOptionsEntry) MarshalJSON() ([]byte, error) {
|
func (e *CacheOptionsEntry) MarshalJSON() ([]byte, error) {
|
||||||
@ -74,14 +112,6 @@ func (e *CacheOptionsEntry) UnmarshalJSON(data []byte) error {
|
|||||||
return e.validate(data)
|
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 {
|
func (e *CacheOptionsEntry) UnmarshalText(text []byte) error {
|
||||||
in := string(text)
|
in := string(text)
|
||||||
fields, err := csvvalue.Fields(in, nil)
|
fields, err := csvvalue.Fields(in, nil)
|
||||||
@ -116,8 +146,6 @@ func (e *CacheOptionsEntry) UnmarshalText(text []byte) error {
|
|||||||
if e.Type == "" {
|
if e.Type == "" {
|
||||||
return errors.Errorf("type required form> %q", in)
|
return errors.Errorf("type required form> %q", in)
|
||||||
}
|
}
|
||||||
addGithubToken(e)
|
|
||||||
addAwsCredentials(e)
|
|
||||||
return e.validate(text)
|
return e.validate(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,23 +168,22 @@ func (e *CacheOptionsEntry) validate(gv interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ParseCacheEntry(in []string) ([]*controllerapi.CacheOptionsEntry, 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 {
|
for _, in := range in {
|
||||||
var out CacheOptionsEntry
|
var out CacheOptionsEntry
|
||||||
if err := out.UnmarshalText([]byte(in)); err != nil {
|
if err := out.UnmarshalText([]byte(in)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
opts = append(opts, &out)
|
||||||
if !out.IsActive() {
|
|
||||||
// Skip inactive cache entries.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
outs = append(outs, out.ToPB())
|
|
||||||
}
|
}
|
||||||
return outs, nil
|
return opts.ToPB(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addGithubToken(ci *CacheOptionsEntry) {
|
func addGithubToken(ci *controllerapi.CacheOptionsEntry) {
|
||||||
if ci.Type != "gha" {
|
if ci.Type != "gha" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -172,7 +199,7 @@ func addGithubToken(ci *CacheOptionsEntry) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAwsCredentials(ci *CacheOptionsEntry) {
|
func addAwsCredentials(ci *controllerapi.CacheOptionsEntry) {
|
||||||
if ci.Type != "s3" {
|
if ci.Type != "s3" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -201,3 +228,11 @@ func addAwsCredentials(ci *CacheOptionsEntry) {
|
|||||||
ci.Attrs["session_token"] = credentials.SessionToken
|
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"] != ""
|
||||||
|
}
|
||||||
|
86
util/buildflags/cache_cty.go
Normal file
86
util/buildflags/cache_cty.go
Normal 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)
|
||||||
|
}
|
39
util/buildflags/cache_test.go
Normal file
39
util/buildflags/cache_test.go
Normal 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)
|
||||||
|
}
|
@ -11,8 +11,13 @@ func ParseContextNames(values []string) (map[string]string, error) {
|
|||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]string, len(values))
|
result := make(map[string]string, len(values))
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
|
if value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
kv := strings.SplitN(value, "=", 2)
|
kv := strings.SplitN(value, "=", 2)
|
||||||
if len(kv) != 2 {
|
if len(kv) != 2 {
|
||||||
return nil, errors.Errorf("invalid context value: %s, expected key=value", value)
|
return nil, errors.Errorf("invalid context value: %s, expected key=value", value)
|
||||||
|
@ -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()))
|
|
||||||
}
|
|
@ -5,6 +5,10 @@ import "github.com/moby/buildkit/util/entitlements"
|
|||||||
func ParseEntitlements(in []string) ([]entitlements.Entitlement, error) {
|
func ParseEntitlements(in []string) ([]entitlements.Entitlement, error) {
|
||||||
out := make([]entitlements.Entitlement, 0, len(in))
|
out := make([]entitlements.Entitlement, 0, len(in))
|
||||||
for _, v := range in {
|
for _, v := range in {
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
e, err := entitlements.Parse(v)
|
e, err := entitlements.Parse(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -16,6 +16,39 @@ import (
|
|||||||
"github.com/tonistiigi/go-csvvalue"
|
"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 ExportEntry struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Attrs map[string]string `json:"attrs,omitempty"`
|
Attrs map[string]string `json:"attrs,omitempty"`
|
||||||
@ -131,18 +164,23 @@ func (e *ExportEntry) validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ParseExports(inp []string) ([]*controllerapi.ExportEntry, error) {
|
func ParseExports(inp []string) ([]*controllerapi.ExportEntry, error) {
|
||||||
var outs []*controllerapi.ExportEntry
|
|
||||||
if len(inp) == 0 {
|
if len(inp) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export := make(Exports, 0, len(inp))
|
||||||
for _, s := range inp {
|
for _, s := range inp {
|
||||||
|
if s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var out ExportEntry
|
var out ExportEntry
|
||||||
if err := out.UnmarshalText([]byte(s)); err != nil {
|
if err := out.UnmarshalText([]byte(s)); err != nil {
|
||||||
return nil, err
|
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) {
|
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)
|
annotations := make(map[exptypes.AnnotationKey]string)
|
||||||
for _, inp := range inp {
|
for _, inp := range inp {
|
||||||
|
if inp == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
k, v, ok := strings.Cut(inp, "=")
|
k, v, ok := strings.Cut(inp, "=")
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.Errorf("invalid annotation %q, expected key=value", inp)
|
return nil, errors.Errorf("invalid annotation %q, expected key=value", inp)
|
||||||
|
90
util/buildflags/export_cty.go
Normal file
90
util/buildflags/export_cty.go
Normal 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)
|
||||||
|
}
|
@ -8,6 +8,39 @@ import (
|
|||||||
"github.com/tonistiigi/go-csvvalue"
|
"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 {
|
type Secret struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
FilePath string `json:"src,omitempty"`
|
FilePath string `json:"src,omitempty"`
|
||||||
@ -85,6 +118,10 @@ func (s *Secret) UnmarshalText(text []byte) error {
|
|||||||
func ParseSecretSpecs(sl []string) ([]*controllerapi.Secret, error) {
|
func ParseSecretSpecs(sl []string) ([]*controllerapi.Secret, error) {
|
||||||
fs := make([]*controllerapi.Secret, 0, len(sl))
|
fs := make([]*controllerapi.Secret, 0, len(sl))
|
||||||
for _, v := range sl {
|
for _, v := range sl {
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
s, err := parseSecret(v)
|
s, err := parseSecret(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
96
util/buildflags/secrets_cty.go
Normal file
96
util/buildflags/secrets_cty.go
Normal 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),
|
||||||
|
})
|
||||||
|
}
|
@ -9,6 +9,39 @@ import (
|
|||||||
"github.com/moby/buildkit/util/gitutil"
|
"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 {
|
type SSH struct {
|
||||||
ID string `json:"id,omitempty" cty:"id"`
|
ID string `json:"id,omitempty" cty:"id"`
|
||||||
Paths []string `json:"paths,omitempty" cty:"paths"`
|
Paths []string `json:"paths,omitempty" cty:"paths"`
|
||||||
@ -62,6 +95,10 @@ func ParseSSHSpecs(sl []string) ([]*controllerapi.SSH, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range sl {
|
for _, s := range sl {
|
||||||
|
if s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var out SSH
|
var out SSH
|
||||||
if err := out.UnmarshalText([]byte(s)); err != nil {
|
if err := out.UnmarshalText([]byte(s)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
105
util/buildflags/ssh_cty.go
Normal file
105
util/buildflags/ssh_cty.go
Normal 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
54
util/buildflags/utils.go
Normal 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() == ""
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user