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