mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-07-31 23:58:03 +08:00
config: fix file/folder ownership
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
@@ -1,39 +1,143 @@
|
||||
package confutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/pelletier/go-toml"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
fs "github.com/tonistiigi/fsutil/copy"
|
||||
)
|
||||
|
||||
// ConfigDir will look for correct configuration store path;
|
||||
// if `$BUILDX_CONFIG` is set - use it, otherwise use parent directory
|
||||
// of Docker config file (i.e. `${DOCKER_CONFIG}/buildx`)
|
||||
func ConfigDir(dockerCli command.Cli) string {
|
||||
if buildxConfig := os.Getenv("BUILDX_CONFIG"); buildxConfig != "" {
|
||||
logrus.Debugf("using config store %q based in \"$BUILDX_CONFIG\" environment variable", buildxConfig)
|
||||
return buildxConfig
|
||||
}
|
||||
const defaultBuildKitConfigFile = "buildkitd.default.toml"
|
||||
|
||||
buildxConfig := filepath.Join(filepath.Dir(dockerCli.ConfigFile().Filename), "buildx")
|
||||
logrus.Debugf("using default config store %q", buildxConfig)
|
||||
return buildxConfig
|
||||
type Config struct {
|
||||
dir string
|
||||
chowner *chowner
|
||||
}
|
||||
|
||||
// DefaultConfigFile returns the default BuildKit configuration file path
|
||||
func DefaultConfigFile(dockerCli command.Cli) (string, bool) {
|
||||
f := path.Join(ConfigDir(dockerCli), "buildkitd.default.toml")
|
||||
type chowner struct {
|
||||
uid int
|
||||
gid int
|
||||
}
|
||||
|
||||
type ConfigOption func(*configOptions)
|
||||
|
||||
type configOptions struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
func WithDir(dir string) ConfigOption {
|
||||
return func(o *configOptions) {
|
||||
o.dir = dir
|
||||
}
|
||||
}
|
||||
|
||||
func NewConfig(dockerCli command.Cli, opts ...ConfigOption) *Config {
|
||||
co := configOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(&co)
|
||||
}
|
||||
|
||||
configDir := co.dir
|
||||
if configDir == "" {
|
||||
configDir = os.Getenv("BUILDX_CONFIG")
|
||||
if configDir == "" {
|
||||
configDir = filepath.Join(filepath.Dir(dockerCli.ConfigFile().Filename), "buildx")
|
||||
}
|
||||
}
|
||||
|
||||
return &Config{
|
||||
dir: configDir,
|
||||
chowner: sudoer(configDir),
|
||||
}
|
||||
}
|
||||
|
||||
// Dir will look for correct configuration store path;
|
||||
// if `$BUILDX_CONFIG` is set - use it, otherwise use parent directory
|
||||
// of Docker config file (i.e. `${DOCKER_CONFIG}/buildx`)
|
||||
func (c *Config) Dir() string {
|
||||
return c.dir
|
||||
}
|
||||
|
||||
// BuildKitConfigFile returns the default BuildKit configuration file path
|
||||
func (c *Config) BuildKitConfigFile() (string, bool) {
|
||||
f := filepath.Join(c.dir, defaultBuildKitConfigFile)
|
||||
if _, err := os.Stat(f); err == nil {
|
||||
return f, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// MkdirAll creates a directory and all necessary parents within the config dir.
|
||||
func (c *Config) MkdirAll(dir string, perm os.FileMode) error {
|
||||
var chown fs.Chowner
|
||||
if c.chowner != nil {
|
||||
chown = func(user *fs.User) (*fs.User, error) {
|
||||
return &fs.User{UID: c.chowner.uid, GID: c.chowner.gid}, nil
|
||||
}
|
||||
}
|
||||
d := filepath.Join(c.dir, dir)
|
||||
st, err := os.Stat(d)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fs.MkdirAll(d, perm, chown, nil)
|
||||
}
|
||||
return err
|
||||
}
|
||||
// if directory already exists, fix the owner if necessary
|
||||
if c.chowner == nil {
|
||||
return nil
|
||||
}
|
||||
currentOwner := fileOwner(st)
|
||||
if currentOwner != nil && (currentOwner.uid != c.chowner.uid || currentOwner.gid != c.chowner.gid) {
|
||||
return os.Chown(d, c.chowner.uid, c.chowner.gid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AtomicWriteFile writes data to a file within the config dir atomically
|
||||
func (c *Config) AtomicWriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||
f := filepath.Join(c.dir, filename)
|
||||
if err := ioutils.AtomicWriteFile(f, data, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.chowner == nil {
|
||||
return nil
|
||||
}
|
||||
return os.Chown(f, c.chowner.uid, c.chowner.gid)
|
||||
}
|
||||
|
||||
var nodeIdentifierMu sync.Mutex
|
||||
|
||||
func (c *Config) TryNodeIdentifier() (out string) {
|
||||
nodeIdentifierMu.Lock()
|
||||
defer nodeIdentifierMu.Unlock()
|
||||
sessionFilename := ".buildNodeID"
|
||||
sessionFilepath := filepath.Join(c.Dir(), sessionFilename)
|
||||
if _, err := os.Lstat(sessionFilepath); err != nil {
|
||||
if os.IsNotExist(err) { // create a new file with stored randomness
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return out
|
||||
}
|
||||
if err := c.AtomicWriteFile(sessionFilename, []byte(hex.EncodeToString(b)), 0600); err != nil {
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
dt, err := os.ReadFile(sessionFilepath)
|
||||
if err == nil {
|
||||
return string(dt)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadConfigTree loads BuildKit config toml tree
|
||||
func LoadConfigTree(fp string) (*toml.Tree, error) {
|
||||
f, err := os.Open(fp)
|
||||
|
60
util/confutil/config_unix.go
Normal file
60
util/confutil/config_unix.go
Normal file
@@ -0,0 +1,60 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package confutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// sudoer returns the user that invoked the current process with sudo only if
|
||||
// sudo HOME env matches the home directory of the user that ran sudo and is
|
||||
// part of configDir.
|
||||
func sudoer(configDir string) *chowner {
|
||||
if _, ok := os.LookupEnv("SUDO_COMMAND"); !ok {
|
||||
return nil
|
||||
}
|
||||
suidenv := os.Getenv("SUDO_UID") // https://www.sudo.ws/docs/man/sudo.man/#SUDO_UID
|
||||
sgidenv := os.Getenv("SUDO_GID") // https://www.sudo.ws/docs/man/sudo.man/#SUDO_GID
|
||||
if suidenv == "" || sgidenv == "" {
|
||||
return nil
|
||||
}
|
||||
u, err := user.LookupId(suidenv)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
suid, err := strconv.Atoi(suidenv)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
sgid, err := strconv.Atoi(sgidenv)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
if home == "" || u.HomeDir != home {
|
||||
return nil
|
||||
}
|
||||
if ok, _ := isSubPath(home, configDir); !ok {
|
||||
return nil
|
||||
}
|
||||
return &chowner{uid: suid, gid: sgid}
|
||||
}
|
||||
|
||||
func fileOwner(fi os.FileInfo) *chowner {
|
||||
st := fi.Sys().(*syscall.Stat_t)
|
||||
return &chowner{uid: int(st.Uid), gid: int(st.Gid)}
|
||||
}
|
||||
|
||||
func isSubPath(basePath, subPath string) (bool, error) {
|
||||
rel, err := filepath.Rel(basePath, subPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return !strings.HasPrefix(rel, "..") && rel != ".", nil
|
||||
}
|
58
util/confutil/config_unix_test.go
Normal file
58
util/confutil/config_unix_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package confutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsSubPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
basePath string
|
||||
subPath string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "SubPath is a direct subdirectory",
|
||||
basePath: "/home/user",
|
||||
subPath: "/home/user/docs",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SubPath is the same as basePath",
|
||||
basePath: "/home/user",
|
||||
subPath: "/home/user",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "SubPath is not a subdirectory",
|
||||
basePath: "/home/user",
|
||||
subPath: "/home/otheruser",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "SubPath is a nested subdirectory",
|
||||
basePath: "/home/user",
|
||||
subPath: "/home/user/docs/reports",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SubPath is a sibling directory",
|
||||
basePath: "/home/user",
|
||||
subPath: "/home/user2",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ok, err := isSubPath(tt.basePath, tt.subPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, ok)
|
||||
})
|
||||
}
|
||||
}
|
11
util/confutil/config_windows.go
Normal file
11
util/confutil/config_windows.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package confutil
|
||||
|
||||
import "os"
|
||||
|
||||
func sudoer(_ string) *chowner {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileOwner(_ os.FileInfo) *chowner {
|
||||
return nil
|
||||
}
|
@@ -1,34 +0,0 @@
|
||||
package confutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var nodeIdentifierMu sync.Mutex
|
||||
|
||||
func TryNodeIdentifier(configDir string) (out string) {
|
||||
nodeIdentifierMu.Lock()
|
||||
defer nodeIdentifierMu.Unlock()
|
||||
sessionFile := filepath.Join(configDir, ".buildNodeID")
|
||||
if _, err := os.Lstat(sessionFile); err != nil {
|
||||
if os.IsNotExist(err) { // create a new file with stored randomness
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return out
|
||||
}
|
||||
if err := os.WriteFile(sessionFile, []byte(hex.EncodeToString(b)), 0600); err != nil {
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dt, err := os.ReadFile(sessionFile)
|
||||
if err == nil {
|
||||
return string(dt)
|
||||
}
|
||||
return
|
||||
}
|
Reference in New Issue
Block a user