diff --git a/changes/16841-run-script-name-team-id b/changes/16841-run-script-name-team-id new file mode 100644 index 0000000000..11dbd0545a --- /dev/null +++ b/changes/16841-run-script-name-team-id @@ -0,0 +1,3 @@ +- Updated `fleetctl run-script` to include new `--team` and `--script-name` flags that enable users + to run live script on a host using a previously saved script referenced by the script name and team ID. +- Updated `POST /scripts/run/sync` API to receive new `script_name` and `team_id` parameters. diff --git a/cmd/fleetctl/scripts.go b/cmd/fleetctl/scripts.go index 942744680b..984c2dd555 100644 --- a/cmd/fleetctl/scripts.go +++ b/cmd/fleetctl/scripts.go @@ -38,7 +38,7 @@ func runScriptCommand() *cli.Command { Required: false, }, &cli.UintFlag{ - Name: "team-id", + Name: "team", 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, }, @@ -65,11 +65,11 @@ func runScriptCommand() *cli.Command { name := c.String("script-name") if path == "" && name == "" { - return errors.New("One of --script-path or --script-name must be specified.") + return errors.New("One of '--script-path' or '--script-name' must be specified.") } if path != "" && name != "" { - return errors.New("Only one of --script-path or --script-name is allowed.") + return errors.New("Only one of '--script-path' or '--script-name' is allowed.") } if path != "" { @@ -117,10 +117,10 @@ func runScriptCommand() *cli.Command { fmt.Println("\nScript is running. Please wait for it to finish...") - res, err := client.RunHostScriptSync(h.ID, b, name, c.Uint("team-id")) + res, err := client.RunHostScriptSync(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-id is allowed.") + return errors.New("Only one of '--script-path' or '--team' is allowed.") } return err } diff --git a/cmd/fleetctl/scripts_test.go b/cmd/fleetctl/scripts_test.go index dd91ff2856..aaeac54c2e 100644 --- a/cmd/fleetctl/scripts_test.go +++ b/cmd/fleetctl/scripts_test.go @@ -51,26 +51,37 @@ func TestRunScriptCommand(t *testing.T) { ds.IsExecutionPendingForHostFunc = func(ctx context.Context, hid uint, scriptID uint) ([]*uint, error) { return []*uint{}, nil } - ds.GetScriptContentsFunc = func(ctx context.Context, id uint) ([]byte, error) { - return []byte("echo hello world"), nil - } generateValidPath := func() string { return writeTmpScriptContents(t, "echo hello world", ".sh") } - maxChars := strings.Repeat("a", 10001) + exceedsMaxCharsUnsaved := strings.Repeat("a", fleet.UnsavedScriptMaxRuneLen+1) + exceedsMaxCharsSaved := strings.Repeat("a", fleet.SavedScriptMaxRuneLen+1) + + expectedOutputSuccess := ` +Exit code: 0 (Script ran successfully.) + +Output: + +------------------------------------------------------------------------------------- + +hello world + +------------------------------------------------------------------------------------- +` type testCase struct { - name string - scriptPath func() string - scriptName string - teamID *uint - scriptResult *fleet.HostScriptResult - expectOutput string - expectErrMsg string - expectNotFound bool - expectOffline bool - expectPending bool + name string + scriptPath func() string + scriptName string + teamID *uint + savedScriptContents func() ([]byte, error) + scriptResult *fleet.HostScriptResult + expectOutput string + expectErrMsg string + expectNotFound bool + expectOffline bool + expectPending bool } cases := []testCase{ @@ -99,25 +110,56 @@ func TestRunScriptCommand(t *testing.T) { { name: "script too long (unsaved)", scriptPath: func() string { - return writeTmpScriptContents(t, maxChars, ".sh") + return writeTmpScriptContents(t, exceedsMaxCharsUnsaved, ".sh") }, expectErrMsg: "Script is too large. Script referenced by '--script-path' is limited to 10,000 characters. To run larger script save it to Fleet and use '--script-name'.", }, + { + name: "script not too long (unsaved)", + scriptPath: func() string { + return writeTmpScriptContents(t, exceedsMaxCharsUnsaved[:fleet.UnsavedScriptMaxRuneLen], ".sh") + }, + scriptResult: &fleet.HostScriptResult{ + ExitCode: ptr.Int64(0), + Output: "hello world", + }, + expectOutput: expectedOutputSuccess, + }, + { + name: "script too long (saved)", + scriptName: "foo", + savedScriptContents: func() ([]byte, error) { + return []byte(exceedsMaxCharsSaved), nil + }, + expectErrMsg: "Script is too large. It's limited to 500,000 characters (approximately 10,000 lines).", + }, + { + name: "script not too long (saved)", + scriptName: "foo", + savedScriptContents: func() ([]byte, error) { + return []byte(exceedsMaxCharsUnsaved), nil + }, + scriptResult: &fleet.HostScriptResult{ + ExitCode: ptr.Int64(0), + Output: "hello world", + }, + expectOutput: expectedOutputSuccess, + }, { 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' 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' must be specified.`, }, { - name: "script-path and team-id disallowed", + name: "script-path and team disallowed", scriptPath: generateValidPath, teamID: ptr.Uint(1), - expectErrMsg: `Only one of --script-path or --team-id is allowed.`, + expectErrMsg: `Only one of '--script-path' or '--team' is allowed.`, }, { name: "script empty", @@ -142,17 +184,7 @@ func TestRunScriptCommand(t *testing.T) { ExitCode: ptr.Int64(0), Output: "hello world", }, - expectOutput: ` -Exit code: 0 (Script ran successfully.) - -Output: - -------------------------------------------------------------------------------------- - -hello world - -------------------------------------------------------------------------------------- -`, + expectOutput: expectedOutputSuccess, }, { name: "script failed", @@ -211,7 +243,7 @@ Error: Scripts are disabled for this host. To run scripts, deploy the fleetd age scriptPath: generateValidPath, scriptResult: &fleet.HostScriptResult{ ExitCode: ptr.Int64(0), - Output: maxChars, + Output: exceedsMaxCharsUnsaved, }, expectOutput: fmt.Sprintf(` Exit code: 0 (Script ran successfully.) @@ -225,7 +257,7 @@ Fleet records the last 10,000 characters to prevent downtime. %s ------------------------------------------------------------------------------------- -`, maxChars), +`, exceedsMaxCharsUnsaved), }, // TODO: this would take 5 minutes to run, we don't want that kind of slowdown in our test suite // but can be useful to have around for manual testing. @@ -287,6 +319,21 @@ Fleet records the last 10,000 characters to prevent downtime. return &fleet.AppConfig{ServerSettings: fleet.ServerSettings{ScriptsDisabled: false}}, nil } } + if c.savedScriptContents != nil { + ds.GetScriptContentsFunc = func(ctx context.Context, id uint) ([]byte, error) { + return c.savedScriptContents() + } + ds.ScriptFunc = func(ctx context.Context, id uint) (*fleet.Script, error) { + return &fleet.Script{ID: id, Name: "foo"}, nil + } + } else { + ds.GetScriptContentsFunc = func(ctx context.Context, id uint) ([]byte, error) { + return []byte("echo hello world"), nil + } + ds.ScriptFunc = func(ctx context.Context, id uint) (*fleet.Script, error) { + return &fleet.Script{ID: id, Name: "foo"}, nil + } + } } for _, c := range cases { @@ -305,7 +352,7 @@ Fleet records the last 10,000 characters to prevent downtime. } if c.teamID != nil { - args = append(args, "--team-id", fmt.Sprintf("%d", *c.teamID)) + args = append(args, "--team", fmt.Sprintf("%d", *c.teamID)) } b, err := runAppNoChecks(args)