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 }