github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/cmd/juju/action/showoperation.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package action 5 6 import ( 7 "time" 8 9 "github.com/juju/clock" 10 "github.com/juju/cmd/v3" 11 "github.com/juju/errors" 12 "github.com/juju/gnuflag" 13 "github.com/juju/names/v5" 14 15 actionapi "github.com/juju/juju/api/client/action" 16 jujucmd "github.com/juju/juju/cmd" 17 "github.com/juju/juju/cmd/modelcmd" 18 "github.com/juju/juju/rpc/params" 19 ) 20 21 func NewShowOperationCommand() cmd.Command { 22 return modelcmd.Wrap(&showOperationCommand{ 23 clock: clock.WallClock, 24 }) 25 } 26 27 // showOperationCommand fetches the results of an operation by ID. 28 type showOperationCommand struct { 29 ActionCommandBase 30 out cmd.Output 31 requestedID string 32 wait time.Duration 33 watch bool 34 utc bool 35 clock clock.Clock 36 } 37 38 const showOperationDoc = ` 39 Show the results returned by an operation with the given ID. 40 To block until the result is known completed or failed, use 41 the --wait option with a duration, as in --wait 5s or --wait 1h. 42 Use --watch to wait indefinitely. 43 44 The default behavior without --wait or --watch is to immediately check and return; 45 if the results are "pending" then only the available information will be 46 displayed. This is also the behavior when any negative time is given. 47 ` 48 49 const showOperationExamples = ` 50 juju show-operation 1 51 juju show-operation 1 --wait=2m 52 juju show-operation 1 --watch 53 ` 54 55 const defaultOperationWait = -1 * time.Second 56 57 // SetFlags implements Command. 58 func (c *showOperationCommand) SetFlags(f *gnuflag.FlagSet) { 59 c.ActionCommandBase.SetFlags(f) 60 defaultFormatter := "yaml" 61 c.out.AddFlags(f, defaultFormatter, map[string]cmd.Formatter{ 62 "yaml": cmd.FormatYaml, 63 "json": cmd.FormatJson, 64 }) 65 66 f.DurationVar(&c.wait, "wait", defaultOperationWait, "Wait for results") 67 f.BoolVar(&c.watch, "watch", false, "Wait indefinitely for results") 68 f.BoolVar(&c.utc, "utc", false, "Show times in UTC") 69 } 70 71 // Info implements Command. 72 func (c *showOperationCommand) Info() *cmd.Info { 73 return jujucmd.Info(&cmd.Info{ 74 Name: "show-operation", 75 Args: "<operation-id>", 76 Purpose: "Show results of an operation.", 77 Doc: showOperationDoc, 78 Examples: showOperationExamples, 79 SeeAlso: []string{ 80 "run", 81 "operations", 82 "show-task", 83 }, 84 }) 85 } 86 87 // Init implements Command. 88 func (c *showOperationCommand) Init(args []string) error { 89 if c.watch { 90 if c.wait != defaultOperationWait { 91 return errors.New("specify either --watch or --wait but not both") 92 } 93 // If we are watching the wait is 0 (indefinite). 94 c.wait = 0 * time.Second 95 } 96 switch len(args) { 97 case 0: 98 return errors.New("no operation ID specified") 99 case 1: 100 if !names.IsValidOperation(args[0]) { 101 return errors.NotValidf("operation ID %q", args[0]) 102 } 103 c.requestedID = args[0] 104 return nil 105 default: 106 return cmd.CheckEmpty(args[1:]) 107 } 108 } 109 110 // Run implements Command. 111 func (c *showOperationCommand) Run(ctx *cmd.Context) error { 112 api, err := c.NewActionAPIClient() 113 if err != nil { 114 return err 115 } 116 defer api.Close() 117 118 wait := c.clock.NewTimer(c.wait) 119 if c.wait.Nanoseconds() == 0 { 120 // Zero duration signals indefinite wait. Discard the tick. 121 <-wait.Chan() 122 } 123 124 var result actionapi.Operation 125 shouldWatch := c.wait.Nanoseconds() >= 0 126 if shouldWatch { 127 tick := c.clock.NewTimer(resultPollTime) 128 result, err = getOperationResult(api, c.requestedID, tick, wait) 129 } else { 130 result, err = fetchOperationResult(api, c.requestedID) 131 } 132 if err != nil { 133 return errors.Trace(err) 134 } 135 136 formatted := formatOperationResult(result, c.utc) 137 return c.out.Write(ctx, formatted) 138 } 139 140 // fetchOperationResult queries the given API for the given operation ID. 141 func fetchOperationResult(api APIClient, requestedId string) (actionapi.Operation, error) { 142 result, err := api.Operation(requestedId) 143 if err != nil { 144 return result, err 145 } 146 return result, nil 147 } 148 149 // getOperationResult tries to repeatedly fetch an operation until it is 150 // in a completed state and then it returns it. 151 // It waits for a maximum of "wait" before returning with the latest operation status. 152 func getOperationResult(api APIClient, requestedId string, tick, wait clock.Timer) (actionapi.Operation, error) { 153 return operationTimerLoop(api, requestedId, tick, wait) 154 } 155 156 // operationTimerLoop loops indefinitely to query the given API, until "wait" times 157 // out, using the "tick" timer to delay the API queries. It writes the 158 // result to the given output. 159 func operationTimerLoop(api APIClient, requestedId string, tick, wait clock.Timer) (actionapi.Operation, error) { 160 var ( 161 result actionapi.Operation 162 err error 163 ) 164 165 // Loop over results until we get "failed", "completed", or "cancelled. Wait for 166 // timer, and reset it each time. 167 for { 168 result, err = fetchOperationResult(api, requestedId) 169 if err != nil { 170 return result, err 171 } 172 173 // Whether or not we're waiting for a result, if a completed 174 // result arrives, we're done. 175 switch result.Status { 176 case params.ActionRunning, params.ActionPending: 177 default: 178 return result, nil 179 } 180 181 // Block until a tick happens, or the timeout arrives. 182 select { 183 case <-wait.Chan(): 184 switch result.Status { 185 case params.ActionRunning, params.ActionPending: 186 return result, errors.NewTimeout(err, "timeout reached") 187 default: 188 return result, nil 189 } 190 case <-tick.Chan(): 191 tick.Reset(resultPollTime) 192 } 193 } 194 }