mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 09:17:49 +08:00
bake: add filesystem entitlements support
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
parent
4b5d78db9b
commit
1af4f05ba4
@ -1315,6 +1315,8 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
bo.SecretSpecs = secrets
|
||||||
|
|
||||||
secretAttachment, err := controllerapi.CreateSecrets(secrets)
|
secretAttachment, err := controllerapi.CreateSecrets(secrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -1328,6 +1330,8 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
|||||||
if len(sshSpecs) == 0 && (buildflags.IsGitSSH(bi.ContextPath) || (inp != nil && buildflags.IsGitSSH(inp.URL))) {
|
if len(sshSpecs) == 0 && (buildflags.IsGitSSH(bi.ContextPath) || (inp != nil && buildflags.IsGitSSH(inp.URL))) {
|
||||||
sshSpecs = append(sshSpecs, &controllerapi.SSH{ID: "default"})
|
sshSpecs = append(sshSpecs, &controllerapi.SSH{ID: "default"})
|
||||||
}
|
}
|
||||||
|
bo.SSHSpecs = sshSpecs
|
||||||
|
|
||||||
sshAttachment, err := controllerapi.CreateSSH(sshSpecs)
|
sshAttachment, err := controllerapi.CreateSSH(sshSpecs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -2,12 +2,17 @@ package bake
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/containerd/console"
|
"github.com/containerd/console"
|
||||||
"github.com/docker/buildx/build"
|
"github.com/docker/buildx/build"
|
||||||
@ -67,10 +72,8 @@ func ParseEntitlements(in []string) (EntitlementConf, error) {
|
|||||||
conf.ImagePush = append(conf.ImagePush, v)
|
conf.ImagePush = append(conf.ImagePush, v)
|
||||||
conf.ImageLoad = append(conf.ImageLoad, v)
|
conf.ImageLoad = append(conf.ImageLoad, v)
|
||||||
default:
|
default:
|
||||||
return conf, errors.Errorf("uknown entitlement key %q", k)
|
return conf, errors.Errorf("unknown entitlement key %q", k)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: dedupe slices and parent paths
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return conf, nil
|
return conf, nil
|
||||||
@ -101,6 +104,66 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rwPaths := map[string]struct{}{}
|
||||||
|
roPaths := map[string]struct{}{}
|
||||||
|
|
||||||
|
for _, out := range bo.Exports {
|
||||||
|
if out.Type == "local" {
|
||||||
|
if dest, ok := out.Attrs["dest"]; ok {
|
||||||
|
rwPaths[dest] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out.Type == "tar" {
|
||||||
|
if dest, ok := out.Attrs["dest"]; ok && dest != "-" {
|
||||||
|
rwPaths[dest] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ce := range bo.CacheTo {
|
||||||
|
if ce.Type == "local" {
|
||||||
|
if dest, ok := ce.Attrs["dest"]; ok {
|
||||||
|
rwPaths[dest] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ci := range bo.CacheFrom {
|
||||||
|
if ci.Type == "local" {
|
||||||
|
if src, ok := ci.Attrs["src"]; ok {
|
||||||
|
roPaths[src] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, secret := range bo.SecretSpecs {
|
||||||
|
if secret.FilePath != "" {
|
||||||
|
roPaths[secret.FilePath] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ssh := range bo.SSHSpecs {
|
||||||
|
for _, p := range ssh.Paths {
|
||||||
|
roPaths[p] = struct{}{}
|
||||||
|
}
|
||||||
|
if len(ssh.Paths) == 0 {
|
||||||
|
expected.SSH = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
expected.FSRead, err = findMissingPaths(c.FSRead, roPaths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
expected.FSWrite, err = findMissingPaths(c.FSWrite, rwPaths)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,32 +176,67 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
|||||||
var msgs []string
|
var msgs []string
|
||||||
var flags []string
|
var flags []string
|
||||||
|
|
||||||
|
// these warnings are currently disabled to give users time to update
|
||||||
|
var msgsFS []string
|
||||||
|
var flagsFS []string
|
||||||
|
|
||||||
if c.NetworkHost {
|
if c.NetworkHost {
|
||||||
msgs = append(msgs, " - Running build containers that can access host network")
|
msgs = append(msgs, " - Running build containers that can access host network")
|
||||||
flags = append(flags, "network.host")
|
flags = append(flags, string(EntitlementKeyNetworkHost))
|
||||||
}
|
}
|
||||||
if c.SecurityInsecure {
|
if c.SecurityInsecure {
|
||||||
msgs = append(msgs, " - Running privileged containers that can make system changes")
|
msgs = append(msgs, " - Running privileged containers that can make system changes")
|
||||||
flags = append(flags, "security.insecure")
|
flags = append(flags, string(EntitlementKeySecurityInsecure))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msgs) == 0 {
|
if c.SSH {
|
||||||
|
msgsFS = append(msgsFS, " - Forwarding default SSH agent socket")
|
||||||
|
flagsFS = append(flagsFS, string(EntitlementKeySSH))
|
||||||
|
}
|
||||||
|
|
||||||
|
roPaths, rwPaths, commonPaths := groupSamePaths(c.FSRead, c.FSWrite)
|
||||||
|
|
||||||
|
if len(commonPaths) > 0 {
|
||||||
|
for _, p := range commonPaths {
|
||||||
|
msgsFS = append(msgsFS, fmt.Sprintf(" - Read and write access to path %s", p))
|
||||||
|
flagsFS = append(flagsFS, string(EntitlementKeyFS)+"="+p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(roPaths) > 0 {
|
||||||
|
for _, p := range roPaths {
|
||||||
|
msgsFS = append(msgsFS, fmt.Sprintf(" - Read access to path %s", p))
|
||||||
|
flagsFS = append(flagsFS, string(EntitlementKeyFSRead)+"="+p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rwPaths) > 0 {
|
||||||
|
for _, p := range rwPaths {
|
||||||
|
msgsFS = append(msgsFS, fmt.Sprintf(" - Write access to path %s", p))
|
||||||
|
flagsFS = append(flagsFS, string(EntitlementKeyFSWrite)+"="+p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msgs) == 0 && len(msgsFS) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(out, "Your build is requesting privileges for following possibly insecure capabilities:\n\n")
|
fmt.Fprintf(out, "Your build is requesting privileges for following possibly insecure capabilities:\n\n")
|
||||||
for _, m := range msgs {
|
for _, m := range slices.Concat(msgs, msgsFS) {
|
||||||
fmt.Fprintf(out, "%s\n", m)
|
fmt.Fprintf(out, "%s\n", m)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, f := range flags {
|
for i, f := range flags {
|
||||||
flags[i] = "--allow=" + f
|
flags[i] = "--allow=" + f
|
||||||
}
|
}
|
||||||
|
for i, f := range flagsFS {
|
||||||
|
flagsFS[i] = "--allow=" + f
|
||||||
|
}
|
||||||
|
|
||||||
if term {
|
if term {
|
||||||
fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(flags, " "))
|
fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(slices.Concat(flags, flagsFS), " "))
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(flags, " "))
|
fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(slices.Concat(flags, flagsFS), " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
args := append([]string(nil), os.Args...)
|
args := append([]string(nil), os.Args...)
|
||||||
@ -149,7 +247,24 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
|||||||
|
|
||||||
if idx != -1 {
|
if idx != -1 {
|
||||||
fmt.Fprintf(out, "\nYour full command with requested privileges:\n\n")
|
fmt.Fprintf(out, "\nYour full command with requested privileges:\n\n")
|
||||||
fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(flags, " "), strings.Join(args[idx+1:], " "))
|
fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(slices.Concat(flags, flagsFS), " "), strings.Join(args[idx+1:], " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
fsEntitlementsEnabled := false
|
||||||
|
v, fsEntitlementsSet := os.LookupEnv("BUILDX_BAKE_ENTITLEMENTS_FS")
|
||||||
|
if fsEntitlementsSet {
|
||||||
|
vv, err := strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to parse BUILDX_BAKE_ENTITLEMENTS_FS value %q", v)
|
||||||
|
}
|
||||||
|
fsEntitlementsEnabled = vv
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fsEntitlementsEnabled && len(msgs) == 0 {
|
||||||
|
if !fsEntitlementsSet {
|
||||||
|
fmt.Fprintf(out, "This warning will become an error in a future release. To enable filesystem entitlements checks at the moment, set BUILDX_BAKE_ENTITLEMENTS_FS=1 .\n\n")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if term {
|
if term {
|
||||||
@ -173,3 +288,277 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
|||||||
|
|
||||||
return errors.Errorf("additional privileges requested")
|
return errors.Errorf("additional privileges requested")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isParentOrEqualPath(p, parent string) bool {
|
||||||
|
if p == parent || parent == "/" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(p, parent+string(filepath.Separator)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMissingPaths(set []string, paths map[string]struct{}) ([]string, error) {
|
||||||
|
paths, err := evaluateToExistingPaths(paths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
paths, err = dedupPaths(paths)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
set, err = evaluatePaths(set)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]string, 0, len(paths))
|
||||||
|
loop0:
|
||||||
|
for p := range paths {
|
||||||
|
for _, c := range set {
|
||||||
|
if isParentOrEqualPath(p, c) {
|
||||||
|
continue loop0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(out)
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dedupPaths(in map[string]struct{}) (map[string]struct{}, error) {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
arr := make([]string, 0, len(in))
|
||||||
|
for p := range in {
|
||||||
|
arr = append(arr, filepath.Clean(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(arr, func(a, b string) int {
|
||||||
|
return cmp.Compare(len(a), len(b))
|
||||||
|
})
|
||||||
|
|
||||||
|
m := make(map[string]struct{}, len(arr))
|
||||||
|
loop0:
|
||||||
|
for _, p := range arr {
|
||||||
|
for parent := range m {
|
||||||
|
if strings.HasPrefix(p, parent+string(filepath.Separator)) {
|
||||||
|
continue loop0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m[p] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for p := range m {
|
||||||
|
rel, err := filepath.Rel(wd, p)
|
||||||
|
if err == nil {
|
||||||
|
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||||
|
delete(m, p)
|
||||||
|
m[rel] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupSamePaths(in1, in2 []string) ([]string, []string, []string) {
|
||||||
|
if in1 == nil || in2 == nil {
|
||||||
|
return in1, in2, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(in1)
|
||||||
|
slices.Sort(in2)
|
||||||
|
|
||||||
|
common := []string{}
|
||||||
|
i, j := 0, 0
|
||||||
|
|
||||||
|
for i < len(in1) && j < len(in2) {
|
||||||
|
switch {
|
||||||
|
case in1[i] == in2[j]:
|
||||||
|
common = append(common, in1[i])
|
||||||
|
i++
|
||||||
|
j++
|
||||||
|
case in1[i] < in2[j]:
|
||||||
|
i++
|
||||||
|
default:
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
in1 = removeCommonPaths(in1, common)
|
||||||
|
in2 = removeCommonPaths(in2, common)
|
||||||
|
|
||||||
|
return in1, in2, common
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCommonPaths(in, common []string) []string {
|
||||||
|
filtered := make([]string, 0, len(in))
|
||||||
|
commonIndex := 0
|
||||||
|
for _, path := range in {
|
||||||
|
if commonIndex < len(common) && path == common[commonIndex] {
|
||||||
|
commonIndex++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, path)
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluatePaths(in []string) ([]string, error) {
|
||||||
|
out := make([]string, 0, len(in))
|
||||||
|
for _, p := range in {
|
||||||
|
v, err := filepath.Abs(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v, err = filepath.EvalSymlinks(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||||
|
}
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluateToExistingPaths(in map[string]struct{}) (map[string]struct{}, error) {
|
||||||
|
m := make(map[string]struct{}, len(in))
|
||||||
|
for p := range in {
|
||||||
|
v, err := evaluateToExistingPath(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||||
|
}
|
||||||
|
m[v] = struct{}{}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluateToExistingPath(in string) (string, error) {
|
||||||
|
in, err := filepath.Abs(in)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
volLen := volumeNameLen(in)
|
||||||
|
pathSeparator := string(os.PathSeparator)
|
||||||
|
|
||||||
|
if volLen < len(in) && os.IsPathSeparator(in[volLen]) {
|
||||||
|
volLen++
|
||||||
|
}
|
||||||
|
vol := in[:volLen]
|
||||||
|
dest := vol
|
||||||
|
linksWalked := 0
|
||||||
|
var end int
|
||||||
|
for start := volLen; start < len(in); start = end {
|
||||||
|
for start < len(in) && os.IsPathSeparator(in[start]) {
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
end = start
|
||||||
|
for end < len(in) && !os.IsPathSeparator(in[end]) {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
|
||||||
|
if end == start {
|
||||||
|
break
|
||||||
|
} else if in[start:end] == "." {
|
||||||
|
continue
|
||||||
|
} else if in[start:end] == ".." {
|
||||||
|
var r int
|
||||||
|
for r = len(dest) - 1; r >= volLen; r-- {
|
||||||
|
if os.IsPathSeparator(dest[r]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r < volLen || dest[r+1:] == ".." {
|
||||||
|
if len(dest) > volLen {
|
||||||
|
dest += pathSeparator
|
||||||
|
}
|
||||||
|
dest += ".."
|
||||||
|
} else {
|
||||||
|
dest = dest[:r]
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) {
|
||||||
|
dest += pathSeparator
|
||||||
|
}
|
||||||
|
dest += in[start:end]
|
||||||
|
|
||||||
|
fi, err := os.Lstat(dest)
|
||||||
|
if err != nil {
|
||||||
|
// If the component doesn't exist, return the last valid path
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
for r := len(dest) - 1; r >= volLen; r-- {
|
||||||
|
if os.IsPathSeparator(dest[r]) {
|
||||||
|
return dest[:r], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vol, nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.Mode()&fs.ModeSymlink == 0 {
|
||||||
|
if !fi.Mode().IsDir() && end < len(in) {
|
||||||
|
return "", syscall.ENOTDIR
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
linksWalked++
|
||||||
|
if linksWalked > 255 {
|
||||||
|
return "", errors.New("too many symlinks")
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := os.Readlink(dest)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
in = link + in[end:]
|
||||||
|
|
||||||
|
v := volumeNameLen(link)
|
||||||
|
if v > 0 {
|
||||||
|
if v < len(link) && os.IsPathSeparator(link[v]) {
|
||||||
|
v++
|
||||||
|
}
|
||||||
|
vol = link[:v]
|
||||||
|
dest = vol
|
||||||
|
end = len(vol)
|
||||||
|
} else if len(link) > 0 && os.IsPathSeparator(link[0]) {
|
||||||
|
dest = link[:1]
|
||||||
|
end = 1
|
||||||
|
vol = link[:1]
|
||||||
|
volLen = 1
|
||||||
|
} else {
|
||||||
|
var r int
|
||||||
|
for r = len(dest) - 1; r >= volLen; r-- {
|
||||||
|
if os.IsPathSeparator(dest[r]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r < volLen {
|
||||||
|
dest = vol
|
||||||
|
} else {
|
||||||
|
dest = dest[:r]
|
||||||
|
}
|
||||||
|
end = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filepath.Clean(dest), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func volumeNameLen(s string) int {
|
||||||
|
return len(filepath.VolumeName(s))
|
||||||
|
}
|
||||||
|
393
bake/entitlements_test.go
Normal file
393
bake/entitlements_test.go
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
package bake
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/buildx/build"
|
||||||
|
"github.com/docker/buildx/controller/pb"
|
||||||
|
"github.com/moby/buildkit/client"
|
||||||
|
"github.com/moby/buildkit/util/entitlements"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEvaluateToExistingPath(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
// Setup temporary directory structure for testing
|
||||||
|
existingFile := filepath.Join(tempDir, "existing_file")
|
||||||
|
err := os.WriteFile(existingFile, []byte("test"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
existingDir := filepath.Join(tempDir, "existing_dir")
|
||||||
|
err = os.Mkdir(existingDir, 0755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
symlinkToFile := filepath.Join(tempDir, "symlink_to_file")
|
||||||
|
err = os.Symlink(existingFile, symlinkToFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
symlinkToDir := filepath.Join(tempDir, "symlink_to_dir")
|
||||||
|
err = os.Symlink(existingDir, symlinkToDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
nonexistentPath := filepath.Join(tempDir, "nonexistent", "path", "file.txt")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Existing file",
|
||||||
|
input: existingFile,
|
||||||
|
expected: existingFile,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Existing directory",
|
||||||
|
input: existingDir,
|
||||||
|
expected: existingDir,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Symlink to file",
|
||||||
|
input: symlinkToFile,
|
||||||
|
expected: existingFile,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Symlink to directory",
|
||||||
|
input: symlinkToDir,
|
||||||
|
expected: existingDir,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-existent path",
|
||||||
|
input: nonexistentPath,
|
||||||
|
expected: tempDir,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-existent intermediate path",
|
||||||
|
input: filepath.Join(tempDir, "nonexistent", "file.txt"),
|
||||||
|
expected: tempDir,
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Root path",
|
||||||
|
input: "/",
|
||||||
|
expected: "/",
|
||||||
|
expectErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := evaluateToExistingPath(tt.input)
|
||||||
|
|
||||||
|
if tt.expectErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDedupePaths(t *testing.T) {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
tcases := []struct {
|
||||||
|
in map[string]struct{}
|
||||||
|
out map[string]struct{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
in: map[string]struct{}{
|
||||||
|
"/a/b/c": {},
|
||||||
|
"/a/b/d": {},
|
||||||
|
"/a/b/e": {},
|
||||||
|
},
|
||||||
|
out: map[string]struct{}{
|
||||||
|
"/a/b/c": {},
|
||||||
|
"/a/b/d": {},
|
||||||
|
"/a/b/e": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: map[string]struct{}{
|
||||||
|
"/a/b/c": {},
|
||||||
|
"/a/b/c/d": {},
|
||||||
|
"/a/b/c/d/e": {},
|
||||||
|
"/a/b/../b/c": {},
|
||||||
|
},
|
||||||
|
out: map[string]struct{}{
|
||||||
|
"/a/b/c": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
in: map[string]struct{}{
|
||||||
|
filepath.Join(wd, "a/b/c"): {},
|
||||||
|
filepath.Join(wd, "../aa"): {},
|
||||||
|
filepath.Join(wd, "a/b"): {},
|
||||||
|
filepath.Join(wd, "a/b/d"): {},
|
||||||
|
filepath.Join(wd, "../aa/b"): {},
|
||||||
|
},
|
||||||
|
out: map[string]struct{}{
|
||||||
|
"a/b": {},
|
||||||
|
filepath.Join(wd, "../aa"): {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range tcases {
|
||||||
|
t.Run(fmt.Sprintf("case%d", i), func(t *testing.T) {
|
||||||
|
out, err := dedupPaths(tc.in)
|
||||||
|
if err != nil {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
require.Equal(t, tc.out, out)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateEntitlements(t *testing.T) {
|
||||||
|
dir1 := t.TempDir()
|
||||||
|
dir2 := t.TempDir()
|
||||||
|
|
||||||
|
escapeLink := filepath.Join(dir1, "escape_link")
|
||||||
|
err := os.Symlink("../../aa", escapeLink)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tcases := []struct {
|
||||||
|
name string
|
||||||
|
conf EntitlementConf
|
||||||
|
opt build.Options
|
||||||
|
expected EntitlementConf
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No entitlements",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NetworkHostMissing",
|
||||||
|
opt: build.Options{
|
||||||
|
Allow: []entitlements.Entitlement{
|
||||||
|
entitlements.EntitlementNetworkHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: EntitlementConf{
|
||||||
|
NetworkHost: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NetworkHostSet",
|
||||||
|
conf: EntitlementConf{
|
||||||
|
NetworkHost: true,
|
||||||
|
},
|
||||||
|
opt: build.Options{
|
||||||
|
Allow: []entitlements.Entitlement{
|
||||||
|
entitlements.EntitlementNetworkHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: EntitlementConf{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SecurityAndNetworkHostMissing",
|
||||||
|
opt: build.Options{
|
||||||
|
Allow: []entitlements.Entitlement{
|
||||||
|
entitlements.EntitlementNetworkHost,
|
||||||
|
entitlements.EntitlementSecurityInsecure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: EntitlementConf{
|
||||||
|
NetworkHost: true,
|
||||||
|
SecurityInsecure: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SecurityMissingAndNetworkHostSet",
|
||||||
|
conf: EntitlementConf{
|
||||||
|
NetworkHost: true,
|
||||||
|
},
|
||||||
|
opt: build.Options{
|
||||||
|
Allow: []entitlements.Entitlement{
|
||||||
|
entitlements.EntitlementNetworkHost,
|
||||||
|
entitlements.EntitlementSecurityInsecure,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: EntitlementConf{
|
||||||
|
SecurityInsecure: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSHMissing",
|
||||||
|
opt: build.Options{
|
||||||
|
SSHSpecs: []*pb.SSH{
|
||||||
|
{
|
||||||
|
ID: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: EntitlementConf{
|
||||||
|
SSH: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExportLocal",
|
||||||
|
opt: build.Options{
|
||||||
|
Exports: []client.ExportEntry{
|
||||||
|
{
|
||||||
|
Type: "local",
|
||||||
|
Attrs: map[string]string{
|
||||||
|
"dest": dir1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "local",
|
||||||
|
Attrs: map[string]string{
|
||||||
|
"dest": filepath.Join(dir1, "subdir"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "local",
|
||||||
|
Attrs: map[string]string{
|
||||||
|
"dest": dir2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: EntitlementConf{
|
||||||
|
FSWrite: func() []string {
|
||||||
|
exp := []string{dir1, dir2}
|
||||||
|
slices.Sort(exp)
|
||||||
|
return exp
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SecretFromSubFile",
|
||||||
|
opt: build.Options{
|
||||||
|
SecretSpecs: []*pb.Secret{
|
||||||
|
{
|
||||||
|
FilePath: filepath.Join(dir1, "subfile"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conf: EntitlementConf{
|
||||||
|
FSRead: []string{dir1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SecretFromEscapeLink",
|
||||||
|
opt: build.Options{
|
||||||
|
SecretSpecs: []*pb.Secret{
|
||||||
|
{
|
||||||
|
FilePath: escapeLink,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conf: EntitlementConf{
|
||||||
|
FSRead: []string{dir1},
|
||||||
|
},
|
||||||
|
expected: EntitlementConf{
|
||||||
|
FSRead: []string{filepath.Join(dir1, "../..")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SecretFromEscapeLinkAllowRoot",
|
||||||
|
opt: build.Options{
|
||||||
|
SecretSpecs: []*pb.Secret{
|
||||||
|
{
|
||||||
|
FilePath: escapeLink,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
conf: EntitlementConf{
|
||||||
|
FSRead: []string{"/"},
|
||||||
|
},
|
||||||
|
expected: EntitlementConf{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
expected, err := tc.conf.Validate(map[string]build.Options{"test": tc.opt})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expected, expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupSamePaths(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in1 []string
|
||||||
|
in2 []string
|
||||||
|
expected1 []string
|
||||||
|
expected2 []string
|
||||||
|
expectedC []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "All common paths",
|
||||||
|
in1: []string{"/path/a", "/path/b", "/path/c"},
|
||||||
|
in2: []string{"/path/a", "/path/b", "/path/c"},
|
||||||
|
expected1: []string{},
|
||||||
|
expected2: []string{},
|
||||||
|
expectedC: []string{"/path/a", "/path/b", "/path/c"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No common paths",
|
||||||
|
in1: []string{"/path/a", "/path/b"},
|
||||||
|
in2: []string{"/path/c", "/path/d"},
|
||||||
|
expected1: []string{"/path/a", "/path/b"},
|
||||||
|
expected2: []string{"/path/c", "/path/d"},
|
||||||
|
expectedC: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Some common paths",
|
||||||
|
in1: []string{"/path/a", "/path/b", "/path/c"},
|
||||||
|
in2: []string{"/path/b", "/path/c", "/path/d"},
|
||||||
|
expected1: []string{"/path/a"},
|
||||||
|
expected2: []string{"/path/d"},
|
||||||
|
expectedC: []string{"/path/b", "/path/c"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty inputs",
|
||||||
|
in1: []string{},
|
||||||
|
in2: []string{},
|
||||||
|
expected1: []string{},
|
||||||
|
expected2: []string{},
|
||||||
|
expectedC: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "One empty input",
|
||||||
|
in1: []string{"/path/a", "/path/b"},
|
||||||
|
in2: []string{},
|
||||||
|
expected1: []string{"/path/a", "/path/b"},
|
||||||
|
expected2: []string{},
|
||||||
|
expectedC: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unsorted inputs with common paths",
|
||||||
|
in1: []string{"/path/c", "/path/a", "/path/b"},
|
||||||
|
in2: []string{"/path/b", "/path/c", "/path/a"},
|
||||||
|
expected1: []string{},
|
||||||
|
expected2: []string{},
|
||||||
|
expectedC: []string{"/path/a", "/path/b", "/path/c"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
out1, out2, common := groupSamePaths(tt.in1, tt.in2)
|
||||||
|
require.Equal(t, tt.expected1, out1, "in1 should match expected1")
|
||||||
|
require.Equal(t, tt.expected2, out2, "in2 should match expected2")
|
||||||
|
require.Equal(t, tt.expectedC, common, "common should match expectedC")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/containerd/containerd/images"
|
"github.com/containerd/containerd/images"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/buildx/builder"
|
"github.com/docker/buildx/builder"
|
||||||
|
controllerapi "github.com/docker/buildx/controller/pb"
|
||||||
"github.com/docker/buildx/driver"
|
"github.com/docker/buildx/driver"
|
||||||
"github.com/docker/buildx/util/confutil"
|
"github.com/docker/buildx/util/confutil"
|
||||||
"github.com/docker/buildx/util/desktop"
|
"github.com/docker/buildx/util/desktop"
|
||||||
@ -76,6 +77,8 @@ type Options struct {
|
|||||||
NoCacheFilter []string
|
NoCacheFilter []string
|
||||||
Platforms []specs.Platform
|
Platforms []specs.Platform
|
||||||
Pull bool
|
Pull bool
|
||||||
|
SecretSpecs []*controllerapi.Secret
|
||||||
|
SSHSpecs []*controllerapi.SSH
|
||||||
ShmSize opts.MemBytes
|
ShmSize opts.MemBytes
|
||||||
Tags []string
|
Tags []string
|
||||||
Target string
|
Target string
|
||||||
|
@ -107,6 +107,13 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to get current working directory")
|
||||||
|
}
|
||||||
|
// filesystem access under the current working directory is allowed by default
|
||||||
|
ent.FSRead = append(ent.FSRead, wd)
|
||||||
|
ent.FSWrite = append(ent.FSWrite, wd)
|
||||||
|
|
||||||
ctx2, cancel := context.WithCancelCause(context.TODO())
|
ctx2, cancel := context.WithCancelCause(context.TODO())
|
||||||
defer cancel(errors.WithStack(context.Canceled))
|
defer cancel(errors.WithStack(context.Canceled))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user