mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Adding cli improvements for run-scripts (#18010)
This commit is contained in:
parent
443bcb92a0
commit
0b9ec5e322
4 changed files with 96 additions and 17 deletions
1
changes/18118-run-script-updates
Normal file
1
changes/18118-run-script-updates
Normal file
|
|
@ -0,0 +1 @@
|
|||
Added `--async` and `--quiet` to `fleetctl run-script` as well as allowing the contents of the script to be inline.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
|
|
|||
Loading…
Reference in a new issue