copier.Stat(): return owner UID and GID if available

Return owner information for items that we've stat'ed.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
This commit is contained in:
Nalin Dahyabhai 2025-08-08 14:55:32 -04:00 committed by openshift-cherrypick-robot
parent f42946075a
commit 4fe68bc9b3
4 changed files with 41 additions and 12 deletions

View File

@ -201,7 +201,7 @@ func (req *request) UIDMap() []idtools.IDMap {
case requestEval:
return nil
case requestStat:
return nil
return req.StatOptions.UIDMap
case requestGet:
return req.GetOptions.UIDMap
case requestPut:
@ -226,7 +226,7 @@ func (req *request) GIDMap() []idtools.IDMap {
case requestEval:
return nil
case requestStat:
return nil
return req.StatOptions.GIDMap
case requestGet:
return req.GetOptions.GIDMap
case requestPut:
@ -284,6 +284,7 @@ type StatForItem struct {
Size int64 // dereferenced value for symlinks
Mode os.FileMode // dereferenced value for symlinks
ModTime time.Time // dereferenced value for symlinks
UID, GID int64 // usually in the uint32 range, set to -1 if unknown
IsSymlink bool
IsDir bool // dereferenced value for symlinks
IsRegular bool // dereferenced value for symlinks
@ -342,8 +343,9 @@ func Eval(root string, directory string, _ EvalOptions) (string, error) {
// StatOptions controls parts of Stat()'s behavior.
type StatOptions struct {
CheckForArchives bool // check for and populate the IsArchive bit in returned values
Excludes []string // contents to pretend don't exist, using the OS-specific path separator
UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs when returning results
CheckForArchives bool // check for and populate the IsArchive bit in returned values
Excludes []string // contents to pretend don't exist, using the OS-specific path separator
}
// Stat globs the specified pattern in the specified directory and returns its
@ -975,7 +977,7 @@ func copierHandler(bulkReader io.Reader, bulkWriter io.Writer, req request) (*re
resp := copierHandlerEval(req)
return resp, nil, nil
case requestStat:
resp := copierHandlerStat(req, pm)
resp := copierHandlerStat(req, pm, idMappings)
return resp, nil, nil
case requestGet:
return copierHandlerGet(bulkWriter, req, pm, idMappings)
@ -1102,7 +1104,7 @@ func copierHandlerEval(req request) *response {
return &response{Eval: evalResponse{Evaluated: filepath.Join(req.rootPrefix, resolvedTarget)}}
}
func copierHandlerStat(req request, pm *fileutils.PatternMatcher) *response {
func copierHandlerStat(req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) *response {
errorResponse := func(fmtspec string, args ...any) *response {
return &response{Error: fmt.Sprintf(fmtspec, args...), Stat: statResponse{}}
}
@ -1160,6 +1162,17 @@ func copierHandlerStat(req request, pm *fileutils.PatternMatcher) *response {
}
result.Size = linfo.Size()
result.Mode = linfo.Mode()
result.UID, result.GID = -1, -1
if uid, gid, err := owner(linfo); err == nil {
if idMappings != nil && !idMappings.Empty() {
hostPair := idtools.IDPair{UID: uid, GID: gid}
uid, gid, err = idMappings.ToContainer(hostPair)
if err != nil {
return errorResponse("copier: stat: mapping host filesystem owners %#v to container filesystem owners: %w", hostPair, err)
}
}
result.UID, result.GID = int64(uid), int64(gid)
}
result.ModTime = linfo.ModTime()
result.IsDir = linfo.IsDir()
result.IsRegular = result.Mode.IsRegular()
@ -1272,7 +1285,7 @@ func checkLinks(item string, req request, info os.FileInfo) (string, os.FileInfo
func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMatcher, idMappings *idtools.IDMappings) (*response, func() error, error) {
statRequest := req
statRequest.Request = requestStat
statResponse := copierHandlerStat(req, pm)
statResponse := copierHandlerStat(req, pm, idMappings)
errorResponse := func(fmtspec string, args ...any) (*response, func() error, error) {
return &response{Error: fmt.Sprintf(fmtspec, args...), Stat: statResponse.Stat, Get: getResponse{}}, nil, nil
}
@ -2270,7 +2283,7 @@ type EnsurePath struct {
// EnsureOptions controls parts of Ensure()'s behavior.
type EnsureOptions struct {
UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs in the chroot
UIDMap, GIDMap []idtools.IDMap // map from containerIDs to hostIDs in the chroot
Paths []EnsurePath
}
@ -2437,7 +2450,7 @@ type ConditionalRemovePath struct {
// ConditionalRemoveOptions controls parts of ConditionalRemove()'s behavior.
type ConditionalRemoveOptions struct {
UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs in the chroot
UIDMap, GIDMap []idtools.IDMap // map from containerIDs to hostIDs in the chroot
Paths []ConditionalRemovePath
}

View File

@ -716,6 +716,7 @@ func testStat(t *testing.T) {
if actualContent, ok := testArchive.contents[testItem.Name]; ok {
testItem.Size = int64(len(actualContent))
}
checkStatInfoOwnership(t, result)
require.Equal(t, testItem.Size, result.Size, "unexpected size difference for %q", name)
require.True(t, result.IsRegular, "expected %q.IsRegular to be true", glob)
require.False(t, result.IsDir, "expected %q.IsDir to be false", glob)

View File

@ -5,6 +5,8 @@ package copier
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
const (
@ -101,3 +103,9 @@ func TestConditionalRemoveChroot(t *testing.T) {
testConditionalRemove(t)
canChroot = couldChroot
}
func checkStatInfoOwnership(t *testing.T, result *StatForItem) {
t.Helper()
require.EqualValues(t, 0, result.UID, "expected the owning user to be reported")
require.EqualValues(t, 0, result.GID, "expected the owning group to be reported")
}

View File

@ -2,7 +2,14 @@
package copier
const (
testModeMask = int64(0o600)
testIgnoreSymlinkDates = true
import (
"testing"
"github.com/stretchr/testify/require"
)
func checkStatInfoOwnership(t *testing.T, result *StatForItem) {
t.Helper()
require.EqualValues(t, -1, result.UID, "expected the owning user to not be supported")
require.EqualValues(t, -1, result.GID, "expected the owning group to not be supported")
}