Refactor and fix bugs in syncuids
Closes #840 * SyncUids can now return applicable errors even during showOnly, so updated ContainerImport to ignore errors during showOnly. * Fixed handling of file gids during SyncUids Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
committed by
Jonathon Anderson
parent
7093d722a5
commit
7b2f278f94
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fix build configuration on Quickstart guide #847
|
||||
- Add Quickstart guide for EL9
|
||||
- Add EL9 Quickstart guide to index.rst
|
||||
- Container file gids are now updated properly during syncuser. #840
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -203,11 +203,14 @@ func ContainerImport(cip *wwapiv1.ContainerImportParameter) (containerName strin
|
||||
return
|
||||
}
|
||||
|
||||
err = container.SyncUids(cip.Name, !cip.SyncUser)
|
||||
if err != nil && !cip.SyncUser {
|
||||
SyncUserShowOnly := !cip.SyncUser
|
||||
err = container.SyncUids(cip.Name, SyncUserShowOnly)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error in user sync, fix error and run 'syncuser' manually: %s", err)
|
||||
wwlog.Error(err.Error())
|
||||
return
|
||||
if cip.SyncUser {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
wwlog.Info("Building container: %s", cip.Name)
|
||||
|
||||
@@ -16,243 +16,402 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type completeUserInfo struct {
|
||||
Name string
|
||||
UidHost int `access:"r,w"`
|
||||
GidHost int `access:"r,w"`
|
||||
UidCont int `access:"r,w"`
|
||||
GidCont int `access:"r,w"`
|
||||
FileListUid []string `access:"r,w"`
|
||||
FileListGid []string `access:"r,w"`
|
||||
}
|
||||
|
||||
type simpleUserInfo struct {
|
||||
name string
|
||||
uid int
|
||||
gid int
|
||||
}
|
||||
const passwdPath = "/etc/passwd"
|
||||
const groupPath = "/etc/group"
|
||||
|
||||
/*
|
||||
sync the uids,gids from the host to the container
|
||||
*/
|
||||
|
||||
// SyncUids updates the /etc/passwd and /etc/group files in the
|
||||
// container identified by containerName by installing the equivalent
|
||||
// files from the host and appending names only in the
|
||||
// container. Files in the container are updated to match the new
|
||||
// numeric id assignments.
|
||||
//
|
||||
// If showOnly is true, the container is not actually updated, but
|
||||
// relevant log entries and output are generated.
|
||||
//
|
||||
// Any I/O errors are returned unmodified.
|
||||
//
|
||||
// A conflict arises if the container has an entry with the same id as
|
||||
// an entry in the host and the host does not have an entry with the
|
||||
// same name. In this case, an error is returned.
|
||||
func SyncUids(containerName string, showOnly bool) error {
|
||||
var userDb []completeUserInfo
|
||||
passwdName := "/etc/passwd"
|
||||
groupName := "/etc/group"
|
||||
containerPath := RootFsDir(containerName)
|
||||
containerPasswdPath := path.Join(containerPath, passwdPath)
|
||||
containerGroupPath := path.Join(containerPath, groupPath)
|
||||
|
||||
// populate db with users from the host
|
||||
hostUsers, err := createPasswdMap(passwdName)
|
||||
if err != nil {
|
||||
wwlog.Error("Could not open "+passwdName)
|
||||
return err
|
||||
}
|
||||
for _, hostUser := range hostUsers {
|
||||
userDb = append(userDb, completeUserInfo{Name: hostUser.name,
|
||||
UidHost: hostUser.uid, GidHost: hostUser.gid, UidCont: -1, GidCont: -1})
|
||||
}
|
||||
passwdSync := make(syncDB)
|
||||
if err := passwdSync.readFromHost(passwdPath); err != nil { return err }
|
||||
if err := passwdSync.readFromContainer(containerPasswdPath); err != nil { return err }
|
||||
if err := passwdSync.checkConflicts(); err != nil { return err }
|
||||
if err := passwdSync.findUserFiles(containerPath); err != nil { return err }
|
||||
|
||||
// merge container users into db and track users that are only in the
|
||||
// container
|
||||
fullPath := RootFsDir(containerName)
|
||||
containerUsers, err := createPasswdMap(path.Join(fullPath, passwdName))
|
||||
if err != nil {
|
||||
wwlog.Error("Could not open "+path.Join(fullPath, passwdName))
|
||||
return err
|
||||
}
|
||||
var userOnlyCont []string
|
||||
for _, containerUser := range containerUsers {
|
||||
foundUser := false
|
||||
for idxHost, user := range userDb {
|
||||
if containerUser.name == user.Name {
|
||||
foundUser = true
|
||||
(&userDb[idxHost]).UidCont = containerUser.uid
|
||||
(&userDb[idxHost]).GidCont = containerUser.gid
|
||||
}
|
||||
}
|
||||
if !foundUser {
|
||||
userDb = append(userDb, completeUserInfo{Name: containerUser.name,
|
||||
UidHost: -1, GidHost: -1, UidCont: containerUser.uid, GidCont: containerUser.gid})
|
||||
wwlog.Warn("user: %s:%v:%v not present on host", containerUser.name, containerUser.uid, containerUser.gid)
|
||||
userOnlyCont = append(userOnlyCont, containerUser.name)
|
||||
}
|
||||
}
|
||||
groupSync := make(syncDB)
|
||||
if err := groupSync.readFromHost(groupPath); err != nil { return err }
|
||||
if err := groupSync.readFromContainer(containerGroupPath); err != nil { return err }
|
||||
if err := groupSync.checkConflicts(); err != nil { return err }
|
||||
if err := groupSync.findGroupFiles(containerPath); err != nil { return err }
|
||||
|
||||
// detect users in the host and container with conflicting uids
|
||||
for _, containerUser := range userDb {
|
||||
if (containerUser.UidCont == -1 || containerUser.UidHost != -1) {
|
||||
// containerUser is either not actually in the
|
||||
// container or is also in the host
|
||||
continue
|
||||
}
|
||||
for _, hostUser := range userDb {
|
||||
if hostUser.UidHost == containerUser.UidCont {
|
||||
wwlog.Warn("uid(%v) collision for host: %s and container: %s",
|
||||
containerUser.UidCont, hostUser.Name, containerUser.Name)
|
||||
return errors.New(fmt.Sprintf("user %s only present in container has same uid(%v) as user %s on host,\n"+
|
||||
"add this user to /etc/passwd on host", containerUser.Name, containerUser.UidCont, hostUser.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
passwdSync.log("passwd")
|
||||
groupSync.log("group")
|
||||
|
||||
if showOnly {
|
||||
wwlog.Info("uid/gid not synced, run \nwwctl container syncuser --write %s\nto synchronize uid/gids.", containerName)
|
||||
return nil
|
||||
}
|
||||
// create list of files which need changed ownerships in order to
|
||||
// change them later what avoid uid/gid collisions
|
||||
for idx, user := range userDb {
|
||||
if (user.UidHost != user.UidCont && user.UidHost != -1) ||
|
||||
(user.GidHost != user.GidCont && user.GidHost != -1 && user.UidHost != -1) {
|
||||
wwlog.Verbose(fmt.Sprintf("host %s:%v:%v <-> container %s:%v:%v",
|
||||
user.Name, user.UidHost, user.GidHost, user.Name, user.UidCont, user.GidCont))
|
||||
err = filepath.Walk(fullPath, func(filePath string, info fs.FileInfo, err error) error {
|
||||
// root is always good, if we fail to get UID/GID of a file
|
||||
var uid, gid int
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
uid = int(stat.Uid)
|
||||
gid = int(stat.Gid)
|
||||
}
|
||||
if uid == user.UidCont {
|
||||
(&userDb[idx]).FileListUid = append((&userDb[idx]).FileListUid, filePath)
|
||||
}
|
||||
if gid == user.GidCont {
|
||||
(&userDb[idx]).FileListGid = append((&userDb[idx]).FileListGid, filePath)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if passwdSync.needsSync() || groupSync.needsSync() {
|
||||
wwlog.Info("uid/gid not synced: run `wwctl container syncuser --write %s`", containerName)
|
||||
} else {
|
||||
wwlog.Info("uid/gid already synced")
|
||||
}
|
||||
}
|
||||
// change uids and gid of file
|
||||
for _, user := range userDb {
|
||||
if len(user.FileListUid) != 0 {
|
||||
//fmt.Printf("uidList(%s): %v\n", user.Name, user.FileListUid)
|
||||
for _, file := range user.FileListUid {
|
||||
fsInfo, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var gid int
|
||||
if stat, ok := fsInfo.Sys().(*syscall.Stat_t); ok {
|
||||
gid = int(stat.Gid)
|
||||
}
|
||||
wwlog.Debug("%s chown(%v,%v)", file, user.UidHost, gid)
|
||||
err = os.Chown(file, user.UidHost, gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(user.FileListGid) != 0 {
|
||||
//fmt.Printf("gidList(%s): %v\n", user.Name, user.FileListGid)
|
||||
for _, file := range user.FileListGid {
|
||||
fsInfo, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var uid int
|
||||
if stat, ok := fsInfo.Sys().(*syscall.Stat_t); ok {
|
||||
uid = int(stat.Uid)
|
||||
}
|
||||
wwlog.Debug("%s chown(%v,%v)", file, user.UidHost, uid)
|
||||
// only chown files and dirs
|
||||
if fsInfo.IsDir() && fsInfo.Mode().IsRegular() {
|
||||
err = os.Chown(file, uid, user.GidHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := passwdSync.chownUserFiles(); err != nil { return err }
|
||||
if err := groupSync.chownGroupFiles(); err != nil { return err }
|
||||
|
||||
if err := passwdSync.update(containerPasswdPath, passwdPath); err != nil { return err }
|
||||
if err := groupSync.update(containerGroupPath, groupPath); err != nil { return err }
|
||||
wwlog.Info("uid/gid synced for container %s", containerName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// A syncDB maps user or group names to syncInfo instances, which
|
||||
// correlate IDs between host and container and track affected
|
||||
// files. This can be used for either /etc/passwd or /etc/group IDs.
|
||||
type syncDB map[string]syncInfo
|
||||
|
||||
|
||||
// checkConflicts inspects a syncDB map for irreconcilable
|
||||
// conflicts. A conflict arises if the container has an entry with the
|
||||
// same id as an entry in the host and the host does not have an entry
|
||||
// with the same name.
|
||||
func (db syncDB) checkConflicts() error {
|
||||
for nameInContainer, containerIds := range db {
|
||||
if (!containerIds.inContainer() || containerIds.inHost()) {
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
// get the entries for the passwd/group file before copy over
|
||||
passwdEntries, err := getEntires(path.Join(fullPath, passwdName), userOnlyCont)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// implicitly assuming that users/groups which only exists on the host have the same name
|
||||
groupEntries, err := getEntires(path.Join(fullPath, groupName), userOnlyCont)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Remove(path.Join(fullPath, passwdName)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Remove(path.Join(fullPath, groupName)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = util.CopyFile(passwdName, path.Join(fullPath, passwdName)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = util.CopyFile(groupName, path.Join(fullPath, groupName)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = util.AppendLines(path.Join(fullPath, passwdName), passwdEntries); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = util.AppendLines(path.Join(fullPath, groupName), groupEntries); err != nil {
|
||||
return err
|
||||
for nameInHost, hostIds := range db {
|
||||
if hostIds.HostID == containerIds.ContainerID {
|
||||
errorMsg := fmt.Sprintf("id(%v) collision: host(%s) container(%s)", containerIds.ContainerID, nameInHost, nameInContainer)
|
||||
wwlog.Error(errorMsg)
|
||||
wwlog.Error("add %s to host to resolve conflict", nameInContainer)
|
||||
return errors.New(errorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
creates simple user db []simpleUserInfo for a /etc/{passwd|group} file
|
||||
*/
|
||||
func createPasswdMap(fileName string) ([]simpleUserInfo, error) {
|
||||
var nameDb []simpleUserInfo
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
fileScanner := bufio.NewScanner(file)
|
||||
for fileScanner.Scan() {
|
||||
line := fileScanner.Text()
|
||||
entries := strings.Split(line, ":")
|
||||
name := entries[0]
|
||||
uid, err := strconv.Atoi(entries[2])
|
||||
if err != nil {
|
||||
wwlog.Warn("could not parse uid(%s) for %s", entries[2], name)
|
||||
}
|
||||
gid, err := strconv.Atoi(entries[3])
|
||||
if err != nil {
|
||||
wwlog.Warn("could not parse gid(%s) for %s", entries[2], name)
|
||||
}
|
||||
if name != "" {
|
||||
nameDb = append(nameDb, simpleUserInfo{name: name, uid: uid, gid: gid})
|
||||
|
||||
}
|
||||
// log generates debug and verbose logs inspecting a syncDB map.
|
||||
//
|
||||
// The provided prefix is prepended to log entries to provide context
|
||||
// for the given syncDB map. (e.g., to differentiate between a user or
|
||||
// group map)
|
||||
func (db syncDB) log(prefix string) {
|
||||
var onlyContainer, onlyHost, match, differ []string
|
||||
for name, ids := range db {
|
||||
wwlog.Debug("%s: %s: host(%v) container(%v) files: %v", prefix, name, ids.HostID, ids.ContainerID, ids.ContainerFiles)
|
||||
if ids.onlyContainer() { onlyContainer = append(onlyContainer, name) }
|
||||
if ids.onlyHost() { onlyHost = append(onlyHost, name) }
|
||||
if ids.match() { match = append(match, name) }
|
||||
if ids.differ() { differ = append(differ, name) }
|
||||
}
|
||||
wwlog.Debug(fmt.Sprintf("created uid/gid map with %v entries from %s", len(nameDb), fileName))
|
||||
return nameDb, nil
|
||||
|
||||
wwlog.Verbose("%s: container only: %v", prefix, onlyContainer)
|
||||
wwlog.Verbose("%s: host only: %v", prefix, onlyHost)
|
||||
wwlog.Verbose("%s: match: %v", prefix, match)
|
||||
wwlog.Verbose("%s: differ: %v", prefix, differ)
|
||||
}
|
||||
|
||||
/*
|
||||
Creates a slice with the entries of of passwd for the given slice of user names
|
||||
*/
|
||||
func getEntires(fileName string, names []string) ([]string, error) {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
// read reads fileName (an /etc/passwd or /etc/group file) into a
|
||||
// syncDB map. Since the name and numerical id for both files are
|
||||
// stored at fields 0 and 2, the same function can read both files.
|
||||
//
|
||||
// fromContainer identifies whether the given fileName includes
|
||||
// entries from the host or the container.
|
||||
func (db syncDB) read(fileName string, fromContainer bool) error {
|
||||
wwlog.Debug("read %s (container: %t)", fileName, fromContainer)
|
||||
if file, err := os.Open(fileName); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer file.Close()
|
||||
fileScanner := bufio.NewScanner(file)
|
||||
for fileScanner.Scan() {
|
||||
line := fileScanner.Text()
|
||||
fields := strings.Split(line, ":")
|
||||
|
||||
name := fields[0]
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(fields[2])
|
||||
if err != nil {
|
||||
wwlog.Warn("Ignoring line %s (parse error)", line)
|
||||
continue
|
||||
}
|
||||
|
||||
entry, ok := db[name]
|
||||
if !ok {
|
||||
entry = syncInfo{HostID: -1, ContainerID: -1}
|
||||
}
|
||||
|
||||
if fromContainer {
|
||||
if entry.ContainerID == -1 {
|
||||
entry.ContainerID = id
|
||||
} else if entry.ContainerID != id {
|
||||
wwlog.Warn("Ignoring container id(%v) for %s from %s after existing value %v", id, name, fileName, entry.ContainerID)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if entry.HostID == -1 {
|
||||
entry.HostID = id
|
||||
} else if entry.HostID != id {
|
||||
wwlog.Warn("Ignoring host id(%v) for %s from %s after existing value %v", id, name, fileName, entry.HostID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
db[name] = entry
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// readFromHost reads fileName into a syncDB map.
|
||||
//
|
||||
// Equivalent to read(fileName, false)
|
||||
func (db syncDB) readFromHost(fileName string) error { return db.read(fileName, false) }
|
||||
|
||||
|
||||
// readFromContainer reads fileName into a syncDB map.
|
||||
//
|
||||
// Equivalent to read(fileName, true)
|
||||
func (db syncDB) readFromContainer(fileName string) error { return db.read(fileName, true) }
|
||||
|
||||
|
||||
// findFiles finds files under containerPath that are owned by each
|
||||
// tracked container ID.
|
||||
//
|
||||
// If byGid is true, files with a matching gid are
|
||||
// recorded. Otherwise, files with a matching uid are recorded.
|
||||
func (db syncDB) findFiles(containerPath string, byGid bool) error {
|
||||
for name, ids := range db {
|
||||
if err := ids.findFiles(containerPath, byGid); err != nil { return err }
|
||||
if len(ids.ContainerFiles) > 0 {
|
||||
wwlog.Debug("files for %s (%v -> %v, gid: %v): %v", name, ids.ContainerID, ids.HostID, byGid, ids.ContainerFiles)
|
||||
}
|
||||
db[name] = ids
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// findUserFiles is equivalent to findFiles(containerPath, false)
|
||||
func (db syncDB) findUserFiles(containerPath string) error { return db.findFiles(containerPath, false) }
|
||||
|
||||
|
||||
// findGroupFiles is equivalent to findFiles(containerPath, true)
|
||||
func (db syncDB) findGroupFiles(containerPath string) error { return db.findFiles(containerPath, true) }
|
||||
|
||||
|
||||
// chownFiles updates found files (see findFiles) to reflect ownership
|
||||
// using host ids.
|
||||
//
|
||||
// If byGid is true, file gids are updated. Otherwise, file uids are
|
||||
// updated.
|
||||
func (db syncDB) chownFiles(byGid bool) error {
|
||||
for _, ids := range db {
|
||||
if err := ids.chownFiles(byGid); err != nil { return err }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// chownUserFiles is equivalent to chownFiles(false)
|
||||
func (db syncDB) chownUserFiles() error { return db.chownFiles(false) }
|
||||
|
||||
|
||||
// chownUserFiles is equivalent to chownFiles(true)
|
||||
func (db syncDB) chownGroupFiles() error { return db.chownFiles(true) }
|
||||
|
||||
|
||||
// getOnlyContainerLines returns the lines from fileName (either an
|
||||
// /etc/passwd or /etc/group file) for names that occur only in the
|
||||
// container.
|
||||
//
|
||||
// These lines are added to the respective file from the host when
|
||||
// updating the container.
|
||||
func (db syncDB) getOnlyContainerLines(fileName string) ([]string, error) {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil { return nil, err }
|
||||
defer file.Close()
|
||||
var list []string
|
||||
fileScanner := bufio.NewScanner(file)
|
||||
|
||||
var lines []string
|
||||
for fileScanner.Scan() {
|
||||
line := fileScanner.Text()
|
||||
entries := strings.Split(line, ":")
|
||||
for _, name := range names {
|
||||
if entries[0] == name {
|
||||
list = append(list, line)
|
||||
fields := strings.Split(line, ":")
|
||||
for name, ids := range db {
|
||||
if fields[0] == name {
|
||||
if ids.onlyContainer() {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
wwlog.Debug("file: %s, list: %v", fileName, list)
|
||||
return list, nil
|
||||
wwlog.Debug("file: %s, entries only in container: %v", fileName, lines)
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
|
||||
// update replaces containerPath with hostPath and adds lines from
|
||||
// getOnlyContainerLines to the end of containerPath. This is used to
|
||||
// replace /etc/passwd (or /etc/group) in the container with the
|
||||
// equivalent file from the host while retaining names unique to the
|
||||
// container.
|
||||
func (db syncDB) update(containerPath string, hostPath string) error {
|
||||
wwlog.Debug("update %s from %s)", containerPath, hostPath)
|
||||
if lines, err := db.getOnlyContainerLines(containerPath); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := os.Remove(containerPath); err != nil { return err }
|
||||
if err := util.CopyFile(hostPath, containerPath); err != nil { return err }
|
||||
if err := util.AppendLines(containerPath, lines); err != nil { return err }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// needsSync returns true if the syncDB map indicates that ids between
|
||||
// the container and host are out-of-sync.
|
||||
func (db syncDB) needsSync() bool {
|
||||
for name, ids := range db {
|
||||
if ids.onlyHost() {
|
||||
wwlog.Debug("sync required: %s only in host", name)
|
||||
return true
|
||||
}
|
||||
if ids.differ() {
|
||||
wwlog.Debug("sync required: %s is %v in host and %v in container", name, ids.HostID, ids.ContainerID)
|
||||
return true
|
||||
}
|
||||
if len(ids.ContainerFiles) > 0 {
|
||||
wwlog.Debug("sync required: %v files to update for %s", len(ids.ContainerFiles), name)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// sycncInfo correlates the numerical id of a name on the host
|
||||
// (HostID) and the container (ContainerID), along with the files in
|
||||
// the container that are owned by that name. This allows affected
|
||||
// files to be updated when the HostID is applied to the container.
|
||||
type syncInfo struct {
|
||||
HostID int `access:"r,w"`
|
||||
ContainerID int `access:"r,w"`
|
||||
ContainerFiles []string `access:"r,w"`
|
||||
}
|
||||
|
||||
|
||||
// inHost returns true if info has a record of an id for this name in
|
||||
// the host.
|
||||
func (info *syncInfo) inHost() bool {
|
||||
return info.HostID != -1
|
||||
}
|
||||
|
||||
|
||||
// inContainer returns true if info has a record of an id for this
|
||||
// name in the container.
|
||||
func (info *syncInfo) inContainer() bool {
|
||||
return info.ContainerID != -1
|
||||
}
|
||||
|
||||
|
||||
// onlyHost returns true if info has a record of an id for this name
|
||||
// in the host and not in the container.
|
||||
func (info *syncInfo) onlyHost() bool {
|
||||
return info.inHost() && !info.inContainer()
|
||||
}
|
||||
|
||||
|
||||
// onlyHost returns true if info has a record of an id for this name
|
||||
// in the container and not in the host.
|
||||
func (info *syncInfo) onlyContainer() bool {
|
||||
return info.inContainer() && !info.inHost()
|
||||
}
|
||||
|
||||
|
||||
// match returns true if info represents a name that exists with the
|
||||
// same numerical id in both the host and the container.
|
||||
func (info *syncInfo) match() bool {
|
||||
return info.inContainer() && info.inHost() && info.HostID == info.ContainerID
|
||||
}
|
||||
|
||||
|
||||
// differ returns true if info represents a name that exists in both
|
||||
// the host and the container but with different numerical ids.
|
||||
func (info *syncInfo) differ() bool {
|
||||
return info.inContainer() && info.inHost() && info.HostID != info.ContainerID
|
||||
}
|
||||
|
||||
|
||||
// findFiles walks containerPath to find files owned by the name
|
||||
// represented by info and records them in info.ContainerFiles.
|
||||
//
|
||||
// If byGid is true, the file's gid is checked; otherwise, the file's
|
||||
// uid is checked.
|
||||
func (info *syncInfo) findFiles(containerPath string, byGid bool) error {
|
||||
var containerFiles []string
|
||||
if (info.inHost() && !info.match()) {
|
||||
if err := filepath.Walk(containerPath, func(filePath string, fileInfo fs.FileInfo, err error) error {
|
||||
if stat, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
|
||||
var id int
|
||||
if byGid {
|
||||
id = int(stat.Gid)
|
||||
} else {
|
||||
id = int(stat.Uid)
|
||||
}
|
||||
if id == info.ContainerID {
|
||||
containerFiles = append(containerFiles, filePath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
info.ContainerFiles = containerFiles
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// chownFiles updates the files recorded in info.ContainerFiles to use
|
||||
// the numerical IDs from the host.
|
||||
//
|
||||
// If byGid is true, the file's gid is updated; otherwise, the file's
|
||||
// uid is updated.
|
||||
func (info *syncInfo) chownFiles(byGid bool) error {
|
||||
for _, file := range info.ContainerFiles {
|
||||
if fileInfo, err := os.Stat(file); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if fileInfo.IsDir() || fileInfo.Mode().IsRegular() {
|
||||
var newUid, newGid int
|
||||
if byGid {
|
||||
newUid = -1
|
||||
newGid = info.HostID
|
||||
} else {
|
||||
newUid = info.HostID
|
||||
newGid = -1
|
||||
}
|
||||
if err := os.Chown(file, newUid, newGid); err != nil { return err }
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
283
internal/pkg/container/syncuids_test.go
Normal file
283
internal/pkg/container/syncuids_test.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
func writeTempFile(t *testing.T, input string) (string) {
|
||||
tempFile, createTempError := os.CreateTemp("", "syncuids-*")
|
||||
assert.NoError(t, createTempError)
|
||||
_, writeError := tempFile.Write([]byte(input))
|
||||
assert.NoError(t, writeError)
|
||||
assert.NoError(t, tempFile.Sync())
|
||||
return tempFile.Name()
|
||||
}
|
||||
|
||||
|
||||
func makeSyncDB(t *testing.T, hostInput string, containerInput string) (syncDB) {
|
||||
hostFileName := writeTempFile(t, hostInput)
|
||||
defer os.Remove(hostFileName)
|
||||
containerFileName := writeTempFile(t, containerInput)
|
||||
defer os.Remove(containerFileName)
|
||||
db := make(syncDB)
|
||||
var err error
|
||||
err = db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
err = db.readFromContainer(containerFileName)
|
||||
assert.NoError(t, err)
|
||||
return db
|
||||
}
|
||||
|
||||
|
||||
func Test_readFromHost_single(t *testing.T) {
|
||||
hostFileName := writeTempFile(t, `testuser:x:1001:1001::/home/testuser:/bin/bash`)
|
||||
defer os.Remove(hostFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
err := db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, db, 1)
|
||||
assert.Equal(t, 1001, db["testuser"].HostID)
|
||||
assert.Equal(t, -1, db["testuser"].ContainerID)
|
||||
}
|
||||
|
||||
|
||||
func Test_readFromHost_multiple(t *testing.T) {
|
||||
hostFileName := writeTempFile(t, `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(hostFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
err := db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, db, 2)
|
||||
assert.Equal(t, 1001, db["testuser1"].HostID)
|
||||
assert.Equal(t, -1, db["testuser1"].ContainerID)
|
||||
assert.Equal(t, 1002, db["testuser2"].HostID)
|
||||
assert.Equal(t, -1, db["testuser2"].ContainerID)
|
||||
}
|
||||
|
||||
|
||||
func Test_readFromContainer_single(t *testing.T) {
|
||||
containerFileName := writeTempFile(t, `testuser:x:1001:1001::/home/testuser:/bin/bash`)
|
||||
defer os.Remove(containerFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
err := db.readFromContainer(containerFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, db, 1)
|
||||
assert.Equal(t, 1001, db["testuser"].ContainerID)
|
||||
assert.Equal(t, -1, db["testuser"].HostID)
|
||||
}
|
||||
|
||||
|
||||
func Test_readFromContainer_multiple(t *testing.T) {
|
||||
containerFileName := writeTempFile(t, `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(containerFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
err := db.readFromContainer(containerFileName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, db, 2)
|
||||
assert.Equal(t, 1001, db["testuser1"].ContainerID)
|
||||
assert.Equal(t, -1, db["testuser1"].HostID)
|
||||
assert.Equal(t, 1002, db["testuser2"].ContainerID)
|
||||
assert.Equal(t, -1, db["testuser2"].HostID)
|
||||
}
|
||||
|
||||
|
||||
func Test_readFromBoth_multiple(t *testing.T) {
|
||||
containerFileName := writeTempFile(t, `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(containerFileName)
|
||||
|
||||
|
||||
hostFileName := writeTempFile(t, `
|
||||
testuser1:x:2001:2001::/home/testuser:/bin/bash
|
||||
testuser3:x:2003:2003::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(hostFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
var err error
|
||||
err = db.readFromContainer(containerFileName)
|
||||
assert.NoError(t, err)
|
||||
err = db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, db, 3)
|
||||
assert.Equal(t, 1001, db["testuser1"].ContainerID)
|
||||
assert.Equal(t, 2001, db["testuser1"].HostID)
|
||||
assert.Equal(t, 1002, db["testuser2"].ContainerID)
|
||||
assert.Equal(t, -1, db["testuser2"].HostID)
|
||||
assert.Equal(t, -1, db["testuser3"].ContainerID)
|
||||
assert.Equal(t, 2003, db["testuser3"].HostID)
|
||||
}
|
||||
|
||||
|
||||
func Test_checkConflicts_empty(t *testing.T) {
|
||||
db := makeSyncDB(t, "", "")
|
||||
assert.NoError(t, db.checkConflicts())
|
||||
}
|
||||
|
||||
|
||||
func Test_checkConflicts_single(t *testing.T) {
|
||||
db := makeSyncDB(t, "", "testuser:x:1001:1001::/home/testuser:/bin/bash")
|
||||
assert.NoError(t, db.checkConflicts())
|
||||
}
|
||||
|
||||
|
||||
func Test_checkConflicts_match(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
"testuser:x:1001:1001::/home/testuser:/bin/bash",
|
||||
"testuser:x:1001:1001::/home/testuser:/bin/bash")
|
||||
assert.NoError(t, db.checkConflicts())
|
||||
}
|
||||
|
||||
|
||||
func Test_checkConflicts_conflict(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
"testuser2:x:1001:1001::/home/testuser:/bin/bash",
|
||||
"testuser1:x:1001:1001::/home/testuser:/bin/bash")
|
||||
assert.Error(t, db.checkConflicts())
|
||||
}
|
||||
|
||||
|
||||
func Test_getOnlyContainerLines(t *testing.T) {
|
||||
containerFileName := writeTempFile(t, `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(containerFileName)
|
||||
|
||||
|
||||
hostFileName := writeTempFile(t, `
|
||||
testuser1:x:2001:2001::/home/testuser:/bin/bash
|
||||
testuser3:x:2003:2003::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(hostFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
var err error
|
||||
err = db.readFromContainer(containerFileName)
|
||||
assert.NoError(t, err)
|
||||
err = db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
lines, err := db.getOnlyContainerLines(containerFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, lines, 1)
|
||||
assert.Equal(t, lines[0], "testuser2:x:1002:1002::/home/testuser:/bin/bash")
|
||||
}
|
||||
|
||||
|
||||
func Test_needsSync_empty(t *testing.T) {
|
||||
db := makeSyncDB(t, "", "")
|
||||
assert.False(t, db.needsSync())
|
||||
}
|
||||
|
||||
|
||||
func Test_needsSync_containerOnly(t *testing.T) {
|
||||
db := makeSyncDB(t, "", `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash`)
|
||||
|
||||
assert.False(t, db.needsSync())
|
||||
}
|
||||
|
||||
|
||||
func Test_needsSync_hostOnly(t *testing.T) {
|
||||
db := makeSyncDB(t, `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash`, "")
|
||||
|
||||
assert.True(t, db.needsSync())
|
||||
}
|
||||
|
||||
|
||||
func Test_needsSync_match(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
"testuser:x:1001:1001::/home/testuser:/bin/bash",
|
||||
"testuser:x:1001:1001::/home/testuser:/bin/bash")
|
||||
|
||||
assert.False(t, db.needsSync())
|
||||
}
|
||||
|
||||
|
||||
func Test_needsSync_differ(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
`
|
||||
testuser1:x:2001:2001::/home/testuser:/bin/bash
|
||||
testuser3:x:2003:2003::/home/testuser:/bin/bash`,
|
||||
`
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash`)
|
||||
|
||||
assert.True(t, db.needsSync())
|
||||
}
|
||||
|
||||
|
||||
func Test_onlyHost(t *testing.T) {
|
||||
db := makeSyncDB(t, "testuser1:x:2001:2001::/home/testuser:/bin/bash", "")
|
||||
entry := db["testuser1"]
|
||||
assert.True(t, entry.inHost())
|
||||
assert.False(t, entry.inContainer())
|
||||
assert.True(t, entry.onlyHost())
|
||||
assert.False(t, entry.onlyContainer())
|
||||
assert.False(t, entry.match())
|
||||
assert.False(t, entry.differ())
|
||||
}
|
||||
|
||||
|
||||
func Test_onlyContainer(t *testing.T) {
|
||||
db := makeSyncDB(t, "", "testuser1:x:2001:2001::/home/testuser:/bin/bash")
|
||||
entry := db["testuser1"]
|
||||
assert.False(t, entry.inHost())
|
||||
assert.True(t, entry.inContainer())
|
||||
assert.False(t, entry.onlyHost())
|
||||
assert.True(t, entry.onlyContainer())
|
||||
assert.False(t, entry.match())
|
||||
assert.False(t, entry.differ())
|
||||
}
|
||||
|
||||
|
||||
func Test_match(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
"testuser1:x:2001:2001::/home/testuser:/bin/bash",
|
||||
"testuser1:x:2001:2001::/home/testuser:/bin/bash")
|
||||
entry := db["testuser1"]
|
||||
assert.True(t, entry.inHost())
|
||||
assert.True(t, entry.inContainer())
|
||||
assert.False(t, entry.onlyHost())
|
||||
assert.False(t, entry.onlyContainer())
|
||||
assert.True(t, entry.match())
|
||||
assert.False(t, entry.differ())
|
||||
}
|
||||
|
||||
|
||||
func Test_differ(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
"testuser1:x:1001:1001::/home/testuser:/bin/bash",
|
||||
"testuser1:x:2001:2001::/home/testuser:/bin/bash")
|
||||
entry := db["testuser1"]
|
||||
assert.True(t, entry.inHost())
|
||||
assert.True(t, entry.inContainer())
|
||||
assert.False(t, entry.onlyHost())
|
||||
assert.False(t, entry.onlyContainer())
|
||||
assert.False(t, entry.match())
|
||||
assert.True(t, entry.differ())
|
||||
}
|
||||
Reference in New Issue
Block a user