copier.Ensure(): also return parent directories

Have Ensure() also return the parent directories of items that it
created, along with information about them that can be used to filter
them out of the layer at commit-time.

This modifies the signature of Ensure(), but it was added in 1.41.0, and
shouldn't (yet) have any external users.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
This commit is contained in:
Nalin Dahyabhai 2025-07-23 18:13:10 -04:00 committed by openshift-cherrypick-robot
parent 19041cde31
commit 4f2feb8f47
3 changed files with 127 additions and 17 deletions

View File

@ -305,7 +305,8 @@ type removeResponse struct{}
// ensureResponse encodes a response to an Ensure request.
type ensureResponse struct {
Created []string // paths that were created because they weren't already present
Created []string // paths that were created because they weren't already present
Noted []EnsureParentPath // preexisting paths that are parents of created items
}
// conditionalRemoveResponse encodes a response to a conditionalRemove request.
@ -2269,12 +2270,22 @@ type EnsureOptions struct {
Paths []EnsurePath
}
// EnsureParentPath is a parent (or grandparent, or...) directory of an item
// created by Ensure(), along with information about it, from before the item
// in question was created. If the information about this directory hasn't
// changed when commit-time rolls around, it's most likely that this directory
// is only being considered for inclusion in the layer because it was pulled
// up, and it was not actually changed.
type EnsureParentPath = ConditionalRemovePath
// Ensure ensures that the specified mount point targets exist under the root.
// If the root directory is not specified, the current root directory is used.
// If root is specified and the current OS supports it, and the calling process
// has the necessary privileges, the operation is performed in a chrooted
// context.
func Ensure(root, directory string, options EnsureOptions) ([]string, error) {
// Returns a slice with the pathnames of items that needed to be created and a
// slice of affected parent directories and information about them.
func Ensure(root, directory string, options EnsureOptions) ([]string, []EnsureParentPath, error) {
req := request{
Request: requestEnsure,
Root: root,
@ -2283,12 +2294,12 @@ func Ensure(root, directory string, options EnsureOptions) ([]string, error) {
}
resp, err := copier(nil, nil, req)
if err != nil {
return nil, err
return nil, nil, err
}
if resp.Error != "" {
return nil, errors.New(resp.Error)
return nil, nil, errors.New(resp.Error)
}
return resp.Ensure.Created, nil
return resp.Ensure.Created, resp.Ensure.Noted, nil
}
func copierHandlerEnsure(req request, idMappings *idtools.IDMappings) *response {
@ -2297,6 +2308,7 @@ func copierHandlerEnsure(req request, idMappings *idtools.IDMappings) *response
}
slices.SortFunc(req.EnsureOptions.Paths, func(a, b EnsurePath) int { return strings.Compare(a.Path, b.Path) })
var created []string
notedByName := map[string]EnsureParentPath{}
for _, item := range req.EnsureOptions.Paths {
uid, gid := 0, 0
if item.Chown != nil {
@ -2340,11 +2352,25 @@ func copierHandlerEnsure(req request, idMappings *idtools.IDMappings) *response
if parentPath == "" {
parentPath = "."
}
leaf := filepath.Join(subdir, component)
leaf := filepath.Join(parentPath, component)
parentInfo, err := os.Stat(filepath.Join(req.Root, parentPath))
if err != nil {
return errorResponse("copier: ensure: checking datestamps on %q (%d: %v): %v", parentPath, i, components, err)
}
if parentPath != "." {
parentModTime := parentInfo.ModTime().UTC()
parentMode := parentInfo.Mode()
uid, gid, err := owner(parentInfo)
if err != nil {
return errorResponse("copier: ensure: error reading owner of %q: %v", parentPath, err)
}
notedByName[parentPath] = EnsureParentPath{
Path: parentPath,
ModTime: &parentModTime,
Mode: &parentMode,
Owner: &idtools.IDPair{UID: uid, GID: gid},
}
}
if i < len(components)-1 || item.Typeflag == tar.TypeDir {
err = os.Mkdir(filepath.Join(req.Root, leaf), mode)
subdir = leaf
@ -2386,7 +2412,15 @@ func copierHandlerEnsure(req request, idMappings *idtools.IDMappings) *response
}
}
slices.Sort(created)
return &response{Error: "", Ensure: ensureResponse{Created: created}}
noted := make([]EnsureParentPath, 0, len(notedByName))
for _, n := range notedByName {
if slices.Contains(created, n.Path) {
continue
}
noted = append(noted, n)
}
slices.SortFunc(noted, func(a, b EnsureParentPath) int { return strings.Compare(a.Path, b.Path) })
return &response{Error: "", Ensure: ensureResponse{Created: created, Noted: noted}}
}
// ConditionalRemovePath is a single item being passed to an ConditionalRemove() call.

View File

@ -2045,12 +2045,15 @@ func TestExtendedGlob(t *testing.T) {
func testEnsure(t *testing.T) {
zero := time.Unix(0, 0)
worldReadable := os.FileMode(0o644)
ugReadable := os.FileMode(0o750)
testCases := []struct {
description string
subdir string
options EnsureOptions
expected []string
description string
subdir string
mkdirs []string
options EnsureOptions
expectCreated []string
expectNoted []EnsureParentPath
}{
{
description: "base",
@ -2078,7 +2081,7 @@ func testEnsure(t *testing.T) {
},
},
},
expected: []string{
expectCreated: []string{
"subdir",
"subdir/a",
"subdir/a/b",
@ -2087,6 +2090,7 @@ func testEnsure(t *testing.T) {
"subdir/a/b/c",
"subdir/a/b/d",
},
expectNoted: []EnsureParentPath{},
},
{
description: "nosubdir",
@ -2103,21 +2107,93 @@ func testEnsure(t *testing.T) {
},
},
},
expected: []string{
expectCreated: []string{
"a",
"a/b",
"a/b/c",
"a/b/d",
},
expectNoted: []EnsureParentPath{},
},
{
description: "mkdir-first",
subdir: "dir/subdir",
mkdirs: []string{"dir", "dir/subdir"},
options: EnsureOptions{
Paths: []EnsurePath{
{
Path: filepath.Join(string(os.PathSeparator), "a", "b", "a"),
Typeflag: tar.TypeReg,
Chmod: &worldReadable,
},
{
Path: filepath.Join("a", "b", "b"),
Typeflag: tar.TypeReg,
ModTime: &zero,
},
{
Path: filepath.Join(string(os.PathSeparator), "a", "b", "c"),
Typeflag: tar.TypeDir,
ModTime: &zero,
},
{
Path: filepath.Join("a", "b", "d"),
Typeflag: tar.TypeDir,
},
},
},
expectCreated: []string{
"dir/subdir/a",
"dir/subdir/a/b",
"dir/subdir/a/b/a",
"dir/subdir/a/b/b",
"dir/subdir/a/b/c",
"dir/subdir/a/b/d",
},
expectNoted: []EnsureParentPath{
{
Path: "dir",
Mode: &ugReadable,
Owner: &idtools.IDPair{UID: 1, GID: 1},
// ModTime gets updated when we create dir/subdir, can't check it
},
{
Path: "dir/subdir",
Mode: &ugReadable,
Owner: &idtools.IDPair{UID: 1, GID: 1},
ModTime: &zero,
},
},
},
}
for i := range testCases {
t.Run(testCases[i].description, func(t *testing.T) {
testStarted := time.Now()
tmpdir := t.TempDir()
created, err := Ensure(tmpdir, testCases[i].subdir, testCases[i].options)
for _, mkdir := range testCases[i].mkdirs {
err := Mkdir(tmpdir, mkdir, MkdirOptions{
ModTimeNew: &zero,
ChmodNew: &ugReadable,
ChownNew: &idtools.IDPair{UID: 1, GID: 1},
})
require.NoError(t, err, "unexpected error ensuring")
}
created, noted, err := Ensure(tmpdir, testCases[i].subdir, testCases[i].options)
require.NoError(t, err, "unexpected error ensuring")
require.EqualValues(t, testCases[i].expected, created, "did not expect these")
require.EqualValues(t, testCases[i].expectCreated, created, "did not expect to create these")
require.Equal(t, len(testCases[i].expectNoted), len(noted), "noticed the wrong number of things")
for n := range noted {
require.Equalf(t, testCases[i].expectNoted[n].Path, noted[n].Path, "noticed item %d path", n)
if testCases[i].expectNoted[n].Mode != nil {
require.Equalf(t, testCases[i].expectNoted[n].Mode.Perm(), noted[n].Mode.Perm(), "noticed item %q mode", noted[n].Path)
}
if testCases[i].expectNoted[n].Owner != nil {
require.Equalf(t, *testCases[i].expectNoted[n].Owner, *noted[n].Owner, "noticed item %q owner", noted[n].Path)
}
if testCases[i].expectNoted[n].ModTime != nil {
require.Equalf(t, testCases[i].expectNoted[n].ModTime.UnixNano(), noted[n].ModTime.UnixNano(), "noticed item %q mtime", noted[n].Path)
}
}
for _, item := range testCases[i].options.Paths {
target := filepath.Join(tmpdir, testCases[i].subdir, item.Path)
st, err := os.Stat(target)
@ -2298,7 +2374,7 @@ func testConditionalRemove(t *testing.T) {
Chmod: what.mode,
})
}
created, err := Ensure(tmpdir, testCases[i].subdir, create)
created, _, err := Ensure(tmpdir, testCases[i].subdir, create)
require.NoErrorf(t, err, "unexpected error creating %#v", create)
remove := testCases[i].remove
for _, what := range created {

View File

@ -2119,7 +2119,7 @@ func (b *Builder) createMountTargets(spec *specs.Spec) ([]copier.ConditionalRemo
if len(targets.Paths) == 0 {
return nil, nil
}
created, err := copier.Ensure(rootfsPath, rootfsPath, targets)
created, _, err := copier.Ensure(rootfsPath, rootfsPath, targets)
if err != nil {
return nil, err
}