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  }