diff --git a/build/build.go b/build/build.go index 2ebf0898..1892d41d 100644 --- a/build/build.go +++ b/build/build.go @@ -151,11 +151,11 @@ func toRepoOnly(in string) (string, error) { return strings.Join(out, ","), nil } -func Build(ctx context.Context, nodes []builder.Node, opts map[string]Options, docker *dockerutil.Client, configDir string, w progress.Writer) (resp map[string]*client.SolveResponse, err error) { - return BuildWithResultHandler(ctx, nodes, opts, docker, configDir, w, nil) +func Build(ctx context.Context, nodes []builder.Node, opts map[string]Options, docker *dockerutil.Client, cfg *confutil.Config, w progress.Writer) (resp map[string]*client.SolveResponse, err error) { + return BuildWithResultHandler(ctx, nodes, opts, docker, cfg, w, nil) } -func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[string]Options, docker *dockerutil.Client, configDir string, w progress.Writer, resultHandleFunc func(driverIndex int, rCtx *ResultHandle)) (resp map[string]*client.SolveResponse, err error) { +func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[string]Options, docker *dockerutil.Client, cfg *confutil.Config, w progress.Writer, resultHandleFunc func(driverIndex int, rCtx *ResultHandle)) (resp map[string]*client.SolveResponse, err error) { if len(nodes) == 0 { return nil, errors.Errorf("driver required for build") } @@ -234,12 +234,12 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[ return nil, err } localOpt := opt - so, release, err := toSolveOpt(ctx, np.Node(), multiDriver, &localOpt, gatewayOpts, configDir, w, docker) + so, release, err := toSolveOpt(ctx, np.Node(), multiDriver, &localOpt, gatewayOpts, cfg, w, docker) opts[k] = localOpt if err != nil { return nil, err } - if err := saveLocalState(so, k, opt, np.Node(), configDir); err != nil { + if err := saveLocalState(so, k, opt, np.Node(), cfg); err != nil { return nil, err } addGitAttrs(so) diff --git a/build/localstate.go b/build/localstate.go index 5d902e44..f6bd1f71 100644 --- a/build/localstate.go +++ b/build/localstate.go @@ -5,10 +5,11 @@ import ( "github.com/docker/buildx/builder" "github.com/docker/buildx/localstate" + "github.com/docker/buildx/util/confutil" "github.com/moby/buildkit/client" ) -func saveLocalState(so *client.SolveOpt, target string, opts Options, node builder.Node, configDir string) error { +func saveLocalState(so *client.SolveOpt, target string, opts Options, node builder.Node, cfg *confutil.Config) error { var err error if so.Ref == "" { return nil @@ -30,7 +31,7 @@ func saveLocalState(so *client.SolveOpt, target string, opts Options, node build if lp == "" && dp == "" { return nil } - l, err := localstate.New(configDir) + l, err := localstate.New(cfg) if err != nil { return err } diff --git a/build/opt.go b/build/opt.go index 88f11dda..197c39b0 100644 --- a/build/opt.go +++ b/build/opt.go @@ -35,7 +35,7 @@ import ( "github.com/tonistiigi/fsutil" ) -func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *Options, bopts gateway.BuildOpts, configDir string, pw progress.Writer, docker *dockerutil.Client) (_ *client.SolveOpt, release func(), err error) { +func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *Options, bopts gateway.BuildOpts, cfg *confutil.Config, pw progress.Writer, docker *dockerutil.Client) (_ *client.SolveOpt, release func(), err error) { nodeDriver := node.Driver defers := make([]func(), 0, 2) releaseF := func() { @@ -271,7 +271,7 @@ func toSolveOpt(ctx context.Context, node builder.Node, multiDriver bool, opt *O // add node identifier to shared key if one was specified if so.SharedKey != "" { - so.SharedKey += ":" + confutil.TryNodeIdentifier(configDir) + so.SharedKey += ":" + cfg.TryNodeIdentifier() } if opt.Pull { diff --git a/builder/builder.go b/builder/builder.go index 1b25ebe8..dfa5051f 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -439,7 +439,7 @@ func Create(ctx context.Context, txn *store.Txn, dockerCli command.Cli, opts Cre if buildkitdConfigFile == "" { // if buildkit daemon config is not provided, check if the default one // is available and use it - if f, ok := confutil.DefaultConfigFile(dockerCli); ok { + if f, ok := confutil.NewConfig(dockerCli).BuildKitConfigFile(); ok { buildkitdConfigFile = f } } @@ -584,7 +584,7 @@ func Leave(ctx context.Context, txn *store.Txn, dockerCli command.Cli, opts Leav return err } - ls, err := localstate.New(confutil.ConfigDir(dockerCli)) + ls, err := localstate.New(confutil.NewConfig(dockerCli)) if err != nil { return err } diff --git a/commands/bake.go b/commands/bake.go index 785a4364..d5105df8 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -265,7 +265,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba } done := timeBuildCommand(mp, attributes) - resp, retErr := build.Build(ctx, nodes, bo, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), printer) + resp, retErr := build.Build(ctx, nodes, bo, dockerutil.NewClient(dockerCli), confutil.NewConfig(dockerCli), printer) if err := printer.Wait(); retErr == nil { retErr = err } @@ -470,7 +470,7 @@ func saveLocalStateGroup(dockerCli command.Cli, in bakeOptions, targets []string refs = append(refs, b.Ref) bo[k] = b } - l, err := localstate.New(confutil.ConfigDir(dockerCli)) + l, err := localstate.New(confutil.NewConfig(dockerCli)) if err != nil { return err } @@ -621,7 +621,7 @@ func bakeMetricAttributes(dockerCli command.Cli, driverType, url, cmdContext str commandNameAttribute.String("bake"), attribute.Stringer(string(commandOptionsHash), &bakeOptionsHash{ bakeOptions: options, - configDir: confutil.ConfigDir(dockerCli), + cfg: confutil.NewConfig(dockerCli), url: url, cmdContext: cmdContext, targets: targets, @@ -633,7 +633,7 @@ func bakeMetricAttributes(dockerCli command.Cli, driverType, url, cmdContext str type bakeOptionsHash struct { *bakeOptions - configDir string + cfg *confutil.Config url string cmdContext string targets []string @@ -657,7 +657,7 @@ func (o *bakeOptionsHash) String() string { joinedFiles := strings.Join(files, ",") joinedTargets := strings.Join(targets, ",") - salt := confutil.TryNodeIdentifier(o.configDir) + salt := o.cfg.TryNodeIdentifier() h := sha256.New() for _, s := range []string{url, cmdContext, joinedFiles, joinedTargets, salt} { diff --git a/commands/build.go b/commands/build.go index ba035bb6..e0e21761 100644 --- a/commands/build.go +++ b/commands/build.go @@ -238,7 +238,7 @@ func buildMetricAttributes(dockerCli command.Cli, driverType string, options *bu commandNameAttribute.String("build"), attribute.Stringer(string(commandOptionsHash), &buildOptionsHash{ buildOptions: options, - configDir: confutil.ConfigDir(dockerCli), + cfg: confutil.NewConfig(dockerCli), }), driverNameAttribute.String(options.builder), driverTypeAttribute.String(driverType), @@ -250,7 +250,7 @@ func buildMetricAttributes(dockerCli command.Cli, driverType string, options *bu // the fmt.Stringer interface. type buildOptionsHash struct { *buildOptions - configDir string + cfg *confutil.Config result string resultOnce sync.Once } @@ -267,7 +267,7 @@ func (o *buildOptionsHash) String() string { if contextPath != "-" && osutil.IsLocalDir(contextPath) { contextPath = osutil.ToAbs(contextPath) } - salt := confutil.TryNodeIdentifier(o.configDir) + salt := o.cfg.TryNodeIdentifier() h := sha256.New() for _, s := range []string{target, contextPath, dockerfile, salt} { diff --git a/controller/build/build.go b/controller/build/build.go index 71c5adf2..7a57dc7d 100644 --- a/controller/build/build.go +++ b/controller/build/build.go @@ -214,7 +214,7 @@ func buildTargets(ctx context.Context, dockerCli command.Cli, nodes []builder.No if generateResult { var mu sync.Mutex var idx int - resp, err = build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress, func(driverIndex int, gotRes *build.ResultHandle) { + resp, err = build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.NewConfig(dockerCli), progress, func(driverIndex int, gotRes *build.ResultHandle) { mu.Lock() defer mu.Unlock() if res == nil || driverIndex < idx { @@ -222,7 +222,7 @@ func buildTargets(ctx context.Context, dockerCli command.Cli, nodes []builder.No } }) } else { - resp, err = build.Build(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress) + resp, err = build.Build(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.NewConfig(dockerCli), progress) } if err != nil { return nil, res, err diff --git a/controller/remote/controller.go b/controller/remote/controller.go index 64dfcc0f..224805e1 100644 --- a/controller/remote/controller.go +++ b/controller/remote/controller.go @@ -258,7 +258,7 @@ func prepareRootDir(dockerCli command.Cli, config *serverConfig) (string, error) } func rootDataDir(dockerCli command.Cli) string { - return filepath.Join(confutil.ConfigDir(dockerCli), "controller") + return filepath.Join(confutil.NewConfig(dockerCli).Dir(), "controller") } func newBuildxClientAndCheck(ctx context.Context, addr string) (*Client, error) { diff --git a/go.mod b/go.mod index 5d63a5d8..8ec28adf 100644 --- a/go.mod +++ b/go.mod @@ -147,6 +147,7 @@ require ( github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect + github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect diff --git a/go.sum b/go.sum index d2a92f56..cb59d3b4 100644 --- a/go.sum +++ b/go.sum @@ -439,6 +439,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= +github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 h1:eUk79E1w8yMtXeHSzjKorxuC8qJOnyXQnLaJehxpJaI= +github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY= github.com/tonistiigi/fsutil v0.0.0-20241003195857-3f140a1299b0 h1:H9++AiQUqjwrOMA/DOpWhxWp3JLyyT+MN4sRPbMmwoY= github.com/tonistiigi/fsutil v0.0.0-20241003195857-3f140a1299b0/go.mod h1:Dl/9oEjK7IqnjAm21Okx/XIxUCFJzvh+XdVHUlBwXTw= github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= diff --git a/localstate/localstate.go b/localstate/localstate.go index 943494c7..4d486018 100644 --- a/localstate/localstate.go +++ b/localstate/localstate.go @@ -8,7 +8,7 @@ import ( "path/filepath" "sync" - "github.com/docker/docker/pkg/ioutils" + "github.com/docker/buildx/util/confutil" "github.com/pkg/errors" "golang.org/x/sync/errgroup" ) @@ -42,18 +42,18 @@ type StateGroup struct { } type LocalState struct { - root string + cfg *confutil.Config } -func New(root string) (*LocalState, error) { - if root == "" { - return nil, errors.Errorf("root dir empty") +func New(cfg *confutil.Config) (*LocalState, error) { + if cfg.Dir() == "" { + return nil, errors.Errorf("config dir empty") } - if err := os.MkdirAll(filepath.Join(root, refsDir), 0700); err != nil { + if err := cfg.MkdirAll(refsDir, 0700); err != nil { return nil, err } return &LocalState{ - root: root, + cfg: cfg, }, nil } @@ -61,7 +61,7 @@ func (ls *LocalState) ReadRef(builderName, nodeName, id string) (*State, error) if err := ls.validate(builderName, nodeName, id); err != nil { return nil, err } - dt, err := os.ReadFile(filepath.Join(ls.root, refsDir, builderName, nodeName, id)) + dt, err := os.ReadFile(filepath.Join(ls.cfg.Dir(), refsDir, builderName, nodeName, id)) if err != nil { return nil, err } @@ -76,19 +76,19 @@ func (ls *LocalState) SaveRef(builderName, nodeName, id string, st State) error if err := ls.validate(builderName, nodeName, id); err != nil { return err } - refDir := filepath.Join(ls.root, refsDir, builderName, nodeName) - if err := os.MkdirAll(refDir, 0700); err != nil { + refDir := filepath.Join(refsDir, builderName, nodeName) + if err := ls.cfg.MkdirAll(refDir, 0700); err != nil { return err } dt, err := json.Marshal(st) if err != nil { return err } - return ioutils.AtomicWriteFile(filepath.Join(refDir, id), dt, 0600) + return ls.cfg.AtomicWriteFile(filepath.Join(refDir, id), dt, 0644) } func (ls *LocalState) ReadGroup(id string) (*StateGroup, error) { - dt, err := os.ReadFile(filepath.Join(ls.root, refsDir, groupDir, id)) + dt, err := os.ReadFile(filepath.Join(ls.cfg.Dir(), refsDir, groupDir, id)) if err != nil { return nil, err } @@ -100,15 +100,15 @@ func (ls *LocalState) ReadGroup(id string) (*StateGroup, error) { } func (ls *LocalState) SaveGroup(id string, stg StateGroup) error { - refDir := filepath.Join(ls.root, refsDir, groupDir) - if err := os.MkdirAll(refDir, 0700); err != nil { + refDir := filepath.Join(refsDir, groupDir) + if err := ls.cfg.MkdirAll(refDir, 0700); err != nil { return err } dt, err := json.Marshal(stg) if err != nil { return err } - return ioutils.AtomicWriteFile(filepath.Join(refDir, id), dt, 0600) + return ls.cfg.AtomicWriteFile(filepath.Join(refDir, id), dt, 0600) } func (ls *LocalState) RemoveBuilder(builderName string) error { @@ -116,7 +116,7 @@ func (ls *LocalState) RemoveBuilder(builderName string) error { return errors.Errorf("builder name empty") } - dir := filepath.Join(ls.root, refsDir, builderName) + dir := filepath.Join(ls.cfg.Dir(), refsDir, builderName) if _, err := os.Lstat(dir); err != nil { if !os.IsNotExist(err) { return err @@ -147,7 +147,7 @@ func (ls *LocalState) RemoveBuilderNode(builderName string, nodeName string) err return errors.Errorf("node name empty") } - dir := filepath.Join(ls.root, refsDir, builderName, nodeName) + dir := filepath.Join(ls.cfg.Dir(), refsDir, builderName, nodeName) if _, err := os.Lstat(dir); err != nil { if !os.IsNotExist(err) { return err @@ -208,7 +208,7 @@ func (ls *LocalState) removeGroup(id string) error { if id == "" { return errors.Errorf("group ref empty") } - f := filepath.Join(ls.root, refsDir, groupDir, id) + f := filepath.Join(ls.cfg.Dir(), refsDir, groupDir, id) if _, err := os.Lstat(f); err != nil { if !os.IsNotExist(err) { return err diff --git a/localstate/localstate_test.go b/localstate/localstate_test.go index 94d4ee8e..180f8e4c 100644 --- a/localstate/localstate_test.go +++ b/localstate/localstate_test.go @@ -4,6 +4,7 @@ import ( "path/filepath" "testing" + "github.com/docker/buildx/util/confutil" "github.com/stretchr/testify/require" ) @@ -39,10 +40,10 @@ func newls(t *testing.T) *LocalState { t.Helper() tmpdir := t.TempDir() - l, err := New(tmpdir) + l, err := New(confutil.NewConfig(nil, confutil.WithDir(tmpdir))) require.NoError(t, err) require.DirExists(t, filepath.Join(tmpdir, refsDir)) - require.Equal(t, tmpdir, l.root) + require.Equal(t, tmpdir, l.cfg.Dir()) require.NoError(t, l.SaveRef(testBuilderName, testNodeName, testStateRefID, testStateRef)) diff --git a/store/store.go b/store/store.go index e1a6937d..d27fda62 100644 --- a/store/store.go +++ b/store/store.go @@ -8,7 +8,7 @@ import ( "time" "github.com/docker/buildx/localstate" - "github.com/docker/docker/pkg/ioutils" + "github.com/docker/buildx/util/confutil" "github.com/gofrs/flock" "github.com/opencontainers/go-digest" "github.com/pkg/errors" @@ -20,25 +20,25 @@ const ( activityDir = "activity" ) -func New(root string) (*Store, error) { - if err := os.MkdirAll(filepath.Join(root, instanceDir), 0700); err != nil { +func New(cfg *confutil.Config) (*Store, error) { + if err := cfg.MkdirAll(instanceDir, 0700); err != nil { return nil, err } - if err := os.MkdirAll(filepath.Join(root, defaultsDir), 0700); err != nil { + if err := cfg.MkdirAll(defaultsDir, 0700); err != nil { return nil, err } - if err := os.MkdirAll(filepath.Join(root, activityDir), 0700); err != nil { + if err := cfg.MkdirAll(activityDir, 0700); err != nil { return nil, err } - return &Store{root: root}, nil + return &Store{cfg: cfg}, nil } type Store struct { - root string + cfg *confutil.Config } func (s *Store) Txn() (*Txn, func(), error) { - l := flock.New(filepath.Join(s.root, ".lock")) + l := flock.New(filepath.Join(s.cfg.Dir(), ".lock")) if err := l.Lock(); err != nil { return nil, nil, err } @@ -54,7 +54,7 @@ type Txn struct { } func (t *Txn) List() ([]*NodeGroup, error) { - pp := filepath.Join(t.s.root, instanceDir) + pp := filepath.Join(t.s.cfg.Dir(), instanceDir) fis, err := os.ReadDir(pp) if err != nil { return nil, err @@ -84,7 +84,7 @@ func (t *Txn) NodeGroupByName(name string) (*NodeGroup, error) { if err != nil { return nil, err } - dt, err := os.ReadFile(filepath.Join(t.s.root, instanceDir, name)) + dt, err := os.ReadFile(filepath.Join(t.s.cfg.Dir(), instanceDir, name)) if err != nil { return nil, err } @@ -110,7 +110,7 @@ func (t *Txn) Save(ng *NodeGroup) error { if err != nil { return err } - return ioutils.AtomicWriteFile(filepath.Join(t.s.root, instanceDir, name), dt, 0600) + return t.s.cfg.AtomicWriteFile(filepath.Join(instanceDir, name), dt, 0600) } func (t *Txn) Remove(name string) error { @@ -121,14 +121,14 @@ func (t *Txn) Remove(name string) error { if err := t.RemoveLastActivity(name); err != nil { return err } - ls, err := localstate.New(t.s.root) + ls, err := localstate.New(t.s.cfg) if err != nil { return err } if err := ls.RemoveBuilder(name); err != nil { return err } - return os.RemoveAll(filepath.Join(t.s.root, instanceDir, name)) + return os.RemoveAll(filepath.Join(t.s.cfg.Dir(), instanceDir, name)) } func (t *Txn) SetCurrent(key, name string, global, def bool) error { @@ -141,28 +141,28 @@ func (t *Txn) SetCurrent(key, name string, global, def bool) error { if err != nil { return err } - if err := ioutils.AtomicWriteFile(filepath.Join(t.s.root, "current"), dt, 0600); err != nil { + if err := t.s.cfg.AtomicWriteFile("current", dt, 0600); err != nil { return err } h := toHash(key) if def { - if err := ioutils.AtomicWriteFile(filepath.Join(t.s.root, defaultsDir, h), []byte(name), 0600); err != nil { + if err := t.s.cfg.AtomicWriteFile(filepath.Join(defaultsDir, h), []byte(name), 0600); err != nil { return err } } else { - os.RemoveAll(filepath.Join(t.s.root, defaultsDir, h)) // ignore error + os.RemoveAll(filepath.Join(t.s.cfg.Dir(), defaultsDir, h)) // ignore error } return nil } func (t *Txn) UpdateLastActivity(ng *NodeGroup) error { - return ioutils.AtomicWriteFile(filepath.Join(t.s.root, activityDir, ng.Name), []byte(time.Now().UTC().Format(time.RFC3339)), 0600) + return t.s.cfg.AtomicWriteFile(filepath.Join(activityDir, ng.Name), []byte(time.Now().UTC().Format(time.RFC3339)), 0600) } func (t *Txn) GetLastActivity(ng *NodeGroup) (la time.Time, _ error) { - dt, err := os.ReadFile(filepath.Join(t.s.root, activityDir, ng.Name)) + dt, err := os.ReadFile(filepath.Join(t.s.cfg.Dir(), activityDir, ng.Name)) if err != nil { if os.IsNotExist(errors.Cause(err)) { return la, nil @@ -177,7 +177,7 @@ func (t *Txn) RemoveLastActivity(name string) error { if err != nil { return err } - return os.RemoveAll(filepath.Join(t.s.root, activityDir, name)) + return os.RemoveAll(filepath.Join(t.s.cfg.Dir(), activityDir, name)) } func (t *Txn) reset(key string) error { @@ -185,11 +185,11 @@ func (t *Txn) reset(key string) error { if err != nil { return err } - return ioutils.AtomicWriteFile(filepath.Join(t.s.root, "current"), dt, 0600) + return t.s.cfg.AtomicWriteFile("current", dt, 0600) } func (t *Txn) Current(key string) (*NodeGroup, error) { - dt, err := os.ReadFile(filepath.Join(t.s.root, "current")) + dt, err := os.ReadFile(filepath.Join(t.s.cfg.Dir(), "current")) if err != nil { if !os.IsNotExist(err) { return nil, err @@ -220,7 +220,7 @@ func (t *Txn) Current(key string) (*NodeGroup, error) { h := toHash(key) - dt, err = os.ReadFile(filepath.Join(t.s.root, defaultsDir, h)) + dt, err = os.ReadFile(filepath.Join(t.s.cfg.Dir(), defaultsDir, h)) if err != nil { if os.IsNotExist(err) { t.reset(key) diff --git a/store/store_test.go b/store/store_test.go index 46239d73..cdac3cdd 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/docker/buildx/util/confutil" "github.com/pkg/errors" "github.com/stretchr/testify/require" ) @@ -15,7 +16,7 @@ func TestEmptyStartup(t *testing.T) { require.NoError(t, err) defer os.RemoveAll(tmpdir) - s, err := New(tmpdir) + s, err := New(confutil.NewConfig(nil, confutil.WithDir(tmpdir))) require.NoError(t, err) txn, release, err := s.Txn() @@ -33,7 +34,7 @@ func TestNodeLocking(t *testing.T) { require.NoError(t, err) defer os.RemoveAll(tmpdir) - s, err := New(tmpdir) + s, err := New(confutil.NewConfig(nil, confutil.WithDir(tmpdir))) require.NoError(t, err) _, release, err := s.Txn() @@ -68,7 +69,7 @@ func TestNodeManagement(t *testing.T) { require.NoError(t, err) defer os.RemoveAll(tmpdir) - s, err := New(tmpdir) + s, err := New(confutil.NewConfig(nil, confutil.WithDir(tmpdir))) require.NoError(t, err) txn, release, err := s.Txn() @@ -240,7 +241,7 @@ func TestNodeInvalidName(t *testing.T) { t.Parallel() tmpdir := t.TempDir() - s, err := New(tmpdir) + s, err := New(confutil.NewConfig(nil, confutil.WithDir(tmpdir))) require.NoError(t, err) txn, release, err := s.Txn() diff --git a/store/storeutil/storeutil.go b/store/storeutil/storeutil.go index 41e7c264..e9b2a14f 100644 --- a/store/storeutil/storeutil.go +++ b/store/storeutil/storeutil.go @@ -17,7 +17,7 @@ import ( // GetStore returns current builder instance store func GetStore(dockerCli command.Cli) (*store.Txn, func(), error) { - s, err := store.New(confutil.ConfigDir(dockerCli)) + s, err := store.New(confutil.NewConfig(dockerCli)) if err != nil { return nil, nil, err } diff --git a/tests/build.go b/tests/build.go index 207f52d3..beb8fbe4 100644 --- a/tests/build.go +++ b/tests/build.go @@ -17,6 +17,7 @@ import ( "github.com/containerd/platforms" "github.com/creack/pty" "github.com/docker/buildx/localstate" + "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/gitutil" "github.com/moby/buildkit/client" "github.com/moby/buildkit/frontend/subrequests/lint" @@ -167,7 +168,7 @@ COPY --from=base /etc/bar /bar err = json.Unmarshal(dt, &md) require.NoError(t, err) - ls, err := localstate.New(buildxConfig(sb)) + ls, err := localstate.New(confutil.NewConfig(nil, confutil.WithDir(buildxConfig(sb)))) require.NoError(t, err) refParts := strings.Split(md.BuildRef, "/") @@ -209,7 +210,7 @@ COPY --from=base /etc/bar /bar err = json.Unmarshal(dt, &md) require.NoError(t, err) - ls, err := localstate.New(buildxConfig(sb)) + ls, err := localstate.New(confutil.NewConfig(nil, confutil.WithDir(buildxConfig(sb)))) require.NoError(t, err) refParts := strings.Split(md.BuildRef, "/") @@ -261,7 +262,7 @@ COPY foo /foo err = json.Unmarshal(dt, &md) require.NoError(t, err) - ls, err := localstate.New(buildxConfig(sb)) + ls, err := localstate.New(confutil.NewConfig(nil, confutil.WithDir(buildxConfig(sb)))) require.NoError(t, err) refParts := strings.Split(md.BuildRef, "/") diff --git a/util/confutil/config.go b/util/confutil/config.go index 54d5e0ec..58036ac8 100644 --- a/util/confutil/config.go +++ b/util/confutil/config.go @@ -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) diff --git a/util/confutil/config_unix.go b/util/confutil/config_unix.go new file mode 100644 index 00000000..00a59137 --- /dev/null +++ b/util/confutil/config_unix.go @@ -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 +} diff --git a/util/confutil/config_unix_test.go b/util/confutil/config_unix_test.go new file mode 100644 index 00000000..3907b47e --- /dev/null +++ b/util/confutil/config_unix_test.go @@ -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) + }) + } +} diff --git a/util/confutil/config_windows.go b/util/confutil/config_windows.go new file mode 100644 index 00000000..42e0968c --- /dev/null +++ b/util/confutil/config_windows.go @@ -0,0 +1,11 @@ +package confutil + +import "os" + +func sudoer(_ string) *chowner { + return nil +} + +func fileOwner(_ os.FileInfo) *chowner { + return nil +} diff --git a/util/confutil/node.go b/util/confutil/node.go deleted file mode 100644 index b6359608..00000000 --- a/util/confutil/node.go +++ /dev/null @@ -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 -} diff --git a/vendor/github.com/tonistiigi/dchapes-mode/.hgignore b/vendor/github.com/tonistiigi/dchapes-mode/.hgignore new file mode 100644 index 00000000..be8f61dd --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/.hgignore @@ -0,0 +1,5 @@ +syntax: glob +bench*.out* +cmode +coverage.out +coverage.txt diff --git a/vendor/github.com/tonistiigi/dchapes-mode/Dockerfile b/vendor/github.com/tonistiigi/dchapes-mode/Dockerfile new file mode 100644 index 00000000..b295c4fb --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/Dockerfile @@ -0,0 +1,29 @@ + +# syntax=docker/dockerfile:1 + +ARG GO_VERSION=1.23 +ARG XX_VERSION=1.5.0 + +FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx + +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS base +RUN apk add --no-cache git +COPY --from=xx / / +WORKDIR /src + +FROM base AS build +ARG TARGETPLATFORM +RUN --mount=target=. --mount=target=/go/pkg/mod,type=cache \ + --mount=target=/root/.cache,type=cache \ + xx-go build ./... + +FROM base AS test +ARG TESTFLAGS +RUN --mount=target=. --mount=target=/go/pkg/mod,type=cache \ + --mount=target=/root/.cache,type=cache \ + xx-go test -v -coverprofile=/tmp/coverage.txt -covermode=atomic ${TESTFLAGS} ./... + +FROM scratch AS test-coverage +COPY --from=test /tmp/coverage.txt /coverage-root.txt + +FROM build \ No newline at end of file diff --git a/vendor/github.com/tonistiigi/dchapes-mode/LICENSE b/vendor/github.com/tonistiigi/dchapes-mode/LICENSE new file mode 100644 index 00000000..a8743fb2 --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/LICENSE @@ -0,0 +1,22 @@ +Copyright © 2016-2018, Dave Chapeskie +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/tonistiigi/dchapes-mode/README.md b/vendor/github.com/tonistiigi/dchapes-mode/README.md new file mode 100644 index 00000000..dca34f9f --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/README.md @@ -0,0 +1,26 @@ +Mode +======== + +This is a fork of [hg.sr.ht/~dchapes/mode](https://hg.sr.ht/~dchapes/mode) with minimal patches and basic CI. + +[Mode](https://hg.sr.ht/~dchapes/mode) +is a [Go](http://golang.org/) package that provides +a native Go implementation of BSD's +[`setmode`](https://www.freebsd.org/cgi/man.cgi?query=setmode&sektion=3) +and `getmode` which can be used to modify the mode bits of +an [`os.FileMode`](https://golang.org/pkg/os#FileMode) value +based on a symbolic value as described by the +Unix [`chmod`](https://www.freebsd.org/cgi/man.cgi?query=chmod&sektion=1) command. + +[![Go Reference](https://pkg.go.dev/badge/hg.sr.ht/~dchapes/mode.svg)](https://pkg.go.dev/hg.sr.ht/~dchapes/mode) + +Online package documentation is available via +[pkg.go.dev](https://pkg.go.dev/hg.sr.ht/~dchapes/mode). + +To install: + + go get hg.sr.ht/~dchapes/mode + +or `go build` any Go code that imports it: + + import "hg.sr.ht/~dchapes/mode" diff --git a/vendor/github.com/tonistiigi/dchapes-mode/bits.go b/vendor/github.com/tonistiigi/dchapes-mode/bits.go new file mode 100644 index 00000000..4dbb08ad --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/bits.go @@ -0,0 +1,76 @@ +package mode + +import "os" + +type modet uint16 + +// Although many of these can be found in the syscall package +// we don't use those to avoid the dependency, add some more +// values, use non-exported Go names, and use octal for better clarity. +// +// Note that Go only uses the the nine least significant bits as "Unix +// permission bits" (os.ModePerm == 0777). We use chmod(1)'s octal +// definitions that include three further bits: isUID, isGID, and +// isTXT (07000). Go has os.ModeSetuid=1<<23, os.ModeSetgid=1<<22, +// and os.ModeSticy=1<<20 for these. We do this so that absolute +// octal values can include those bits as defined by chmod(1). +const ( + //ifDir = 040000 // directory + isUID = 04000 // set user id on execution + isGID = 02000 // set group id on execution + isTXT = 01000 // sticky bit + iRWXU = 00700 // RWX mask for owner + iRUser = 00400 // R for owner + iWUser = 00200 // W for owner + iXUser = 00100 // X for owner + iRWXG = 00070 // RWX mask for group + iRGroup = 00040 // R for group + iWGroup = 00020 // W for group + iXGroup = 00010 // X for group + iRWXO = 00007 // RWX mask for other + iROther = 00004 // R for other + iWOther = 00002 // W for other + iXOther = 00001 // X for other + + standardBits = isUID | isGID | iRWXU | iRWXG | iRWXO + + // os.FileMode bits we touch + fmBits = os.ModeSetuid | os.ModeSetgid | os.ModeSticky | os.ModePerm +) + +func fileModeToBits(fm os.FileMode) modet { + m := modet(fm.Perm()) + /* + if fm&os.ModeSetuid != 0 { + m |= isUID + } + if fm&os.ModeSetgid != 0 { + m |= isGID + } + if fm&os.ModeSticky != 0 { + m |= isTXT + } + */ + m |= modet(fm & (os.ModeSetuid | os.ModeSetgid) >> 12) + m |= modet(fm & os.ModeSticky >> 11) + return m +} + +func bitsToFileMode(old os.FileMode, m modet) os.FileMode { + fm := old &^ fmBits + fm |= os.FileMode(m) & os.ModePerm + /* + if m&isUID != 0 { + fm |= os.ModeSetuid + } + if m&isGID != 0 { + fm |= os.ModeSetgid + } + if m&isTXT != 0 { + fm |= os.ModeSticky + } + */ + fm |= os.FileMode(m&(isUID|isGID)) << 12 + fm |= os.FileMode(m&isTXT) << 11 + return fm +} diff --git a/vendor/github.com/tonistiigi/dchapes-mode/docker-bake.hcl b/vendor/github.com/tonistiigi/dchapes-mode/docker-bake.hcl new file mode 100644 index 00000000..1220e909 --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/docker-bake.hcl @@ -0,0 +1,24 @@ +variable "GO_VERSION" { + default = null +} + +group "default" { + targets = ["build"] +} + +target "build" { + args = { + GO_VERSION = GO_VERSION + } + output = ["type=cacheonly"] +} + +target "test" { + inherits = ["build"] + target = "test" +} + +target "cross" { + inherits = ["build"] + platforms = ["linux/amd64", "linux/386", "linux/arm64", "linux/arm", "linux/ppc64le", "linux/s390x", "darwin/amd64", "darwin/arm64", "windows/amd64", "windows/arm64", "freebsd/amd64", "freebsd/arm64"] +} \ No newline at end of file diff --git a/vendor/github.com/tonistiigi/dchapes-mode/mode.go b/vendor/github.com/tonistiigi/dchapes-mode/mode.go new file mode 100644 index 00000000..01d3444f --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/mode.go @@ -0,0 +1,546 @@ +/* + +Parts of this file are a heavily modified C to Go +translation of BSD's /usr/src/lib/libc/gen/setmode.c +that contains the following copyright notice: + + * Copyright (c) 1989, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Dave Borman at Cray Research, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + +*/ + +// Package mode provides a native Go implementation of BSD's setmode and getmode +// which can be used to modify the mode bits of an os.FileMode value based on +// a symbolic value as described by the Unix chmod command. +// +// For a full description of the mode string see chmod(1). +// Some examples include: +// +// 644 make a file readable by anyone and writable by the owner +// only. +// +// go-w deny write permission to group and others. +// +// =rw,+X set the read and write permissions to the usual defaults, +// but retain any execute permissions that are currently set. +// +// +X make a directory or file searchable/executable by everyone +// if it is already searchable/executable by anyone. +// +// 755 +// u=rwx,go=rx +// u=rwx,go=u-w make a file readable/executable by everyone and writable by +// the owner only. +// +// go= clear all mode bits for group and others. +// +// go=u-w set the group bits equal to the user bits, but clear the +// group write bit. +// +// See Also: +// +// setmode(3): https://www.freebsd.org/cgi/man.cgi?query=setmode&sektion=3 +// chmod(1): https://www.freebsd.org/cgi/man.cgi?query=chmod&sektion=1 +package mode + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" +) + +// Set is a set of changes to apply to an os.FileMode. +// Changes include setting or clearing specific bits, copying bits from one +// user class to another (e.g. "u=go" sets the user permissions to a copy of +// the group and other permsissions), etc. +type Set struct { + cmds []bitcmd +} + +type bitcmd struct { + cmd byte + cmd2 byte + bits modet +} + +const ( + cmd2Clear byte = 1 << iota + cmd2Set + cmd2GBits + cmd2OBits + cmd2UBits +) + +func (c bitcmd) String() string { + c2 := "" + if c.cmd2 != 0 { + c2 = " cmd2:" + if c.cmd2&cmd2Clear != 0 { + c2 += " CLR" + } + if c.cmd2&cmd2Set != 0 { + c2 += " SET" + } + if c.cmd2&cmd2UBits != 0 { + c2 += " UBITS" + } + if c.cmd2&cmd2GBits != 0 { + c2 += " GBITS" + } + if c.cmd2&cmd2OBits != 0 { + c2 += " OBITS" + } + } + return fmt.Sprintf("cmd: %q bits %#05o%s", c.cmd, c.bits, c2) +} + +// The String method will likely only be useful when testing. +func (s Set) String() string { + var buf strings.Builder + buf.Grow(21*len(s.cmds) + 10) + _, _ = buf.WriteString("set: {\n") + for _, c := range s.cmds { + _, _ = buf.WriteString(c.String()) + _ = buf.WriteByte('\n') + } + _, _ = buf.WriteString("}") + return buf.String() +} + +// ErrSyntax indicates an argument does not represent a valid mode. +var ErrSyntax = errors.New("invalid syntax") + +// Apply changes the provided os.FileMode based on the given umask and +// absolute or symbolic mode value. +// +// Apply is a convience to calling ParseWithUmask followed by Apply. +// Since it needs to parse the mode value string on each call it +// should only be used when mode value string will not be reapplied. +func Apply(s string, perm os.FileMode, umask uint) (os.FileMode, error) { + set, err := ParseWithUmask(s, umask) + if err != nil { + return 0, err + } + return set.Apply(perm), nil +} + +// Parse takes an absolute (octal) or symbolic mode value, +// as described in chmod(1), as an argument and returns +// the set of bit operations representing the mode value +// that can be applied to specific os.FileMode values. +// +// Same as ParseWithUmask(s, 0). +func Parse(s string) (Set, error) { + return ParseWithUmask(s, 0) +} + +// TODO(dchapes): A Set.Parse method that reuses existing memory. + +// TODO(dchapes): Only call syscall.Umask when abosolutely necessary and +// provide a Set method to query if set is umask dependant (and perhaps +// the umask that was in effect when parsed). + +// ParseWithUmask is like Parse but uses the provided +// file creation mask instead of calling syscall.Umask. +func ParseWithUmask(s string, umask uint) (Set, error) { + var m Set + if s == "" { + return m, ErrSyntax + } + + // If an absolute number, get it and return; + // disallow non-octal digits or illegal bits. + if d := s[0]; '0' <= d && d <= '9' { + v, err := strconv.ParseInt(s, 8, 16) + if err != nil { + return m, err + } + if v&^(standardBits|isTXT) != 0 { + return m, ErrSyntax + } + // We know this takes exactly two bitcmds. + m.cmds = make([]bitcmd, 0, 2) + m.addcmd('=', standardBits|isTXT, modet(v), 0) + return m, nil + } + + // Get a copy of the mask for the permissions that are mask relative. + // Flip the bits, we want what's not set. + var mask modet = ^modet(umask) + + // Pre-allocate room for several commands. + //m.cmds = make([]bitcmd, 0, 8) + + // Build list of bitcmd structs to set/clear/copy bits as described by + // each clause of the symbolic mode. + equalOpDone := false + for { + // First, find out which bits might be modified. + var who modet + whoLoop: + for { + if len(s) == 0 { + return Set{}, ErrSyntax + } + switch s[0] { + case 'a': + who |= standardBits + case 'u': + who |= isUID | iRWXU + case 'g': + who |= isGID | iRWXG + case 'o': + who |= iRWXO + default: + break whoLoop + } + s = s[1:] + } + + var op byte + getop: + op, s = s[0], s[1:] + switch op { + case '+', '-': + // Nothing. + case '=': + equalOpDone = false + default: + return Set{}, ErrSyntax + } + + who &^= isTXT + permLoop: + for perm, permX := modet(0), modet(0); ; s = s[1:] { + var b byte + if len(s) > 0 { + b = s[0] + } + switch b { + case 'r': + perm |= iRUser | iRGroup | iROther + case 's': + // If only "other" bits ignore set-id. + if who == 0 || who&^iRWXO != 0 { + perm |= isUID | isGID + } + case 't': + // If only "other bits ignore sticky. + if who == 0 || who&^iRWXO != 0 { + who |= isTXT + perm |= isTXT + } + case 'w': + perm |= iWUser | iWGroup | iWOther + case 'X': + if op == '+' { + permX = iXUser | iXGroup | iXOther + } + case 'x': + perm |= iXUser | iXGroup | iXOther + case 'u', 'g', 'o': + // Whenever we hit 'u', 'g', or 'o', we have + // to flush out any partial mode that we have, + // and then do the copying of the mode bits. + if perm != 0 { + m.addcmd(op, who, perm, mask) + perm = 0 + } + if op == '=' { + equalOpDone = true + } + if permX != 0 { + m.addcmd('X', who, permX, mask) + permX = 0 + } + m.addcmd(b, who, modet(op), mask) + default: + // Add any permissions that we haven't alread done. + if perm != 0 || op == '=' && !equalOpDone { + if op == '=' { + equalOpDone = true + } + m.addcmd(op, who, perm, mask) + //perm = 0 + } + if permX != 0 { + m.addcmd('X', who, permX, mask) + //permX = 0 + } + break permLoop + } + } + + if s == "" { + break + } + if s[0] != ',' { + goto getop + } + s = s[1:] + } + + m.compress() + return m, nil +} + +// Apply returns the os.FileMode after applying the set of changes. +func (s Set) Apply(perm os.FileMode) os.FileMode { + omode := fileModeToBits(perm) + newmode := omode + + // When copying the user, group or other bits around, we "know" + // where the bits are in the mode so that we can do shifts to + // copy them around. If we don't use shifts, it gets real + // grundgy with lots of single bit checks and bit sets. + common := func(c bitcmd, value modet) { + if c.cmd2&cmd2Clear != 0 { + var clrval modet + if c.cmd2&cmd2Set != 0 { + clrval = iRWXO + } else { + clrval = value + } + if c.cmd2&cmd2UBits != 0 { + newmode &^= clrval << 6 & c.bits + } + if c.cmd2&cmd2GBits != 0 { + newmode &^= clrval << 3 & c.bits + } + if c.cmd2&cmd2OBits != 0 { + newmode &^= clrval & c.bits + } + } + if c.cmd2&cmd2Set != 0 { + if c.cmd2&cmd2UBits != 0 { + newmode |= value << 6 & c.bits + } + if c.cmd2&cmd2GBits != 0 { + newmode |= value << 3 & c.bits + } + if c.cmd2&cmd2OBits != 0 { + newmode |= value & c.bits + } + } + } + + for _, c := range s.cmds { + switch c.cmd { + case 'u': + common(c, newmode&iRWXU>>6) + case 'g': + common(c, newmode&iRWXG>>3) + case 'o': + common(c, newmode&iRWXO) + + case '+': + newmode |= c.bits + case '-': + newmode &^= c.bits + + case 'X': + if omode&(iXUser|iXGroup|iXOther) != 0 || perm.IsDir() { + newmode |= c.bits + } + } + } + + return bitsToFileMode(perm, newmode) +} + +// Chmod is a convience routine that applies the changes in +// Set to the named file. To avoid some race conditions, +// it opens the file and uses os.File.Stat and +// os.File.Chmod rather than os.Stat and os.Chmod if possible. +func (s *Set) Chmod(name string) (old, new os.FileMode, err error) { + if f, err := os.Open(name); err == nil { // nolint: vetshadow + defer f.Close() // nolint: errcheck + return s.ChmodFile(f) + } + // Fallback to os.Stat and os.Chmod if we + // don't have permission to open the file. + fi, err := os.Stat(name) + if err != nil { + return 0, 0, err + } + old = fi.Mode() + new = s.Apply(old) + if new != old { + err = os.Chmod(name, new) + } + return old, new, err + +} + +// ChmodFile is a convience routine that applies +// the changes in Set to the open file f. +func (s *Set) ChmodFile(f *os.File) (old, new os.FileMode, err error) { + fi, err := f.Stat() + if err != nil { + return 0, 0, err + } + old = fi.Mode() + new = s.Apply(old) + if new != old { + err = f.Chmod(new) + } + return old, new, err +} + +func (s *Set) addcmd(op byte, who, oparg, mask modet) { + c := bitcmd{} + switch op { + case '=': + c.cmd = '-' + if who != 0 { + c.bits = who + } else { + c.bits = standardBits + } + + s.cmds = append(s.cmds, c) + //c = bitcmd{} // reset, not actually needed + op = '+' + fallthrough + case '+', '-', 'X': + c.cmd = op + if who != 0 { + c.bits = who & oparg + } else { + c.bits = mask & oparg + } + + case 'u', 'g', 'o': + c.cmd = op + if who != 0 { + if who&iRUser != 0 { + c.cmd2 |= cmd2UBits + } + if who&iRGroup != 0 { + c.cmd2 |= cmd2GBits + } + if who&iROther != 0 { + c.cmd2 |= cmd2OBits + } + c.bits = ^modet(0) + } else { + c.cmd2 = cmd2UBits | cmd2GBits | cmd2OBits + c.bits = mask + } + + switch oparg { + case '+': + c.cmd2 |= cmd2Set + case '-': + c.cmd2 |= cmd2Clear + case '=': + c.cmd2 |= cmd2Set | cmd2Clear + } + default: + panic("unreachable") + } + s.cmds = append(s.cmds, c) +} + +// compress by compacting consecutive '+', '-' and 'X' +// commands into at most 3 commands, one of each. The 'u', +// 'g' and 'o' commands continue to be separate. They could +// probably be compacted, but it's not worth the effort. +func (s *Set) compress() { + //log.Println("before:", *m) + //log.Println("Start compress:") + j := 0 + for i := 0; i < len(s.cmds); i++ { + c := s.cmds[i] + //log.Println(" read", i, c) + if strings.IndexByte("+-X", c.cmd) < 0 { + // Copy over any 'u', 'g', and 'o' commands. + if i != j { + s.cmds[j] = c + } + //log.Println(" wrote", j, "from", i) + j++ + continue + } + var setbits, clrbits, Xbits modet + for ; i < len(s.cmds); i++ { + c = s.cmds[i] + //log.Println(" scan", i, c) + switch c.cmd { + case '-': + clrbits |= c.bits + setbits &^= c.bits + Xbits &^= c.bits + continue + case '+': + setbits |= c.bits + clrbits &^= c.bits + Xbits &^= c.bits + continue + case 'X': + Xbits |= c.bits &^ setbits + continue + default: + i-- + } + break + } + if clrbits != 0 { + s.cmds[j].cmd = '-' + s.cmds[j].cmd2 = 0 + s.cmds[j].bits = clrbits + //log.Println(" wrote", j, "clrbits") + j++ + } + if setbits != 0 { + s.cmds[j].cmd = '+' + s.cmds[j].cmd2 = 0 + s.cmds[j].bits = setbits + //log.Println(" wrote", j, "setbits") + j++ + } + if Xbits != 0 { + s.cmds[j].cmd = 'X' + s.cmds[j].cmd2 = 0 + s.cmds[j].bits = Xbits + //log.Println(" wrote", j, "Xbits") + j++ + } + } + /* + if len(m.cmds) != j { + log.Println("compressed", len(m.cmds), "down to", j) + } + */ + s.cmds = s.cmds[:j] + //log.Println("after:", *m) +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/copy.go b/vendor/github.com/tonistiigi/fsutil/copy/copy.go new file mode 100644 index 00000000..c2b1ab97 --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/copy.go @@ -0,0 +1,691 @@ +package fs + +import ( + "context" + "os" + "path" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + "github.com/containerd/continuity/fs" + "github.com/moby/patternmatcher" + "github.com/pkg/errors" + mode "github.com/tonistiigi/dchapes-mode" + "github.com/tonistiigi/fsutil" +) + +var bufferPool = &sync.Pool{ + New: func() interface{} { + buffer := make([]byte, 32*1024) + return &buffer + }, +} + +func rootPath(root, p string, followLinks bool) (string, error) { + p = filepath.Join("/", p) + if p == "/" { + return root, nil + } + if followLinks { + return fs.RootPath(root, p) + } + d, f := filepath.Split(p) + ppath, err := fs.RootPath(root, d) + if err != nil { + return "", err + } + return filepath.Join(ppath, f), nil +} + +func ResolveWildcards(root, src string, followLinks bool) ([]string, error) { + d1, d2 := splitWildcards(src) + if d2 != "" { + p, err := rootPath(root, d1, followLinks) + if err != nil { + return nil, err + } + matches, err := resolveWildcards(p, d2) + if err != nil { + return nil, err + } + for i, m := range matches { + p, err := rel(root, m) + if err != nil { + return nil, err + } + matches[i] = p + } + return matches, nil + } + return []string{d1}, nil +} + +// Copy copies files using `cp -a` semantics. +// Copy is likely unsafe to be used in non-containerized environments. +func Copy(ctx context.Context, srcRoot, src, dstRoot, dst string, opts ...Opt) error { + var ci CopyInfo + for _, o := range opts { + o(&ci) + } + ensureDstPath := dst + if d, f := filepath.Split(dst); f != "" && f != "." { + ensureDstPath = d + } + if ensureDstPath != "" { + ensureDstPath, err := fs.RootPath(dstRoot, ensureDstPath) + if err != nil { + return err + } + if err := MkdirAll(ensureDstPath, 0755, ci.Chown, ci.Utime); err != nil { + return err + } + } + + var modeSet *mode.Set + if ci.ModeStr != "" { + ms, err := mode.ParseWithUmask(ci.ModeStr, 0) + if err != nil { + return err + } + modeSet = &ms + } + + dst, err := fs.RootPath(dstRoot, filepath.Clean(dst)) + if err != nil { + return err + } + + c, err := newCopier(dstRoot, ci.Chown, ci.Utime, ci.Mode, modeSet, ci.XAttrErrorHandler, ci.IncludePatterns, ci.ExcludePatterns, ci.AlwaysReplaceExistingDestPaths, ci.ChangeFunc) + if err != nil { + return err + } + srcs := []string{src} + + if ci.AllowWildcards { + matches, err := ResolveWildcards(srcRoot, src, ci.FollowLinks) + if err != nil { + return err + } + if len(matches) == 0 { + return errors.Errorf("no matches found: %s", src) + } + srcs = matches + } + + for _, src := range srcs { + srcFollowed, err := rootPath(srcRoot, src, ci.FollowLinks) + if err != nil { + return err + } + dst, err := c.prepareTargetDir(srcFollowed, src, dst, ci.CopyDirContents) + if err != nil { + return err + } + if err := c.copy(ctx, srcFollowed, "", dst, false, patternmatcher.MatchInfo{}, patternmatcher.MatchInfo{}); err != nil { + return err + } + } + + return nil +} + +func (c *copier) prepareTargetDir(srcFollowed, src, destPath string, copyDirContents bool) (string, error) { + fiSrc, err := os.Lstat(srcFollowed) + if err != nil { + return "", err + } + + fiDest, err := os.Stat(destPath) + if err != nil { + if !os.IsNotExist(err) { + return "", errors.Wrap(err, "failed to lstat destination path") + } + } + + if (!copyDirContents && fiSrc.IsDir() && fiDest != nil) || (!fiSrc.IsDir() && fiDest != nil && fiDest.IsDir()) { + destPath = filepath.Join(destPath, filepath.Base(src)) + } + + target := filepath.Dir(destPath) + + if copyDirContents && fiSrc.IsDir() && fiDest == nil { + target = destPath + } + if err := MkdirAll(target, 0755, c.chown, c.utime); err != nil { + return "", err + } + + return destPath, nil +} + +type User struct { + UID, GID int + SID string +} + +type Chowner func(*User) (*User, error) + +type XAttrErrorHandler func(dst, src, xattrKey string, err error) error + +type CopyInfo struct { + Chown Chowner + Utime *time.Time + AllowWildcards bool + Mode *int + // ModeStr is mode in non-octal format. Overrides Mode if non-empty. + ModeStr string + XAttrErrorHandler XAttrErrorHandler + CopyDirContents bool + FollowLinks bool + // Include only files/dirs matching at least one of these patterns + IncludePatterns []string + // Exclude files/dir matching any of these patterns (even if they match an include pattern) + ExcludePatterns []string + // If true, any source path that overwrite existing destination paths will always replace + // the existing destination path, even if they are of different types (e.g. a directory will + // replace any existing symlink or file) + AlwaysReplaceExistingDestPaths bool + ChangeFunc fsutil.ChangeFunc +} + +type Opt func(*CopyInfo) + +func WithCopyInfo(ci CopyInfo) func(*CopyInfo) { + return func(c *CopyInfo) { + *c = ci + } +} + +func WithChown(uid, gid int) Opt { + return func(ci *CopyInfo) { + ci.Chown = func(*User) (*User, error) { + return &User{UID: uid, GID: gid}, nil + } + } +} + +func AllowWildcards(ci *CopyInfo) { + ci.AllowWildcards = true +} + +func WithXAttrErrorHandler(h XAttrErrorHandler) Opt { + return func(ci *CopyInfo) { + ci.XAttrErrorHandler = h + } +} + +func AllowXAttrErrors(ci *CopyInfo) { + h := func(string, string, string, error) error { + return nil + } + WithXAttrErrorHandler(h)(ci) +} + +func WithIncludePattern(includePattern string) Opt { + return func(ci *CopyInfo) { + ci.IncludePatterns = append(ci.IncludePatterns, includePattern) + } +} + +func WithExcludePattern(excludePattern string) Opt { + return func(ci *CopyInfo) { + ci.ExcludePatterns = append(ci.ExcludePatterns, excludePattern) + } +} + +func WithChangeNotifier(fn fsutil.ChangeFunc) Opt { + return func(ci *CopyInfo) { + ci.ChangeFunc = fn + } +} + +type copier struct { + chown Chowner + utime *time.Time + mode *int + modeSet *mode.Set + inodes map[uint64]string + xattrErrorHandler XAttrErrorHandler + includePatternMatcher *patternmatcher.PatternMatcher + excludePatternMatcher *patternmatcher.PatternMatcher + parentDirs []parentDir + changefn fsutil.ChangeFunc + root string + alwaysReplaceExistingDestPaths bool +} + +type parentDir struct { + srcPath string + dstPath string + copied bool +} + +func newCopier(root string, chown Chowner, tm *time.Time, mode *int, modeSet *mode.Set, xeh XAttrErrorHandler, includePatterns, excludePatterns []string, alwaysReplaceExistingDestPaths bool, changeFunc fsutil.ChangeFunc) (*copier, error) { + if xeh == nil { + xeh = func(dst, src, key string, err error) error { + return err + } + } + + var includePatternMatcher *patternmatcher.PatternMatcher + if len(includePatterns) != 0 { + var err error + includePatternMatcher, err = patternmatcher.New(includePatterns) + if err != nil { + return nil, errors.Wrapf(err, "invalid includepatterns: %s", includePatterns) + } + } + + var excludePatternMatcher *patternmatcher.PatternMatcher + if len(excludePatterns) != 0 { + var err error + excludePatternMatcher, err = patternmatcher.New(excludePatterns) + if err != nil { + return nil, errors.Wrapf(err, "invalid excludepatterns: %s", excludePatterns) + } + } + + return &copier{ + root: root, + inodes: map[uint64]string{}, + chown: chown, + utime: tm, + xattrErrorHandler: xeh, + mode: mode, + modeSet: modeSet, + includePatternMatcher: includePatternMatcher, + excludePatternMatcher: excludePatternMatcher, + changefn: changeFunc, + alwaysReplaceExistingDestPaths: alwaysReplaceExistingDestPaths, + }, nil +} + +// dest is always clean +func (c *copier) copy(ctx context.Context, src, srcComponents, target string, overwriteTargetMetadata bool, parentIncludeMatchInfo, parentExcludeMatchInfo patternmatcher.MatchInfo) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + fi, err := os.Lstat(src) + if err != nil { + return errors.Wrapf(err, "failed to stat %s", src) + } + targetFi, err := os.Lstat(target) + if err != nil && !os.IsNotExist(err) { + return errors.Wrapf(err, "failed to stat %s", src) + } + + include := true + var ( + includeMatchInfo patternmatcher.MatchInfo + excludeMatchInfo patternmatcher.MatchInfo + ) + if srcComponents != "" { + matchesIncludePattern := false + matchesExcludePattern := false + matchesIncludePattern, includeMatchInfo, err = c.include(srcComponents, fi, parentIncludeMatchInfo) + if err != nil { + return err + } + include = matchesIncludePattern + + matchesExcludePattern, excludeMatchInfo, err = c.exclude(srcComponents, fi, parentExcludeMatchInfo) + if err != nil { + return err + } + if matchesExcludePattern { + include = false + } + } + + if include { + if err := c.removeTargetIfNeeded(src, target, fi, targetFi); err != nil { + return err + } + + if err := c.createParentDirs(src, srcComponents, target, overwriteTargetMetadata); err != nil { + return err + } + } + + if !fi.IsDir() { + if !include { + return nil + } + + if err := ensureEmptyFileTarget(target); err != nil { + return err + } + } + + copyFileInfo := include + restoreFileTimestamp := false + notify := true + + switch { + case fi.IsDir(): + if created, err := c.copyDirectory( + ctx, src, srcComponents, target, fi, overwriteTargetMetadata, + include, includeMatchInfo, excludeMatchInfo, + ); err != nil { + return err + } else if !overwriteTargetMetadata { + // if we aren't supposed to overwrite existing target metadata, + // then we only need to copy the new file info if we newly created + // it, or restore the previous file timestamp if not + copyFileInfo = created + restoreFileTimestamp = !created + } + notify = false + case (fi.Mode() & os.ModeType) == 0: + link, err := getLinkSource(target, fi, c.inodes) + if err != nil { + return errors.Wrap(err, "failed to get hardlink") + } + if link != "" { + if err := os.Link(link, target); err != nil { + return errors.Wrap(err, "failed to create hard link") + } + } else if err := copyFile(src, target); err != nil { + return errors.Wrap(err, "failed to copy files") + } + case (fi.Mode() & os.ModeSymlink) == os.ModeSymlink: + link, err := os.Readlink(src) + if err != nil { + return errors.Wrapf(err, "failed to read link: %s", src) + } + if err := os.Symlink(link, target); err != nil { + return errors.Wrapf(err, "failed to create symlink: %s", target) + } + case (fi.Mode() & os.ModeDevice) == os.ModeDevice, + (fi.Mode() & os.ModeNamedPipe) == os.ModeNamedPipe, + (fi.Mode() & os.ModeSocket) == os.ModeSocket: + if err := copyDevice(target, fi); err != nil { + return errors.Wrapf(err, "failed to create device") + } + } + + if copyFileInfo { + if err := c.copyFileInfo(fi, src, target); err != nil { + return errors.Wrap(err, "failed to copy file info") + } + + if err := copyXAttrs(target, src, c.xattrErrorHandler); err != nil { + return errors.Wrap(err, "failed to copy xattrs") + } + } else if restoreFileTimestamp && targetFi != nil { + if err := c.copyFileTimestamp(fi, target); err != nil { + return errors.Wrap(err, "failed to restore file timestamp") + } + } + if notify { + if err := c.notifyChange(target, fi); err != nil { + return err + } + } + return nil +} + +func (c *copier) notifyChange(target string, fi os.FileInfo) error { + if c.changefn != nil { + if err := c.changefn(fsutil.ChangeKindAdd, path.Clean(strings.TrimPrefix(target, c.root)), fi, nil); err != nil { + return errors.Wrap(err, "failed to notify file change") + } + } + return nil +} + +func (c *copier) include(path string, fi os.FileInfo, parentIncludeMatchInfo patternmatcher.MatchInfo) (bool, patternmatcher.MatchInfo, error) { + if c.includePatternMatcher == nil { + return true, patternmatcher.MatchInfo{}, nil + } + + m, matchInfo, err := c.includePatternMatcher.MatchesUsingParentResults(path, parentIncludeMatchInfo) + if err != nil { + return false, matchInfo, errors.Wrap(err, "failed to match includepatterns") + } + return m, matchInfo, nil +} + +func (c *copier) exclude(path string, fi os.FileInfo, parentExcludeMatchInfo patternmatcher.MatchInfo) (bool, patternmatcher.MatchInfo, error) { + if c.excludePatternMatcher == nil { + return false, patternmatcher.MatchInfo{}, nil + } + + m, matchInfo, err := c.excludePatternMatcher.MatchesUsingParentResults(path, parentExcludeMatchInfo) + if err != nil { + return false, matchInfo, errors.Wrap(err, "failed to match excludepatterns") + } + return m, matchInfo, nil +} + +func (c *copier) removeTargetIfNeeded(src, target string, srcFi, targetFi os.FileInfo) error { + if !c.alwaysReplaceExistingDestPaths { + return nil + } + if targetFi == nil { + // already doesn't exist + return nil + } + if srcFi.IsDir() && targetFi.IsDir() { + // directories are merged, not replaced + return nil + } + return os.RemoveAll(target) +} + +// Delayed creation of parent directories when a file or dir matches an include +// pattern. +func (c *copier) createParentDirs(src, srcComponents, target string, overwriteTargetMetadata bool) error { + for i, parentDir := range c.parentDirs { + if parentDir.copied { + continue + } + + fi, err := os.Stat(parentDir.srcPath) + if err != nil { + return errors.Wrapf(err, "failed to stat %s", src) + } + if !fi.IsDir() { + return errors.Errorf("%s is not a directory", parentDir.srcPath) + } + + created, err := copyDirectoryOnly(parentDir.srcPath, parentDir.dstPath, fi, overwriteTargetMetadata) + if err != nil { + return err + } + if created { + if err := c.copyFileInfo(fi, parentDir.srcPath, parentDir.dstPath); err != nil { + return errors.Wrap(err, "failed to copy file info") + } + + if err := copyXAttrs(parentDir.dstPath, parentDir.srcPath, c.xattrErrorHandler); err != nil { + return errors.Wrap(err, "failed to copy xattrs") + } + } + + c.parentDirs[i].copied = true + } + return nil +} + +func (c *copier) copyDirectory( + ctx context.Context, + src string, + srcComponents string, + dst string, + stat os.FileInfo, + overwriteTargetMetadata bool, + include bool, + includeMatchInfo patternmatcher.MatchInfo, + excludeMatchInfo patternmatcher.MatchInfo, +) (bool, error) { + if !stat.IsDir() { + return false, errors.Errorf("source is not directory") + } + + created := false + + parentDir := parentDir{ + srcPath: src, + dstPath: dst, + } + + // If this directory passed include/exclude matching directly, go ahead + // and create the directory. Otherwise, delay to handle include + // patterns like a/*/c where we do not want to create a/b until we + // encounter a/b/c. + if include { + var err error + created, err = copyDirectoryOnly(src, dst, stat, overwriteTargetMetadata) + if err != nil { + return created, err + } + if created || overwriteTargetMetadata { + if err := c.notifyChange(dst, stat); err != nil { + return created, err + } + } + parentDir.copied = true + } + + c.parentDirs = append(c.parentDirs, parentDir) + + defer func() { + c.parentDirs = c.parentDirs[:len(c.parentDirs)-1] + }() + + fis, err := os.ReadDir(src) + if err != nil { + return false, errors.Wrapf(err, "failed to read %s", src) + } + + for _, fi := range fis { + if err := c.copy( + ctx, + filepath.Join(src, fi.Name()), filepath.Join(srcComponents, fi.Name()), + filepath.Join(dst, fi.Name()), + true, includeMatchInfo, excludeMatchInfo, + ); err != nil { + return false, err + } + } + + return created, nil +} + +func copyDirectoryOnly(src, dst string, stat os.FileInfo, overwriteTargetMetadata bool) (bool, error) { + if st, err := os.Lstat(dst); err != nil { + if !os.IsNotExist(err) { + return false, err + } + if err := os.Mkdir(dst, stat.Mode()); err != nil { + return false, errors.Wrapf(err, "failed to mkdir %s", dst) + } + return true, nil + } else if !st.IsDir() { + return false, errors.Errorf("cannot copy to non-directory: %s", dst) + } else if overwriteTargetMetadata { + if err := os.Chmod(dst, stat.Mode()); err != nil { + return false, errors.Wrapf(err, "failed to chmod on %s", dst) + } + } + return false, nil +} + +func ensureEmptyFileTarget(dst string) error { + fi, err := os.Lstat(dst) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return errors.Wrap(err, "failed to lstat file target") + } + if fi.IsDir() { + return errors.Errorf("cannot replace to directory %s with file", dst) + } + return os.Remove(dst) +} + +func containsWildcards(name string) bool { + isWindows := runtime.GOOS == "windows" + for i := 0; i < len(name); i++ { + ch := name[i] + if ch == '\\' && !isWindows { + i++ + } else if ch == '*' || ch == '?' || ch == '[' { + return true + } + } + return false +} + +func splitWildcards(p string) (d1, d2 string) { + parts := strings.Split(filepath.Join(p), string(filepath.Separator)) + var p1, p2 []string + var found bool + for _, p := range parts { + if !found && containsWildcards(p) { + found = true + } + if p == "" { + p = "/" + } + if !found { + p1 = append(p1, p) + } else { + p2 = append(p2, p) + } + } + return filepath.Join(p1...), filepath.Join(p2...) +} + +func resolveWildcards(basePath, comp string) ([]string, error) { + var out []string + err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + rel, err := rel(basePath, path) + if err != nil { + return err + } + if rel == "." { + return nil + } + if match, _ := filepath.Match(comp, rel); !match { + return nil + } + out = append(out, path) + if info.IsDir() { + return filepath.SkipDir + } + return nil + }) + if err != nil { + return nil, err + } + return out, nil +} + +// rel makes a path relative to base path. Same as `filepath.Rel` but can also +// handle UUID paths in windows. +func rel(basepath, targpath string) (string, error) { + // filepath.Rel can't handle UUID paths in windows + if runtime.GOOS == "windows" { + pfx := basepath + `\` + if strings.HasPrefix(targpath, pfx) { + p := strings.TrimPrefix(targpath, pfx) + if p == "" { + p = "." + } + return p, nil + } + } + return filepath.Rel(basepath, targpath) +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/copy_darwin.go b/vendor/github.com/tonistiigi/fsutil/copy/copy_darwin.go new file mode 100644 index 00000000..0cdc00a8 --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/copy_darwin.go @@ -0,0 +1,47 @@ +//go:build darwin +// +build darwin + +package fs + +import ( + "io" + "os" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" +) + +func copyFile(source, target string) error { + if err := unix.Clonefileat(unix.AT_FDCWD, source, unix.AT_FDCWD, target, unix.CLONE_NOFOLLOW); err != nil { + if err != unix.EINVAL && err != unix.EXDEV { + return err + } + } else { + return nil + } + + src, err := os.Open(source) + if err != nil { + return errors.Wrapf(err, "failed to open source %s", source) + } + defer src.Close() + tgt, err := os.Create(target) + if err != nil { + return errors.Wrapf(err, "failed to open target %s", target) + } + defer tgt.Close() + + return copyFileContent(tgt, src) +} + +func copyFileContent(dst, src *os.File) error { + buf := bufferPool.Get().(*[]byte) + _, err := io.CopyBuffer(dst, src, *buf) + bufferPool.Put(buf) + + return err +} + +func mknod(dst string, mode uint32, rDev int) error { + return unix.Mknod(dst, uint32(mode), rDev) +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/copy_freebsd.go b/vendor/github.com/tonistiigi/fsutil/copy/copy_freebsd.go new file mode 100644 index 00000000..1b9dbb3d --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/copy_freebsd.go @@ -0,0 +1,38 @@ +//go:build freebsd +// +build freebsd + +package fs + +import ( + "io" + "os" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" +) + +func copyFile(source, target string) error { + src, err := os.Open(source) + if err != nil { + return errors.Wrapf(err, "failed to open source %s", source) + } + defer src.Close() + tgt, err := os.Create(target) + if err != nil { + return errors.Wrapf(err, "failed to open target %s", target) + } + defer tgt.Close() + + return copyFileContent(tgt, src) +} + +func copyFileContent(dst, src *os.File) error { + buf := bufferPool.Get().(*[]byte) + _, err := io.CopyBuffer(dst, src, *buf) + bufferPool.Put(buf) + return err +} + +func mknod(dst string, mode uint32, rDev int) error { + return unix.Mknod(dst, uint32(mode), uint64(rDev)) +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/copy_linux.go b/vendor/github.com/tonistiigi/fsutil/copy/copy_linux.go new file mode 100644 index 00000000..9b046c53 --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/copy_linux.go @@ -0,0 +1,129 @@ +package fs + +import ( + "io" + "math" + "os" + "syscall" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" +) + +func getUIDGID(fi os.FileInfo) (uid, gid int) { + st := fi.Sys().(*syscall.Stat_t) + return int(st.Uid), int(st.Gid) +} + +func (c *copier) copyFileInfo(fi os.FileInfo, src, name string) error { + chown := c.chown + uid, gid := getUIDGID(fi) + old := &User{UID: uid, GID: gid} + if chown == nil { + chown = func(u *User) (*User, error) { + return u, nil + } + } + if err := Chown(name, old, chown); err != nil { + return errors.Wrapf(err, "failed to chown %s", name) + } + + m := fi.Mode() + if c.modeSet != nil { + m = c.modeSet.Apply(m) + } else if c.mode != nil { + m = os.FileMode(*c.mode).Perm() + if *c.mode&syscall.S_ISGID != 0 { + m |= os.ModeSetgid + } + if *c.mode&syscall.S_ISUID != 0 { + m |= os.ModeSetuid + } + if *c.mode&syscall.S_ISVTX != 0 { + m |= os.ModeSticky + } + } + if (fi.Mode() & os.ModeSymlink) != os.ModeSymlink { + if err := os.Chmod(name, m); err != nil { + return errors.Wrapf(err, "failed to chmod %s", name) + } + } + + if err := c.copyFileTimestamp(fi, name); err != nil { + return err + } + + return nil +} + +func (c *copier) copyFileTimestamp(fi os.FileInfo, name string) error { + if c.utime != nil { + return Utimes(name, c.utime) + } + + st := fi.Sys().(*syscall.Stat_t) + timespec := []unix.Timespec{unix.Timespec(StatAtime(st)), unix.Timespec(StatMtime(st))} + if err := unix.UtimesNanoAt(unix.AT_FDCWD, name, timespec, unix.AT_SYMLINK_NOFOLLOW); err != nil { + return errors.Wrapf(err, "failed to utime %s", name) + } + return nil +} + +func copyFile(source, target string) error { + src, err := os.Open(source) + if err != nil { + return errors.Wrapf(err, "failed to open source %s", source) + } + defer src.Close() + tgt, err := os.Create(target) + if err != nil { + return errors.Wrapf(err, "failed to open target %s", target) + } + defer tgt.Close() + + return copyFileContent(tgt, src) +} + +func copyFileContent(dst, src *os.File) error { + st, err := src.Stat() + if err != nil { + return errors.Wrap(err, "unable to stat source") + } + + var written int64 + size := st.Size() + first := true + + for written < size { + var desired int + if size-written > math.MaxInt32 { + desired = int(math.MaxInt32) + } else { + desired = int(size - written) + } + + n, err := unix.CopyFileRange(int(src.Fd()), nil, int(dst.Fd()), nil, desired, 0) + if err != nil { + // matches go/src/internal/poll/copy_file_range_linux.go + if (err != unix.ENOSYS && err != unix.EXDEV && err != unix.EPERM && err != syscall.EIO && err != unix.EOPNOTSUPP && err != syscall.EINVAL) || !first { + return errors.Wrap(err, "copy file range failed") + } + + buf := bufferPool.Get().(*[]byte) + _, err = io.CopyBuffer(dst, src, *buf) + bufferPool.Put(buf) + if err != nil { + return errors.Wrap(err, "userspace copy failed") + } + return nil + } + + first = false + written += int64(n) + } + return nil +} + +func mknod(dst string, mode uint32, rDev int) error { + return unix.Mknod(dst, uint32(mode), rDev) +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/copy_nowindows.go b/vendor/github.com/tonistiigi/fsutil/copy/copy_nowindows.go new file mode 100644 index 00000000..382fe201 --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/copy_nowindows.go @@ -0,0 +1,46 @@ +//go:build !windows +// +build !windows + +package fs + +import ( + "os" + "syscall" + + "github.com/pkg/errors" + + "github.com/containerd/continuity/sysx" +) + +// copyXAttrs requires xeh to be non-nil +func copyXAttrs(dst, src string, xeh XAttrErrorHandler) error { + xattrKeys, err := sysx.LListxattr(src) + if err != nil { + return xeh(dst, src, "", errors.Wrapf(err, "failed to list xattrs on %s", src)) + } + for _, xattr := range xattrKeys { + data, err := sysx.LGetxattr(src, xattr) + if err != nil { + return xeh(dst, src, xattr, errors.Wrapf(err, "failed to get xattr %q on %s", xattr, src)) + } + if err := sysx.LSetxattr(dst, xattr, data, 0); err != nil { + return xeh(dst, src, xattr, errors.Wrapf(err, "failed to set xattr %q on %s", xattr, dst)) + } + } + + return nil +} + +func copyDevice(dst string, fi os.FileInfo) error { + st, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return errors.New("unsupported stat type") + } + var rDev int + if fi.Mode()&os.ModeDevice == os.ModeDevice || fi.Mode()&os.ModeCharDevice == os.ModeCharDevice { + rDev = int(st.Rdev) + } + mode := st.Mode + mode &^= syscall.S_IFSOCK // socket copied as stub + return mknod(dst, uint32(mode), rDev) +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/copy_unix.go b/vendor/github.com/tonistiigi/fsutil/copy/copy_unix.go new file mode 100644 index 00000000..e90a41d3 --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/copy_unix.go @@ -0,0 +1,70 @@ +//go:build solaris || darwin || freebsd +// +build solaris darwin freebsd + +package fs + +import ( + "os" + "syscall" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" +) + +func getUIDGID(fi os.FileInfo) (uid, gid int) { + st := fi.Sys().(*syscall.Stat_t) + return int(st.Uid), int(st.Gid) +} + +func (c *copier) copyFileInfo(fi os.FileInfo, src, name string) error { + chown := c.chown + uid, gid := getUIDGID(fi) + old := &User{UID: uid, GID: gid} + if chown == nil { + chown = func(u *User) (*User, error) { + return u, nil + } + } + if err := Chown(name, old, chown); err != nil { + return errors.Wrapf(err, "failed to chown %s", name) + } + + m := fi.Mode() + if c.modeSet != nil { + m = c.modeSet.Apply(m) + } else if c.mode != nil { + m = os.FileMode(*c.mode).Perm() + if *c.mode&syscall.S_ISGID != 0 { + m |= os.ModeSetgid + } + if *c.mode&syscall.S_ISUID != 0 { + m |= os.ModeSetuid + } + if *c.mode&syscall.S_ISVTX != 0 { + m |= os.ModeSticky + } + } + if (fi.Mode() & os.ModeSymlink) != os.ModeSymlink { + if err := os.Chmod(name, m); err != nil { + return errors.Wrapf(err, "failed to chmod %s", name) + } + } + + if err := c.copyFileTimestamp(fi, name); err != nil { + return err + } + return nil +} + +func (c *copier) copyFileTimestamp(fi os.FileInfo, name string) error { + if c.utime != nil { + return Utimes(name, c.utime) + } + + st := fi.Sys().(*syscall.Stat_t) + timespec := []unix.Timespec{unix.Timespec(StatAtime(st)), unix.Timespec(StatMtime(st))} + if err := unix.UtimesNanoAt(unix.AT_FDCWD, name, timespec, unix.AT_SYMLINK_NOFOLLOW); err != nil { + return errors.Wrapf(err, "failed to utime %s", name) + } + return nil +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/copy_windows.go b/vendor/github.com/tonistiigi/fsutil/copy/copy_windows.go new file mode 100644 index 00000000..a049565e --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/copy_windows.go @@ -0,0 +1,128 @@ +package fs + +import ( + "io" + "os" + + "github.com/Microsoft/go-winio" + "github.com/pkg/errors" + "golang.org/x/sys/windows" +) + +const ( + seTakeOwnershipPrivilege = "SeTakeOwnershipPrivilege" +) + +func getUIDGID(fi os.FileInfo) (uid, gid int) { + return 0, 0 +} + +func getFileSecurityInfo(name string) (*windows.SID, *windows.ACL, error) { + secInfo, err := windows.GetNamedSecurityInfo( + name, windows.SE_FILE_OBJECT, + windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION) + + if err != nil { + return nil, nil, errors.Wrap(err, "fetching security info") + } + sid, _, err := secInfo.Owner() + if err != nil { + return nil, nil, errors.Wrap(err, "fetching owner SID") + } + dacl, _, err := secInfo.DACL() + if err != nil { + return nil, nil, errors.Wrap(err, "fetching dacl") + } + return sid, dacl, nil +} + +func (c *copier) copyFileInfo(fi os.FileInfo, src, name string) error { + if c.modeSet != nil { + return errors.Errorf("non-octal mode not supported on windows") + } + + if err := os.Chmod(name, fi.Mode()); err != nil { + return errors.Wrapf(err, "failed to chmod %s", name) + } + + sid, dacl, err := getFileSecurityInfo(src) + if err != nil { + return errors.Wrap(err, "getting file info") + } + + if c.chown != nil { + // Use the defined chowner. + usr := &User{SID: sid.String()} + if err := Chown(name, usr, c.chown); err != nil { + return errors.Wrapf(err, "failed to chown %s", name) + } + return nil + } else { + // Copy file ownership and ACL from the source file. + // We need SeRestorePrivilege and SeTakeOwnershipPrivilege in order + // to restore security info on a file, especially if we're trying to + // apply security info which includes SIDs not necessarily present on + // the host. + privileges := []string{winio.SeRestorePrivilege, seTakeOwnershipPrivilege} + if err := winio.EnableProcessPrivileges(privileges); err != nil { + return err + } + defer winio.DisableProcessPrivileges(privileges) + + if err := windows.SetNamedSecurityInfo( + name, windows.SE_FILE_OBJECT, + windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION, + sid, nil, dacl, nil); err != nil { + + return err + } + } + + if err := c.copyFileTimestamp(fi, name); err != nil { + return err + } + return nil +} + +func (c *copier) copyFileTimestamp(fi os.FileInfo, name string) error { + if c.utime != nil { + return Utimes(name, c.utime) + } + + if fi.Mode()&os.ModeSymlink == 0 { + if err := os.Chtimes(name, fi.ModTime(), fi.ModTime()); err != nil { + return errors.Wrap(err, "changing mtime") + } + } + return nil +} + +func copyFile(source, target string) error { + src, err := os.Open(source) + if err != nil { + return errors.Wrapf(err, "failed to open source %s", source) + } + defer src.Close() + tgt, err := os.Create(target) + if err != nil { + return errors.Wrapf(err, "failed to open target %s", target) + } + defer tgt.Close() + + return copyFileContent(tgt, src) +} + +func copyFileContent(dst, src *os.File) error { + buf := bufferPool.Get().(*[]byte) + _, err := io.CopyBuffer(dst, src, *buf) + bufferPool.Put(buf) + return err +} + +func copyXAttrs(dst, src string, xeh XAttrErrorHandler) error { + return nil +} + +func copyDevice(dst string, fi os.FileInfo) error { + return errors.New("device copy not supported") +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/hardlink.go b/vendor/github.com/tonistiigi/fsutil/copy/hardlink.go new file mode 100644 index 00000000..38da9381 --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/hardlink.go @@ -0,0 +1,27 @@ +package fs + +import "os" + +// GetLinkInfo returns an identifier representing the node a hardlink is pointing +// to. If the file is not hard linked then 0 will be returned. +func GetLinkInfo(fi os.FileInfo) (uint64, bool) { + return getLinkInfo(fi) +} + +// getLinkSource returns a path for the given name and +// file info to its link source in the provided inode +// map. If the given file name is not in the map and +// has other links, it is added to the inode map +// to be a source for other link locations. +func getLinkSource(name string, fi os.FileInfo, inodes map[uint64]string) (string, error) { + inode, isHardlink := getLinkInfo(fi) + if !isHardlink { + return "", nil + } + + path, ok := inodes[inode] + if !ok { + inodes[inode] = name + } + return path, nil +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/hardlink_unix.go b/vendor/github.com/tonistiigi/fsutil/copy/hardlink_unix.go new file mode 100644 index 00000000..a02c5a58 --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/hardlink_unix.go @@ -0,0 +1,18 @@ +//go:build !windows +// +build !windows + +package fs + +import ( + "os" + "syscall" +) + +func getLinkInfo(fi os.FileInfo) (uint64, bool) { + s, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return 0, false + } + + return uint64(s.Ino), !fi.IsDir() && s.Nlink > 1 +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/hardlink_windows.go b/vendor/github.com/tonistiigi/fsutil/copy/hardlink_windows.go new file mode 100644 index 00000000..ad8845a7 --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/hardlink_windows.go @@ -0,0 +1,7 @@ +package fs + +import "os" + +func getLinkInfo(fi os.FileInfo) (uint64, bool) { + return 0, false +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/mkdir.go b/vendor/github.com/tonistiigi/fsutil/copy/mkdir.go new file mode 100644 index 00000000..9553c08b --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/mkdir.go @@ -0,0 +1,65 @@ +package fs + +import ( + "os" + "syscall" + "time" +) + +// MkdirAll is forked os.MkdirAll +func MkdirAll(path string, perm os.FileMode, user Chowner, tm *time.Time) error { + // Fast path: if we can tell whether path is a directory or file, stop with success or error. + dir, err := os.Stat(path) + if err == nil { + if dir.IsDir() { + return nil + } + return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR} + } + + // Slow path: make sure parent exists and then call Mkdir for path. + i := len(path) + for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator. + i-- + } + + j := i + for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element. + j-- + } + + if j > 1 { + // Create parent. + err = MkdirAll(fixRootDirectory(path[:j-1]), perm, user, tm) + if err != nil { + return err + } + } + + dir, err1 := os.Lstat(path) + if err1 == nil && dir.IsDir() { + return nil + } + + // Parent now exists; invoke Mkdir and use its result. + err = os.Mkdir(path, perm) + if err != nil { + // Handle arguments like "foo/." by + // double-checking that directory doesn't exist. + dir, err1 := os.Lstat(path) + if err1 == nil && dir.IsDir() { + return nil + } + return err + } + + if err := Chown(path, nil, user); err != nil { + return err + } + + if err := Utimes(path, tm); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/mkdir_unix.go b/vendor/github.com/tonistiigi/fsutil/copy/mkdir_unix.go new file mode 100644 index 00000000..8bc5711b --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/mkdir_unix.go @@ -0,0 +1,50 @@ +//go:build !windows +// +build !windows + +package fs + +import ( + "os" + "time" + + "github.com/pkg/errors" + "golang.org/x/sys/unix" +) + +func fixRootDirectory(p string) string { + return p +} + +func Utimes(p string, tm *time.Time) error { + if tm == nil { + return nil + } + + ts, err := unix.TimeToTimespec(*tm) + if err != nil { + return err + } + + timespec := []unix.Timespec{ts, ts} + if err := unix.UtimesNanoAt(unix.AT_FDCWD, p, timespec, unix.AT_SYMLINK_NOFOLLOW); err != nil { + return errors.Wrapf(err, "failed to utime %s", p) + } + + return nil +} + +func Chown(p string, old *User, fn Chowner) error { + if fn == nil { + return nil + } + user, err := fn(old) + if err != nil { + return errors.WithStack(err) + } + if user != nil { + if err := os.Lchown(p, user.UID, user.GID); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/mkdir_windows.go b/vendor/github.com/tonistiigi/fsutil/copy/mkdir_windows.go new file mode 100644 index 00000000..d8dddae9 --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/mkdir_windows.go @@ -0,0 +1,103 @@ +//go:build windows +// +build windows + +package fs + +import ( + "fmt" + "os" + "syscall" + "time" + + "github.com/Microsoft/go-winio" + "github.com/pkg/errors" + "golang.org/x/sys/windows" +) + +const ( + containerAdministratorSidString = "S-1-5-93-2-1" +) + +func fixRootDirectory(p string) string { + if len(p) == len(`\\?\c:`) { + if os.IsPathSeparator(p[0]) && os.IsPathSeparator(p[1]) && p[2] == '?' && os.IsPathSeparator(p[3]) && p[5] == ':' { + return p + `\` + } + } + return p +} + +func Utimes(p string, tm *time.Time) error { + info, err := os.Lstat(p) + if err != nil { + return errors.Wrap(err, "fetching file info") + } + if tm != nil && info.Mode()&os.ModeSymlink == 0 { + if err := os.Chtimes(p, *tm, *tm); err != nil { + return errors.Wrap(err, "changing times") + } + } + return nil +} + +func Chown(p string, old *User, fn Chowner) error { + if fn == nil { + return nil + } + user, err := fn(old) + if err != nil { + return errors.WithStack(err) + } + var userSIDstring string + if user != nil && user.SID != "" { + userSIDstring = user.SID + } + if userSIDstring == "" { + userSIDstring = containerAdministratorSidString + + } + + sidPtr, err := syscall.UTF16PtrFromString(userSIDstring) + if err != nil { + return errors.Wrap(err, "converting to utf16 ptr") + } + var userSID *windows.SID + if err := windows.ConvertStringSidToSid(sidPtr, &userSID); err != nil { + return errors.Wrap(err, "converting to windows SID") + } + var dacl *windows.ACL + newEntries := []windows.EXPLICIT_ACCESS{ + { + AccessPermissions: windows.GENERIC_ALL, + AccessMode: windows.GRANT_ACCESS, + Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT, + Trustee: windows.TRUSTEE{ + TrusteeForm: windows.TRUSTEE_IS_SID, + TrusteeValue: windows.TrusteeValueFromSID(userSID), + }, + }, + } + newAcl, err := windows.ACLFromEntries(newEntries, dacl) + if err != nil { + return fmt.Errorf("adding acls: %w", err) + } + + // Copy file ownership and ACL + // We need SeRestorePrivilege and SeTakeOwnershipPrivilege in order + // to restore security info on a file, especially if we're trying to + // apply security info which includes SIDs not necessarily present on + // the host. + privileges := []string{winio.SeRestorePrivilege, seTakeOwnershipPrivilege} + err = winio.RunWithPrivileges(privileges, func() error { + if err := windows.SetNamedSecurityInfo( + p, windows.SE_FILE_OBJECT, + windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION, + userSID, nil, newAcl, nil); err != nil { + + return err + } + return nil + }) + + return err +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/stat_bsd.go b/vendor/github.com/tonistiigi/fsutil/copy/stat_bsd.go new file mode 100644 index 00000000..362142de --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/stat_bsd.go @@ -0,0 +1,17 @@ +// +build darwin freebsd netbsd openbsd + +package fs + +import ( + "syscall" +) + +// Returns the last-accessed time +func StatAtime(st *syscall.Stat_t) syscall.Timespec { + return st.Atimespec +} + +// Returns the last-modified time +func StatMtime(st *syscall.Stat_t) syscall.Timespec { + return st.Mtimespec +} diff --git a/vendor/github.com/tonistiigi/fsutil/copy/stat_sysv.go b/vendor/github.com/tonistiigi/fsutil/copy/stat_sysv.go new file mode 100644 index 00000000..31ea3d94 --- /dev/null +++ b/vendor/github.com/tonistiigi/fsutil/copy/stat_sysv.go @@ -0,0 +1,18 @@ +//go:build dragonfly || linux || solaris +// +build dragonfly linux solaris + +package fs + +import ( + "syscall" +) + +// Returns the last-accessed time +func StatAtime(st *syscall.Stat_t) syscall.Timespec { + return st.Atim +} + +// Returns the last-modified time +func StatMtime(st *syscall.Stat_t) syscall.Timespec { + return st.Mtim +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 3ee0f5ed..8e9b076a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -710,9 +710,13 @@ github.com/theupdateframework/notary/tuf/data github.com/theupdateframework/notary/tuf/signed github.com/theupdateframework/notary/tuf/utils github.com/theupdateframework/notary/tuf/validation +# github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 +## explicit; go 1.21 +github.com/tonistiigi/dchapes-mode # github.com/tonistiigi/fsutil v0.0.0-20241003195857-3f140a1299b0 ## explicit; go 1.21 github.com/tonistiigi/fsutil +github.com/tonistiigi/fsutil/copy github.com/tonistiigi/fsutil/types # github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 ## explicit; go 1.16