...

Source file src/k8s.io/kubernetes/cmd/kubeadm/app/util/users/users_linux.go

Documentation: k8s.io/kubernetes/cmd/kubeadm/app/util/users

     1  //go:build linux
     2  // +build linux
     3  
     4  /*
     5  Copyright 2021 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package users
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"path/filepath"
    28  	"sort"
    29  	"strconv"
    30  	"strings"
    31  	"syscall"
    32  	"time"
    33  
    34  	"github.com/pkg/errors"
    35  
    36  	"k8s.io/klog/v2"
    37  
    38  	"k8s.io/kubernetes/cmd/kubeadm/app/constants"
    39  )
    40  
    41  // EntryMap holds a map of user or group entries.
    42  type EntryMap struct {
    43  	entries map[string]*entry
    44  }
    45  
    46  // UsersAndGroups is a structure that holds entry maps of users and groups.
    47  // It is returned by AddUsersAndGroups.
    48  type UsersAndGroups struct {
    49  	// Users is an entry map of users.
    50  	Users *EntryMap
    51  	// Groups is an entry map of groups.
    52  	Groups *EntryMap
    53  }
    54  
    55  // entry is a structure that holds information about a UNIX user or group.
    56  // It partialially conforms parsing of both users from /etc/passwd and groups from /etc/group.
    57  type entry struct {
    58  	name      string
    59  	id        int64
    60  	gid       int64
    61  	userNames []string
    62  	shell     string
    63  }
    64  
    65  // limits is used to hold information about the minimum and maximum system ranges for UID and GID.
    66  type limits struct {
    67  	minUID, maxUID, minGID, maxGID int64
    68  }
    69  
    70  const (
    71  	// These are constants used when parsing /etc/passwd or /etc/group in terms of how many
    72  	// fields and entry has.
    73  	totalFieldsGroup = 4
    74  	totalFieldsUser  = 7
    75  
    76  	// klogLevel holds the klog level to use for output.
    77  	klogLevel = 5
    78  
    79  	// noshell holds a path to a binary to disable shell login.
    80  	noshell = "/bin/false"
    81  
    82  	// These are constants for the default system paths on Linux.
    83  	fileEtcLoginDefs = "/etc/login.defs"
    84  	fileEtcPasswd    = "/etc/passwd"
    85  	fileEtcGroup     = "/etc/group"
    86  )
    87  
    88  var (
    89  	// these entries hold the users and groups to create as defined in:
    90  	// https://git.k8s.io/enhancements/keps/sig-cluster-lifecycle/kubeadm/2568-kubeadm-non-root-control-plane
    91  	usersToCreateSpec = []*entry{
    92  		{name: constants.EtcdUserName},
    93  		{name: constants.KubeAPIServerUserName},
    94  		{name: constants.KubeControllerManagerUserName},
    95  		{name: constants.KubeSchedulerUserName},
    96  	}
    97  	groupsToCreateSpec = []*entry{
    98  		{name: constants.EtcdUserName, userNames: []string{constants.EtcdUserName}},
    99  		{name: constants.KubeAPIServerUserName, userNames: []string{constants.KubeAPIServerUserName}},
   100  		{name: constants.KubeControllerManagerUserName, userNames: []string{constants.KubeControllerManagerUserName}},
   101  		{name: constants.KubeSchedulerUserName, userNames: []string{constants.KubeSchedulerUserName}},
   102  		{name: constants.ServiceAccountKeyReadersGroupName, userNames: []string{constants.KubeAPIServerUserName, constants.KubeControllerManagerUserName}},
   103  	}
   104  
   105  	// defaultLimits holds the default limits in case values are missing in /etc/login.defs
   106  	defaultLimits = &limits{minUID: 100, maxUID: 999, minGID: 100, maxGID: 999}
   107  )
   108  
   109  // ID returns the ID for an entry based on the entry name.
   110  // In case of a user entry it returns the user UID.
   111  // In case of a group entry it returns the group GID.
   112  // It returns nil if no such entry exists.
   113  func (u *EntryMap) ID(name string) *int64 {
   114  	entry, ok := u.entries[name]
   115  	if !ok {
   116  		return nil
   117  	}
   118  	id := entry.id
   119  	return &id
   120  }
   121  
   122  // String converts an EntryMap object to a readable string.
   123  func (u *EntryMap) String() string {
   124  	lines := make([]string, 0, len(u.entries))
   125  	for k, e := range u.entries {
   126  		lines = append(lines, fmt.Sprintf("%s{%d,%d};", k, e.id, e.gid))
   127  	}
   128  	sort.Strings(lines)
   129  	return strings.Join(lines, "")
   130  }
   131  
   132  // AddUsersAndGroups is a public wrapper around addUsersAndGroupsImpl with default system file paths.
   133  func AddUsersAndGroups() (*UsersAndGroups, error) {
   134  	return addUsersAndGroupsImpl(fileEtcLoginDefs, fileEtcPasswd, fileEtcGroup)
   135  }
   136  
   137  // addUsersAndGroupsImpl adds the managed users and groups to the files specified
   138  // by pathUsers and pathGroups. It uses the file specified with pathLoginDef to
   139  // determine limits for UID and GID. If managed users and groups exist in these files
   140  // validation is performed on them. The function returns a pointer to a Users object
   141  // that can be used to return UID and GID of managed users.
   142  func addUsersAndGroupsImpl(pathLoginDef, pathUsers, pathGroups string) (*UsersAndGroups, error) {
   143  	klog.V(1).Info("Adding managed users and groups")
   144  	klog.V(klogLevel).Infof("Parsing %q", pathLoginDef)
   145  
   146  	// Read and parse /etc/login.def. Some distributions might be missing this file, which makes
   147  	// them non-standard. If an error occurs fallback to defaults by passing an empty string
   148  	// to parseLoginDefs().
   149  	var loginDef string
   150  	f, close, err := openFileWithLock(pathLoginDef)
   151  	if err != nil {
   152  		klog.V(1).Infof("Could not open %q, using default system limits: %v", pathLoginDef, err)
   153  	} else {
   154  		loginDef, err = readFile(f)
   155  		if err != nil {
   156  			klog.V(1).Infof("Could not read %q, using default system limits: %v", pathLoginDef, err)
   157  		}
   158  		close()
   159  	}
   160  	limits, err := parseLoginDefs(loginDef)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	klog.V(klogLevel).Infof("Using system UID/GID limits: %+v", limits)
   166  	klog.V(klogLevel).Infof("Parsing %q and %q", pathUsers, pathGroups)
   167  
   168  	// Open /etc/passwd and /etc/group with locks.
   169  	fUsers, close, err := openFileWithLock(pathUsers)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	defer close()
   174  	fGroups, close, err := openFileWithLock(pathGroups)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  	defer close()
   179  
   180  	// Read the files.
   181  	fileUsers, err := readFile(fUsers)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	fileGroups, err := readFile(fGroups)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	// Parse the files.
   191  	users, err := parseEntries(fileUsers, totalFieldsUser)
   192  	if err != nil {
   193  		return nil, errors.Wrapf(err, "could not parse %q", pathUsers)
   194  	}
   195  	groups, err := parseEntries(fileGroups, totalFieldsGroup)
   196  	if err != nil {
   197  		return nil, errors.Wrapf(err, "could not parse %q", pathGroups)
   198  	}
   199  
   200  	klog.V(klogLevel).Info("Validating existing users and groups")
   201  
   202  	// Validate for existing tracked entries based on limits.
   203  	usersToCreate, groupsToCreate, err := validateEntries(users, groups, limits)
   204  	if err != nil {
   205  		return nil, errors.Wrap(err, "error validating existing users and groups")
   206  	}
   207  
   208  	// Allocate and assign IDs to users / groups.
   209  	allocUIDs, err := allocateIDs(users, limits.minUID, limits.maxUID, len(usersToCreate))
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	allocGIDs, err := allocateIDs(groups, limits.minGID, limits.maxGID, len(groupsToCreate))
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	if err := assignUserAndGroupIDs(groups, usersToCreate, groupsToCreate, allocUIDs, allocGIDs); err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	if len(usersToCreate) > 0 {
   222  		klog.V(klogLevel).Infof("Adding users: %s", entriesToString(usersToCreate))
   223  	}
   224  	if len(groupsToCreate) > 0 {
   225  		klog.V(klogLevel).Infof("Adding groups: %s", entriesToString(groupsToCreate))
   226  	}
   227  
   228  	// Add users and groups.
   229  	fileUsers = addEntries(fileUsers, usersToCreate, createUser)
   230  	fileGroups = addEntries(fileGroups, groupsToCreate, createGroup)
   231  
   232  	// Write the files.
   233  	klog.V(klogLevel).Infof("Writing %q and %q", pathUsers, pathGroups)
   234  	if err := writeFile(fUsers, fileUsers); err != nil {
   235  		return nil, err
   236  	}
   237  	if err := writeFile(fGroups, fileGroups); err != nil {
   238  		return nil, err
   239  	}
   240  
   241  	// Prepare the maps of users and groups.
   242  	usersConcat := append(users, usersToCreate...)
   243  	mapUsers, err := entriesToEntryMap(usersConcat, usersToCreateSpec)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	groupsConcat := append(groups, groupsToCreate...)
   248  	mapGroups, err := entriesToEntryMap(groupsConcat, groupsToCreateSpec)
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  	return &UsersAndGroups{Users: mapUsers, Groups: mapGroups}, nil
   253  }
   254  
   255  // RemoveUsersAndGroups is a public wrapper around removeUsersAndGroupsImpl with
   256  // default system file paths.
   257  func RemoveUsersAndGroups() error {
   258  	return removeUsersAndGroupsImpl(fileEtcPasswd, fileEtcGroup)
   259  }
   260  
   261  // removeUsersAndGroupsImpl removes the managed users and groups from the files specified
   262  // by pathUsers and pathGroups.
   263  func removeUsersAndGroupsImpl(pathUsers, pathGroups string) error {
   264  	klog.V(1).Info("Removing managed users and groups")
   265  	klog.V(klogLevel).Infof("Opening %q and %q", pathUsers, pathGroups)
   266  
   267  	// Open /etc/passwd and /etc/group.
   268  	fUsers, close, err := openFileWithLock(pathUsers)
   269  	if err != nil {
   270  		return err
   271  	}
   272  	defer close()
   273  	fGroups, close, err := openFileWithLock(pathGroups)
   274  	if err != nil {
   275  		return err
   276  	}
   277  	defer close()
   278  
   279  	// Read the files.
   280  	fileUsers, err := readFile(fUsers)
   281  	if err != nil {
   282  		return err
   283  	}
   284  	fileGroups, err := readFile(fGroups)
   285  	if err != nil {
   286  		return err
   287  	}
   288  
   289  	klog.V(klogLevel).Infof("Removing users: %s", entriesToString(usersToCreateSpec))
   290  	klog.V(klogLevel).Infof("Removing groups: %s", entriesToString(groupsToCreateSpec))
   291  
   292  	// Delete users / groups.
   293  	fileUsers, _ = removeEntries(fileUsers, usersToCreateSpec)
   294  	fileGroups, _ = removeEntries(fileGroups, groupsToCreateSpec)
   295  
   296  	klog.V(klogLevel).Infof("Writing %q and %q", pathUsers, pathGroups)
   297  
   298  	// Write the files.
   299  	if err := writeFile(fUsers, fileUsers); err != nil {
   300  		return err
   301  	}
   302  	if err := writeFile(fGroups, fileGroups); err != nil {
   303  		return err
   304  	}
   305  
   306  	return nil
   307  }
   308  
   309  // parseLoginDefs can be used to parse an /etc/login.defs file and obtain system ranges for UID and GID.
   310  // Passing an empty string will return the defaults. The defaults are 100-999 for both UID and GID.
   311  func parseLoginDefs(file string) (*limits, error) {
   312  	l := *defaultLimits
   313  	if len(file) == 0 {
   314  		return &l, nil
   315  	}
   316  	var mapping = map[string]*int64{
   317  		"SYS_UID_MIN": &l.minUID,
   318  		"SYS_UID_MAX": &l.maxUID,
   319  		"SYS_GID_MIN": &l.minGID,
   320  		"SYS_GID_MAX": &l.maxGID,
   321  	}
   322  	lines := strings.Split(file, "\n")
   323  	for i, line := range lines {
   324  		for k, v := range mapping {
   325  			// A line must start with one of the definitions
   326  			if !strings.HasPrefix(line, k) {
   327  				continue
   328  			}
   329  			line = strings.TrimPrefix(line, k)
   330  			line = strings.TrimSpace(line)
   331  			val, err := strconv.ParseInt(line, 10, 64)
   332  			if err != nil {
   333  				return nil, errors.Wrapf(err, "could not parse value for %s at line %d", k, i)
   334  			}
   335  			*v = val
   336  		}
   337  	}
   338  	return &l, nil
   339  }
   340  
   341  // parseEntries can be used to parse an /etc/passwd or /etc/group file as their format is similar.
   342  // It returns a slice of entries obtained from the file.
   343  // https://www.cyberciti.biz/faq/understanding-etcpasswd-file-format/
   344  // https://www.cyberciti.biz/faq/understanding-etcgroup-file/
   345  func parseEntries(file string, totalFields int) ([]*entry, error) {
   346  	if totalFields != totalFieldsUser && totalFields != totalFieldsGroup {
   347  		return nil, errors.Errorf("unsupported total fields for entry parsing: %d", totalFields)
   348  	}
   349  	lines := strings.Split(file, "\n")
   350  	entries := []*entry{}
   351  	for i, line := range lines {
   352  		line = strings.TrimSpace(line)
   353  		if len(line) == 0 {
   354  			continue
   355  		}
   356  		fields := strings.Split(line, ":")
   357  		if len(fields) != totalFields {
   358  			return nil, errors.Errorf("entry must have %d fields separated by ':', "+
   359  				"got %d at line %d: %s", totalFields, len(fields), i, line)
   360  		}
   361  		id, err := strconv.ParseInt(fields[2], 10, 64)
   362  		if err != nil {
   363  			return nil, errors.Wrapf(err, "error parsing id at line %d", i)
   364  		}
   365  		entry := &entry{name: fields[0], id: id}
   366  		if totalFields == totalFieldsGroup {
   367  			entry.userNames = strings.Split(fields[3], ",")
   368  		} else {
   369  			gid, err := strconv.ParseInt(fields[3], 10, 64)
   370  			if err != nil {
   371  				return nil, errors.Wrapf(err, "error parsing GID at line %d", i)
   372  			}
   373  			entry.gid = gid
   374  			entry.shell = fields[6]
   375  		}
   376  		entries = append(entries, entry)
   377  	}
   378  	return entries, nil
   379  }
   380  
   381  // validateEntries takes user and group entries and validates if these entries are valid based on limits,
   382  // mapping between users and groups and specs. Returns slices of missing user and group entries that must be created.
   383  // Returns an error if existing users and groups do not match requirements.
   384  func validateEntries(users, groups []*entry, limits *limits) ([]*entry, []*entry, error) {
   385  	u := []*entry{}
   386  	g := []*entry{}
   387  	// Validate users
   388  	for _, uc := range usersToCreateSpec {
   389  		for _, user := range users {
   390  			if uc.name != user.name {
   391  				continue
   392  			}
   393  			// Found existing user
   394  			if user.id < limits.minUID || user.id > limits.maxUID {
   395  				return nil, nil, errors.Errorf("UID %d for user %q is outside the system UID range: %d - %d",
   396  					user.id, user.name, limits.minUID, limits.maxUID)
   397  			}
   398  			if user.shell != noshell {
   399  				return nil, nil, errors.Errorf("user %q has unexpected shell %q; expected %q",
   400  					user.name, user.shell, noshell)
   401  			}
   402  			for _, g := range groups {
   403  				if g.id != user.gid {
   404  					continue
   405  				}
   406  				// Found matching group GID for user GID
   407  				if g.name != uc.name {
   408  					return nil, nil, errors.Errorf("user %q has GID %d but the group with that GID is not named %q",
   409  						uc.name, g.id, uc.name)
   410  				}
   411  				goto skipUser // Valid group GID and name; skip
   412  			}
   413  			return nil, nil, errors.Errorf("could not find group with GID %d for user %q", user.gid, user.name)
   414  		}
   415  		u = append(u, uc)
   416  	skipUser:
   417  	}
   418  	// validate groups
   419  	for _, gc := range groupsToCreateSpec {
   420  		for _, group := range groups {
   421  			if gc.name != group.name {
   422  				continue
   423  			}
   424  			if group.id < limits.minGID || group.id > limits.maxGID {
   425  				return nil, nil, errors.Errorf("GID %d for user %q is outside the system UID range: %d - %d",
   426  					group.id, group.name, limits.minGID, limits.maxGID)
   427  			}
   428  			u1 := strings.Join(gc.userNames, ",")
   429  			u2 := strings.Join(group.userNames, ",")
   430  			if u1 != u2 {
   431  				return nil, nil, errors.Errorf("expected users %q for group %q; got %q",
   432  					u1, gc.name, u2)
   433  			}
   434  			goto skipGroup // group has valid users; skip
   435  		}
   436  		g = append(g, gc)
   437  	skipGroup:
   438  	}
   439  	return u, g, nil
   440  }
   441  
   442  // allocateIDs takes a list of entries and based on minimum and maximum ID allocates a "total" of IDs.
   443  func allocateIDs(entries []*entry, min, max int64, total int) ([]int64, error) {
   444  	if total == 0 {
   445  		return []int64{}, nil
   446  	}
   447  	ids := make([]int64, 0, total)
   448  	for i := min; i < max+1; i++ {
   449  		i64 := int64(i)
   450  		for _, e := range entries {
   451  			if i64 == e.id {
   452  				goto continueLoop
   453  			}
   454  		}
   455  		ids = append(ids, i64)
   456  		if len(ids) == total {
   457  			return ids, nil
   458  		}
   459  	continueLoop:
   460  	}
   461  	return nil, errors.Errorf("could not allocate %d IDs based on existing entries in the range: %d - %d",
   462  		total, min, max)
   463  }
   464  
   465  // addEntries takes /etc/passwd or /etc/group file content and appends entries to it based
   466  // on a createEntry function. Returns the updated contents of the file.
   467  func addEntries(file string, entries []*entry, createEntry func(*entry) string) string {
   468  	out := file
   469  	newLines := make([]string, 0, len(entries))
   470  	for _, e := range entries {
   471  		newLines = append(newLines, createEntry(e))
   472  	}
   473  	newLinesStr := ""
   474  	if len(newLines) > 0 {
   475  		if !strings.HasSuffix(out, "\n") { // Append a new line if its missing.
   476  			newLinesStr = "\n"
   477  		}
   478  		newLinesStr += strings.Join(newLines, "\n") + "\n"
   479  	}
   480  	return out + newLinesStr
   481  }
   482  
   483  // removeEntries takes /etc/passwd or /etc/group file content and deletes entries from them
   484  // by name matching. Returns the updated contents of the file and the number of entries removed.
   485  func removeEntries(file string, entries []*entry) (string, int) {
   486  	lines := strings.Split(file, "\n")
   487  	total := len(lines) - len(entries)
   488  	if total < 0 {
   489  		total = 0
   490  	}
   491  	newLines := make([]string, 0, total)
   492  	removed := 0
   493  	for _, line := range lines {
   494  		for _, entry := range entries {
   495  			if strings.HasPrefix(line, entry.name+":") {
   496  				removed++
   497  				goto continueLoop
   498  			}
   499  		}
   500  		newLines = append(newLines, line)
   501  	continueLoop:
   502  	}
   503  	return strings.Join(newLines, "\n"), removed
   504  }
   505  
   506  // assignUserAndGroupIDs takes the list of existing groups, the users and groups to be created,
   507  // and assigns UIDs and GIDs to the users and groups to be created based on a list of provided UIDs and GIDs.
   508  // Returns an error if not enough UIDs or GIDs are passed. It does not perform any other validation.
   509  func assignUserAndGroupIDs(groups, usersToCreate, groupsToCreate []*entry, uids, gids []int64) error {
   510  	if len(gids) < len(groupsToCreate) {
   511  		return errors.Errorf("not enough GIDs to assign to groups: have %d, want %d", len(gids), len(groupsToCreate))
   512  	}
   513  	if len(uids) < len(usersToCreate) {
   514  		return errors.Errorf("not enough UIDs to assign to users: have %d, want %d", len(uids), len(usersToCreate))
   515  	}
   516  	for i := range groupsToCreate {
   517  		groupsToCreate[i].id = gids[i]
   518  	}
   519  	// Concat the list of old and new groups to find a matching GID.
   520  	groupsConcat := append([]*entry{}, groups...)
   521  	groupsConcat = append(groupsConcat, groupsToCreate...)
   522  	for i := range usersToCreate {
   523  		usersToCreate[i].id = uids[i]
   524  		for _, g := range groupsConcat {
   525  			if usersToCreate[i].name == g.name {
   526  				usersToCreate[i].gid = g.id
   527  				break
   528  			}
   529  		}
   530  	}
   531  	return nil
   532  }
   533  
   534  // createGroup is a helper function to produce a group from entry.
   535  func createGroup(e *entry) string {
   536  	return fmt.Sprintf("%s:x:%d:%s", e.name, e.id, strings.Join(e.userNames, ","))
   537  }
   538  
   539  // createUser is a helper function to produce a user from entry.
   540  func createUser(e *entry) string {
   541  	return fmt.Sprintf("%s:x:%d:%d:::/bin/false", e.name, e.id, e.gid)
   542  }
   543  
   544  // entriesToEntryMap takes a list of entries and prepares an EntryMap object.
   545  func entriesToEntryMap(entries, spec []*entry) (*EntryMap, error) {
   546  	m := map[string]*entry{}
   547  	for _, spec := range spec {
   548  		for _, e := range entries {
   549  			if spec.name == e.name {
   550  				entry := *e
   551  				m[e.name] = &entry
   552  				goto continueLoop
   553  			}
   554  		}
   555  		return nil, errors.Errorf("could not find entry %q in the list", spec.name)
   556  	continueLoop:
   557  	}
   558  	return &EntryMap{entries: m}, nil
   559  }
   560  
   561  // entriesToString is a utility to convert a list of entries to string.
   562  func entriesToString(entries []*entry) string {
   563  	lines := make([]string, 0, len(entries))
   564  	for _, e := range entries {
   565  		lines = append(lines, e.name)
   566  	}
   567  	sort.Strings(lines)
   568  	return strings.Join(lines, ",")
   569  }
   570  
   571  // openFileWithLock opens the file at path by acquiring an exclive write lock.
   572  // The returned close() function should be called to release the lock and close the file.
   573  // If a lock cannot be obtained the function fails after a period of time.
   574  func openFileWithLock(path string) (f *os.File, close func(), err error) {
   575  	f, err = os.OpenFile(path, os.O_RDWR, os.ModePerm)
   576  	if err != nil {
   577  		return nil, nil, err
   578  	}
   579  	deadline := time.Now().Add(time.Second * 5)
   580  	for {
   581  		// If another process is holding a write lock, this call will exit
   582  		// with an error. F_SETLK is used instead of F_SETLKW to avoid
   583  		// the case where a runaway process grabs the exclusive lock and
   584  		// blocks this call indefinitely.
   585  		// https://man7.org/linux/man-pages/man2/fcntl.2.html
   586  		lock := syscall.Flock_t{Type: syscall.F_WRLCK}
   587  		if err = syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &lock); err == nil {
   588  			break
   589  		}
   590  		time.Sleep(200 * time.Millisecond)
   591  		if time.Now().After(deadline) {
   592  			err = errors.Wrapf(err, "timeout attempting to obtain lock on file %q", path)
   593  			break
   594  		}
   595  	}
   596  	if err != nil {
   597  		f.Close()
   598  		return nil, nil, err
   599  	}
   600  	close = func() {
   601  		// This function should be called once operations with the file are finished.
   602  		// It unlocks the file and closes it.
   603  		unlock := syscall.Flock_t{Type: syscall.F_UNLCK}
   604  		syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &unlock)
   605  		f.Close()
   606  	}
   607  	return f, close, nil
   608  }
   609  
   610  // readFile reads a File into a string.
   611  func readFile(f *os.File) (string, error) {
   612  	buf := bytes.NewBuffer(nil)
   613  	if _, err := f.Seek(0, io.SeekStart); err != nil {
   614  		return "", err
   615  	}
   616  	if _, err := io.Copy(buf, f); err != nil {
   617  		return "", err
   618  	}
   619  	return buf.String(), nil
   620  }
   621  
   622  // writeFile writes a string to a File.
   623  func writeFile(f *os.File, str string) error {
   624  	if _, err := f.Seek(0, io.SeekStart); err != nil {
   625  		return err
   626  	}
   627  	if _, err := f.Write([]byte(str)); err != nil {
   628  		return err
   629  	}
   630  	if err := f.Truncate(int64(len(str))); err != nil {
   631  		return err
   632  	}
   633  	return nil
   634  }
   635  
   636  // UpdatePathOwnerAndPermissions updates the owner and permissions of the given path.
   637  // If the path is a directory it is not recursively updated.
   638  func UpdatePathOwnerAndPermissions(path string, uid, gid int64, perms uint32) error {
   639  	if err := os.Chown(path, int(uid), int(gid)); err != nil {
   640  		return errors.Wrapf(err, "failed to update owner of %q to uid: %d and gid: %d", path, uid, gid)
   641  	}
   642  	fm := os.FileMode(perms)
   643  	if err := os.Chmod(path, fm); err != nil {
   644  		return errors.Wrapf(err, "failed to update permissions of %q to %s", path, fm.String())
   645  	}
   646  	return nil
   647  }
   648  
   649  // UpdatePathOwner recursively updates the owners of a directory.
   650  // It is equivalent to calling `chown -R uid:gid /path/to/dir`.
   651  func UpdatePathOwner(dirPath string, uid, gid int64) error {
   652  	err := filepath.WalkDir(dirPath, func(path string, d os.DirEntry, err error) error {
   653  		if err := os.Chown(path, int(uid), int(gid)); err != nil {
   654  			return errors.Wrapf(err, "failed to update owner of %q to uid: %d and gid: %d", path, uid, gid)
   655  		}
   656  		return nil
   657  	})
   658  	return err
   659  }
   660  

View as plain text