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 }