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