internal/util/util.go (117 lines of code) (raw):

package util import ( "bytes" "context" "fmt" "io" "os" "os/exec" "os/user" "strconv" "syscall" ) // CommandOutput wraps the output from an exec command as strings. type CommandOutput struct { Stdout string Stderr string } // ExecuteCommand executes the command and returns Stdout and Stderr as strings. func ExecuteCommand(ctx context.Context, c []string, runAsUser string, envVars []string, stdin io.ReadCloser) (output CommandOutput, err error) { // Separate name and args, plus catch a few error cases var name string var args []string // Check the empty struct case ([]string{}) for the command if len(c) == 0 { return CommandOutput{}, fmt.Errorf("must provide a command") } // Set the name of the command and check if args are also provided name = c[0] if len(c) > 1 { args = c[1:] } // Set command and create output buffers cmd := exec.CommandContext(ctx, name, args...) var stdoutb, stderrb bytes.Buffer cmd.Stdout = &stdoutb cmd.Stderr = &stderrb // Set command stdin if the stdin parameter is provided if stdin != nil { cmd.Stdin = stdin } // Set runAsUser, if defined, otherwise will run as root if runAsUser != "" { uid, gid, err := getUIDandGID(runAsUser) if err != nil { return CommandOutput{Stdout: stdoutb.String(), Stderr: stderrb.String()}, fmt.Errorf("error looking up user: %w", err) } cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} } // Append environment variables cmd.Env = os.Environ() cmd.Env = append(cmd.Env, envVars...) // Start the command's execution if err = cmd.Start(); err != nil { return CommandOutput{Stdout: stdoutb.String(), Stderr: stderrb.String()}, fmt.Errorf("error starting specified command: %w", err) } // Wait for the command to exit if err = cmd.Wait(); err != nil { return CommandOutput{Stdout: stdoutb.String(), Stderr: stderrb.String()}, fmt.Errorf("error waiting for specified command to exit: %w", err) } return CommandOutput{Stdout: stdoutb.String(), Stderr: stderrb.String()}, err } // ExecuteCommandYes wraps ExecuteCommand with the yes binary in order to bypass user input states in automation. func ExecuteCommandYes(ctx context.Context, c []string, runAsUser string, envVars []string) (output CommandOutput, err error) { // Set exec commands, one for yes and another for the specified command cmdYes := exec.Command("/usr/bin/yes") // Pipe cmdYes into cmd stdin, err := cmdYes.StdoutPipe() if err != nil { return CommandOutput{}, fmt.Errorf("error creating pipe between commands") } // Start the command to run /usr/bin/yes if err = cmdYes.Start(); err != nil { return CommandOutput{}, fmt.Errorf("error starting /usr/bin/yes command: %w", err) } return ExecuteCommand(ctx, c, runAsUser, envVars, stdin) } // getUIDandGID takes a username and returns the uid and gid for that user. // While testing UID/GID lookup for a user, it was found that the user.Lookup() function does not always return // information for a new user on first boot. In the case that user.Lookup() fails, try dscacheutil, which has a // higher success rate. If that fails, return an error. Any successful case returns the UID and GID as ints. func getUIDandGID(username string) (uid int, gid int, err error) { var uidstr, gidstr string // Preference is user.Lookup(), if it works u, lookupErr := user.Lookup(username) if lookupErr == nil { // user.Lookup() was successful, use the returned UID/GID uidstr = u.Uid gidstr = u.Gid } else { // user.Lookup() has failed, second try by checking the DS cache out, cmdErr := ExecuteCommand(context.Background(), []string{"dscacheutil", "-q", "user", "-a", "name", username}, "", []string{}, nil) if cmdErr != nil { // dscacheutil has failed with an error return 0, 0, fmt.Errorf("dscacheutil: %w", cmdErr) } if len(out.Stdout) == 0 { // dscacheutil returns nothing if user is not found return 0, 0, fmt.Errorf("dscacheutil read user %q: %w", username, cmdErr) } else { // dscacheutil found user, extract the user info by keys dscacheUserInfo := extractDSCacheUtilKeyValues([]byte(out.Stdout), []string{"gid", "uid"}) if len(dscacheUserInfo) == 0 { return 0, 0, fmt.Errorf("dscacheutil read user %q: %w", username, cmdErr) } if gid, ok := dscacheUserInfo["gid"]; ok { gidstr = gid } if uid, ok := dscacheUserInfo["uid"]; ok { uidstr = uid } } } // make sure we actually resolved a user before carrying on if uidstr == "" || gidstr == "" { return 0, 0, fmt.Errorf("user %q: no user info", username) } // Parse read UID and GID to integers uid, err = strconv.Atoi(uidstr) if err != nil { return 0, 0, fmt.Errorf("parse %q uid: %w", username, err) } gid, err = strconv.Atoi(gidstr) if err != nil { return 0, 0, fmt.Errorf("parse %q gid: %w", username, err) } return uid, gid, nil } func extractDSCacheUtilKeyValues(text []byte, keys []string) map[string]string { // Command output from dscacheutil should look like: // // name: ec2-user // password: ******** // uid: 501 // gid: 20 // dir: /Users/ec2-user // shell: /bin/bash // gecos: ec2-user // extracted := map[string]string{} lines := bytes.Split(text, []byte("\n")) // split on newline to separate uid and gid for _, kvLine := range lines { // kv splits the [key: value] lines into their components kv := bytes.SplitN(kvLine, []byte(": "), 2) if len(kv) < 2 { continue } for _, key := range keys { if bytes.EqualFold(kv[0], []byte(key)) { extracted[key] = string(kv[1]) } } } return extracted }