google_guest_agent/run/run.go (164 lines of code) (raw):
// Copyright 2023 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// https://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package run is a package with utilities for running command and handling results.
package run
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
"text/template"
"time"
"github.com/GoogleCloudPlatform/guest-logging-go/logger"
)
var (
// Client is the Runner running commands.
Client RunnerInterface
// ErrCommandTemplate is the error returned when a CommandSpec's Command template is boggus.
ErrCommandTemplate = errors.New("invalid command format template")
// ErrTemplateError is the error returned when a CommandSpec's Error template is boggus.
ErrTemplateError = errors.New("invalid error format template")
)
// Result wraps a command execution result.
type Result struct {
// Exit code. Set to -1 if we failed to run the command.
ExitCode int
// Stderr or err.Error if we failed to run the command.
StdErr string
// Stdout or "" if we failed to run the command.
StdOut string
// Combined is the process' stdout and stderr combined.
Combined string
}
// RunnerInterface defines the runner running commands.
type RunnerInterface interface {
// Quiet runs a command and doesn't return a result, but errors in case of failure.
Quiet(ctx context.Context, name string, args ...string) error
// WithOutput runs a command and returns the result.
WithOutput(ctx context.Context, name string, args ...string) *Result
// WithOutputTimeout runs a command with a defined timeout and returns its result.
WithOutputTimeout(ctx context.Context, timeout time.Duration, name string, args ...string) *Result
// WithCombinedOutput runs a command and returns a result with stderr and stdout
// combined in the Combined member of Result.
WithCombinedOutput(ctx context.Context, name string, args ...string) *Result
}
// CommandSpec defines a Command template and an Error template. The data
// structure to be used with the templates is up to the user to define.
type CommandSpec struct {
// Command is the command template i.e: "echo '{{.MyDataString}}'".
Command string
// Error is the error template, if the command fails this template is
// used to build the error message, i.e: "failed to parse file {{.FileName}}".
Error string
}
// CommandSet is set of commands to be executed together, IOW a command batch.
type CommandSet []CommandSpec
// init initializes the RunClient.
func init() {
Client = Runner{}
}
// RunQuiet runs all the commands in a CommandSet, no command output is handled.
// All commands are run as a batch.
func (s CommandSet) RunQuiet(ctx context.Context, data any) error {
for _, curr := range s {
if err := curr.RunQuiet(ctx, data); err != nil {
return err
}
}
return nil
}
// commandFormat formats the CommandSpec's Command field. The data is passed in
// to the template parsing and execution.
func (c CommandSpec) commandFormat(data any) ([]string, error) {
if len(strings.Trim(c.Command, " ")) == 0 {
return nil, ErrCommandTemplate
}
tmpl, err := template.New("").Parse(c.Command)
if err != nil {
logger.Debugf("error parsing command format: %+v", err)
return nil, ErrCommandTemplate
}
var buffer bytes.Buffer
if err := tmpl.Execute(&buffer, data); err != nil {
logger.Debugf("error executing command format: %+v", err)
return nil, ErrCommandTemplate
}
return strings.Split(buffer.String(), " "), nil
}
// errorFormat formats the CommandSpec's Error field. The data is passed in to the
// template parsing and execution.
func (c CommandSpec) errorFormat(data any) (string, error) {
tmpl, err := template.New("").Parse(c.Error)
if err != nil {
logger.Debugf("error parsing error format: %+v", err)
return "", ErrTemplateError
}
var buffer bytes.Buffer
if err := tmpl.Execute(&buffer, data); err != nil {
logger.Debugf("error executing error format: %+v", err)
return "", ErrTemplateError
}
return buffer.String(), nil
}
// RunQuiet runs a CommandSpec command, no command output is handled.
func (c CommandSpec) RunQuiet(ctx context.Context, data any) error {
tokens, err := c.commandFormat(data)
if err != nil {
return err
}
errorMsg, err := c.errorFormat(data)
if err != nil {
return err
}
if err := Client.Quiet(ctx, tokens[0], tokens[1:]...); err != nil {
return fmt.Errorf("%+s: %+v: %+v", errorMsg, len(tokens), err)
}
return nil
}
// Error return an error containing the stderr content.
func (e Result) Error() string {
return strings.TrimSuffix(e.StdErr, "\n")
}
// Runner implements the RunnerInterface and represents the runner running commands.
type Runner struct{}
// Quiet runs a command and doesn't return a result, but an error in case of failure.
func (r Runner) Quiet(ctx context.Context, name string, args ...string) error {
res := execCommand(exec.CommandContext(ctx, name, args...))
if res.ExitCode != 0 {
return res
}
return nil
}
// WithOutput runs a command and returns the result.
func (r Runner) WithOutput(ctx context.Context, name string, args ...string) *Result {
return execCommand(exec.CommandContext(ctx, name, args...))
}
// WithOutputTimeout runs a command with a defined timeout and returns its result.
func (r Runner) WithOutputTimeout(ctx context.Context, timeout time.Duration, name string, args ...string) *Result {
child, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
res := execCommand(exec.CommandContext(child, name, args...))
if child.Err() != nil && errors.Is(child.Err(), context.DeadlineExceeded) {
res.ExitCode = 124 // By convention
}
return res
}
// WithCombinedOutput returns a result with stderr and stdout combined in the Combined
// member of Result.
func (r Runner) WithCombinedOutput(ctx context.Context, name string, args ...string) *Result {
cmd := exec.CommandContext(ctx, name, args...)
output, err := cmd.CombinedOutput()
if err != nil {
exitCode := -1
if ee, ok := err.(*exec.ExitError); ok {
exitCode = ee.ExitCode()
}
return &Result{
ExitCode: exitCode,
StdErr: err.Error(),
}
}
return &Result{
Combined: string(output),
}
}
// Quiet runs the current RunClient's Quiet() function.
func Quiet(ctx context.Context, name string, args ...string) error {
return Client.Quiet(ctx, name, args...)
}
// WithOutput runs the current RunClient's WithOutput() function.
func WithOutput(ctx context.Context, name string, args ...string) *Result {
return Client.WithOutput(ctx, name, args...)
}
// WithOutputTimeout runs the current RunClient's WithOutputTimeout() function.
func WithOutputTimeout(ctx context.Context, timeout time.Duration, name string, args ...string) *Result {
return Client.WithOutputTimeout(ctx, timeout, name, args...)
}
// WithCombinedOutput runs the current RunCLient's WithCombinedOutput function.
func WithCombinedOutput(ctx context.Context, name string, args ...string) *Result {
return Client.WithCombinedOutput(ctx, name, args...)
}
func execCommand(cmd *exec.Cmd) *Result {
var stdout, stderr bytes.Buffer
logger.Debugf("exec: %v", cmd)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return &Result{
ExitCode: ee.ExitCode(),
StdOut: stdout.String(),
StdErr: stderr.String(),
}
}
return &Result{
ExitCode: -1,
StdErr: err.Error(),
}
}
return &Result{
ExitCode: 0,
StdOut: stdout.String(),
}
}