Adding cli improvements for run-scripts (#18010)

This commit is contained in:
George Karr 2024-05-07 10:10:22 -05:00 committed by GitHub
parent 443bcb92a0
commit 0b9ec5e322
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 96 additions and 17 deletions

View file

@ -0,0 +1 @@
Added `--async` and `--quiet` to `fleetctl run-script` as well as allowing the contents of the script to be inline.

View file

@ -15,6 +15,14 @@ import (
"github.com/urfave/cli/v2"
)
// Helper function to convert a boolean to an integer
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
func runScriptCommand() *cli.Command {
return &cli.Command{
Name: "run-script",
@ -42,6 +50,16 @@ func runScriptCommand() *cli.Command {
Usage: `Available in Fleet Premium. ID of the team that the saved script belongs to. 0 targets hosts assigned to “No team” (default: 0).`,
Required: false,
},
&cli.BoolFlag{
Name: "async",
Usage: `Queue the script and don't wait for the return.`,
Required: false,
},
&cli.BoolFlag{
Name: "quiet",
Usage: `Suppress messages that are not the script output / error`,
Required: false,
},
configFlag(),
contextFlag(),
debugFlag(),
@ -61,15 +79,22 @@ func runScriptCommand() *cli.Command {
return errors.New(fleet.RunScriptScriptsDisabledGloballyErrMsg)
}
async := c.Bool("async")
quiet := c.Bool("quiet")
// Require 1 and only 1 of these 3 options
path := c.String("script-path")
name := c.String("script-name")
args := c.Args().Len()
if path == "" && name == "" {
return errors.New("One of '--script-path' or '--script-name' must be specified.")
notEmpty := boolToInt(path != "") + boolToInt(name != "") + boolToInt(args > 0)
if notEmpty < 1 {
return errors.New("One of '--script-path' or '--script-name' or '-- <contents>' must be specified.")
}
if path != "" && name != "" {
return errors.New("Only one of '--script-path' or '--script-name' is allowed.")
if notEmpty > 1 {
return errors.New("Only one of '--script-path' or '--script-name' or '-- <contents>' is allowed.")
}
if path != "" {
@ -99,10 +124,17 @@ func runScriptCommand() *cli.Command {
}
var b []byte
if path != "" {
b, err = os.ReadFile(path)
if err != nil {
return err
if path != "" || args > 0 {
if path != "" {
b, err = os.ReadFile(path)
if err != nil {
return err
}
}
if args > 0 {
commandString := strings.Join(c.Args().Slice(), " ")
b = []byte(commandString)
}
// validate script contents with isSavedScript flag set to false so that we check
@ -115,7 +147,21 @@ func runScriptCommand() *cli.Command {
}
}
fmt.Println("\nScript is running. Please wait for it to finish...")
if async {
res, err := client.RunHostScriptAsync(h.ID, b, name, c.Uint("team"))
if err != nil {
if strings.Contains(err.Error(), `Only one of 'script_contents' or 'team_id' is allowed`) {
return errors.New("Only one of '--script-path' or '--team' is allowed.")
}
return err
}
fmt.Fprintf(c.App.Writer, "%s\n", res.ExecutionID)
return nil
}
if !quiet {
fmt.Println("\nScript is running. Please wait for it to finish...")
}
res, err := client.RunHostScriptSync(h.ID, b, name, c.Uint("team"))
if err != nil {
@ -125,8 +171,12 @@ func runScriptCommand() *cli.Command {
return err
}
if err := renderScriptResult(c, res); err != nil {
return err
if !quiet {
if err := renderScriptResult(c, res); err != nil {
return err
}
} else {
fmt.Fprintf(c.App.Writer, "%s", res.Output)
}
return nil

View file

@ -70,6 +70,9 @@ hello world
-------------------------------------------------------------------------------------
`
expectedQuietOutputSuccess := `hello world
`
type testCase struct {
name string
scriptPath func() string
@ -77,6 +80,8 @@ hello world
teamID *uint
savedScriptContents func() ([]byte, error)
scriptResult *fleet.HostScriptResult
quiet bool
async bool
expectOutput string
expectErrMsg string
expectNotFound bool
@ -190,11 +195,11 @@ hello world
name: "script-path and script-name disallowed",
scriptPath: generateValidPath,
scriptName: "foo",
expectErrMsg: `Only one of '--script-path' or '--script-name' is allowed.`,
expectErrMsg: `Only one of '--script-path' or '--script-name' or '-- <contents>' is allowed.`,
},
{
name: "missing one of script-path and script-nqme",
expectErrMsg: `One of '--script-path' or '--script-name' must be specified.`,
expectErrMsg: `One of '--script-path' or '--script-name' or '-- <contents>' must be specified.`,
},
{
name: "script-path and team disallowed",
@ -227,6 +232,16 @@ hello world
},
expectOutput: expectedOutputSuccess,
},
{
name: "script quiet",
scriptPath: generateValidPath,
scriptResult: &fleet.HostScriptResult{
ExitCode: ptr.Int64(0),
Output: "hello world\n",
},
expectOutput: expectedQuietOutputSuccess,
quiet: true,
},
{
name: "script failed",
scriptPath: generateValidPath,
@ -392,6 +407,14 @@ Fleet records the last 10,000 characters to prevent downtime.
args = append(args, "--script-name", c.scriptName)
}
if c.quiet {
args = append(args, "--quiet")
}
if c.async {
args = append(args, "--async")
}
if c.teamID != nil {
args = append(args, "--team", fmt.Sprintf("%d", *c.teamID))
}

View file

@ -13,7 +13,15 @@ import (
func (c *Client) RunHostScriptSync(hostID uint, scriptContents []byte, scriptName string, teamID uint) (*fleet.HostScriptResult, error) {
verb, path := "POST", "/api/latest/fleet/scripts/run/sync"
return c.runHostScript(verb, path, hostID, scriptContents, scriptName, teamID, http.StatusOK)
}
func (c *Client) RunHostScriptAsync(hostID uint, scriptContents []byte, scriptName string, teamID uint) (*fleet.HostScriptResult, error) {
verb, path := "POST", "/api/latest/fleet/scripts/run"
return c.runHostScript(verb, path, hostID, scriptContents, scriptName, teamID, http.StatusAccepted)
}
func (c *Client) runHostScript(verb, path string, hostID uint, scriptContents []byte, scriptName string, teamID uint, successStatusCode int) (*fleet.HostScriptResult, error) {
req := fleet.HostScriptRequestPayload{
HostID: hostID,
ScriptName: scriptName,
@ -32,7 +40,7 @@ func (c *Client) RunHostScriptSync(hostID uint, scriptContents []byte, scriptNam
defer res.Body.Close()
switch res.StatusCode {
case http.StatusOK:
case successStatusCode:
b, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("reading %s %s response: %w", verb, path, err)
@ -45,13 +53,10 @@ func (c *Client) RunHostScriptSync(hostID uint, scriptContents []byte, scriptNam
if err != nil {
return nil, err
}
if strings.Contains(errMsg, fleet.RunScriptScriptsDisabledGloballyErrMsg) {
return nil, errors.New(fleet.RunScriptScriptsDisabledGloballyErrMsg)
}
return nil, errors.New(fleet.RunScriptForbiddenErrMsg)
case http.StatusPaymentRequired:
if teamID > 0 {
return nil, errors.New("Team id parameter requires Fleet Premium license.")