github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/action/showoutput.go (about)

     1  // Copyright 2014, 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package action
     5  
     6  import (
     7  	"regexp"
     8  	"time"
     9  
    10  	"github.com/juju/cmd"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/gnuflag"
    13  
    14  	"github.com/juju/juju/apiserver/params"
    15  	jujucmd "github.com/juju/juju/cmd"
    16  	"github.com/juju/juju/cmd/modelcmd"
    17  	"github.com/juju/juju/cmd/output"
    18  )
    19  
    20  func NewShowOutputCommand() cmd.Command {
    21  	return modelcmd.Wrap(&showOutputCommand{})
    22  }
    23  
    24  // showOutputCommand fetches the results of an action by ID.
    25  type showOutputCommand struct {
    26  	ActionCommandBase
    27  	out         cmd.Output
    28  	requestedId string
    29  	fullSchema  bool
    30  	wait        string
    31  }
    32  
    33  const showOutputDoc = `
    34  Show the results returned by an action with the given ID.  A partial ID may
    35  also be used.  To block until the result is known completed or failed, use
    36  the --wait option with a duration, as in --wait 5s or --wait 1h.  Use --wait 0
    37  to wait indefinitely.  If units are left off, seconds are assumed.
    38  
    39  The default behavior without --wait is to immediately check and return; if
    40  the results are "pending" then only the available information will be
    41  displayed.  This is also the behavior when any negative time is given.
    42  `
    43  
    44  // Set up the output.
    45  func (c *showOutputCommand) SetFlags(f *gnuflag.FlagSet) {
    46  	c.ActionCommandBase.SetFlags(f)
    47  	c.out.AddFlags(f, "yaml", output.DefaultFormatters)
    48  	f.StringVar(&c.wait, "wait", "-1s", "Wait for results")
    49  }
    50  
    51  func (c *showOutputCommand) Info() *cmd.Info {
    52  	return jujucmd.Info(&cmd.Info{
    53  		Name:    "show-action-output",
    54  		Args:    "<action ID>",
    55  		Purpose: "Show results of an action by ID.",
    56  		Doc:     showOutputDoc,
    57  	})
    58  }
    59  
    60  // Init validates the action ID and any other options.
    61  func (c *showOutputCommand) Init(args []string) error {
    62  	switch len(args) {
    63  	case 0:
    64  		return errors.New("no action ID specified")
    65  	case 1:
    66  		c.requestedId = args[0]
    67  		return nil
    68  	default:
    69  		return cmd.CheckEmpty(args[1:])
    70  	}
    71  }
    72  
    73  // Run issues the API call to get Actions by ID.
    74  func (c *showOutputCommand) Run(ctx *cmd.Context) error {
    75  	// Check whether units were left off our time string.
    76  	r := regexp.MustCompile("[a-zA-Z]")
    77  	matches := r.FindStringSubmatch(c.wait[len(c.wait)-1:])
    78  	// If any match, we have units.  Otherwise, we don't; assume seconds.
    79  	if len(matches) == 0 {
    80  		c.wait = c.wait + "s"
    81  	}
    82  
    83  	waitDur, err := time.ParseDuration(c.wait)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	api, err := c.NewActionAPIClient()
    89  	if err != nil {
    90  		return err
    91  	}
    92  	defer api.Close()
    93  
    94  	wait := time.NewTimer(0 * time.Second)
    95  
    96  	switch {
    97  	case waitDur.Nanoseconds() < 0:
    98  		// Negative duration signals immediate return.  All is well.
    99  	case waitDur.Nanoseconds() == 0:
   100  		// Zero duration signals indefinite wait.  Discard the tick.
   101  		wait = time.NewTimer(0 * time.Second)
   102  		_ = <-wait.C
   103  	default:
   104  		// Otherwise, start an ordinary timer.
   105  		wait = time.NewTimer(waitDur)
   106  	}
   107  
   108  	result, err := GetActionResult(api, c.requestedId, wait)
   109  	if err != nil {
   110  		return errors.Trace(err)
   111  	}
   112  
   113  	return c.out.Write(ctx, FormatActionResult(result))
   114  }
   115  
   116  // GetActionResult tries to repeatedly fetch an action until it is
   117  // in a completed state and then it returns it.
   118  // It waits for a maximum of "wait" before returning with the latest action status.
   119  func GetActionResult(api APIClient, requestedId string, wait *time.Timer) (params.ActionResult, error) {
   120  
   121  	// tick every two seconds, to delay the loop timer.
   122  	// TODO(fwereade): 2016-03-17 lp:1558657
   123  	tick := time.NewTimer(2 * time.Second)
   124  
   125  	return timerLoop(api, requestedId, wait, tick)
   126  }
   127  
   128  // timerLoop loops indefinitely to query the given API, until "wait" times
   129  // out, using the "tick" timer to delay the API queries.  It writes the
   130  // result to the given output.
   131  func timerLoop(api APIClient, requestedId string, wait, tick *time.Timer) (params.ActionResult, error) {
   132  	var (
   133  		result params.ActionResult
   134  		err    error
   135  	)
   136  
   137  	// Loop over results until we get "failed" or "completed".  Wait for
   138  	// timer, and reset it each time.
   139  	for {
   140  		result, err = fetchResult(api, requestedId)
   141  		if err != nil {
   142  			return result, err
   143  		}
   144  
   145  		// Whether or not we're waiting for a result, if a completed
   146  		// result arrives, we're done.
   147  		switch result.Status {
   148  		case params.ActionRunning, params.ActionPending:
   149  		default:
   150  			return result, nil
   151  		}
   152  
   153  		// Block until a tick happens, or the timeout arrives.
   154  		select {
   155  		case _ = <-wait.C:
   156  			return result, nil
   157  
   158  		case _ = <-tick.C:
   159  			tick.Reset(2 * time.Second)
   160  		}
   161  	}
   162  }
   163  
   164  // fetchResult queries the given API for the given Action ID prefix, and
   165  // makes sure the results are acceptable, returning an error if they are not.
   166  func fetchResult(api APIClient, requestedId string) (params.ActionResult, error) {
   167  	none := params.ActionResult{}
   168  
   169  	actionTag, err := getActionTagByPrefix(api, requestedId)
   170  	if err != nil {
   171  		return none, err
   172  	}
   173  
   174  	actions, err := api.Actions(params.Entities{
   175  		Entities: []params.Entity{{actionTag.String()}},
   176  	})
   177  	if err != nil {
   178  		return none, err
   179  	}
   180  	actionResults := actions.Results
   181  	numActionResults := len(actionResults)
   182  	if numActionResults == 0 {
   183  		return none, errors.Errorf("no results for action %s", requestedId)
   184  	}
   185  	if numActionResults != 1 {
   186  		return none, errors.Errorf("too many results for action %s", requestedId)
   187  	}
   188  
   189  	result := actionResults[0]
   190  	if result.Error != nil {
   191  		return none, result.Error
   192  	}
   193  
   194  	return result, nil
   195  }
   196  
   197  // FormatActionResult removes empty values from the given ActionResult and
   198  // inserts the remaining ones in a map[string]interface{} for cmd.Output to
   199  // write in an easy-to-read format.
   200  func FormatActionResult(result params.ActionResult) map[string]interface{} {
   201  	response := map[string]interface{}{"status": result.Status}
   202  	if result.Message != "" {
   203  		response["message"] = result.Message
   204  	}
   205  	if len(result.Output) != 0 {
   206  		response["results"] = result.Output
   207  	}
   208  
   209  	if result.Enqueued.IsZero() && result.Started.IsZero() && result.Completed.IsZero() {
   210  		return response
   211  	}
   212  
   213  	responseTiming := make(map[string]string)
   214  	for k, v := range map[string]time.Time{
   215  		"enqueued":  result.Enqueued,
   216  		"started":   result.Started,
   217  		"completed": result.Completed,
   218  	} {
   219  		if !v.IsZero() {
   220  			responseTiming[k] = v.String()
   221  		}
   222  	}
   223  	response["timing"] = responseTiming
   224  
   225  	return response
   226  }