From 7b2f278f94719ed8d3143623a0a97482d4e264f7 Mon Sep 17 00:00:00 2001 From: Jonathon Anderson Date: Fri, 26 May 2023 17:23:41 -0600 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + internal/pkg/api/container/container.go | 9 +- internal/pkg/container/syncuids.go | 581 +++++++++++++++--------- internal/pkg/container/syncuids_test.go | 283 ++++++++++++ 4 files changed, 660 insertions(+), 214 deletions(-) create mode 100644 internal/pkg/container/syncuids_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3df7afb8..5596f867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/internal/pkg/api/container/container.go b/internal/pkg/api/container/container.go index 5ae4347a..6c427664 100644 --- a/internal/pkg/api/container/container.go +++ b/internal/pkg/api/container/container.go @@ -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) diff --git a/internal/pkg/container/syncuids.go b/internal/pkg/container/syncuids.go index 62cd8b27..a4bac584 100644 --- a/internal/pkg/container/syncuids.go +++ b/internal/pkg/container/syncuids.go @@ -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 } diff --git a/internal/pkg/container/syncuids_test.go b/internal/pkg/container/syncuids_test.go new file mode 100644 index 00000000..d4cf3f8d --- /dev/null +++ b/internal/pkg/container/syncuids_test.go @@ -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()) +}