Adjust error messages for run scripts API (#13618)

This commit is contained in:
gillespi314 2023-08-31 10:37:51 -05:00 committed by GitHub
parent 8a796ff5bd
commit 72f2f7ac12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 46 additions and 21 deletions

View file

@ -62,32 +62,32 @@ func (svc *Service) RunHostScript(ctx context.Context, request *fleet.HostScript
// look for the script length in bytes first, as rune counting a huge string
// can be expensive.
if len(request.ScriptContents) > utf8.UTFMax*maxScriptRuneLen {
return nil, fleet.NewInvalidArgumentError("script_contents", "Error: Script is too large. It's limited to 10,000 characters (approximately 125 lines).")
return nil, fleet.NewInvalidArgumentError("script_contents", "Script is too large. It's limited to 10,000 characters (approximately 125 lines).")
}
// now that we know that the script is at most 4*maxScriptRuneLen bytes long,
// we can safely count the runes for a precise check.
if utf8.RuneCountInString(request.ScriptContents) > maxScriptRuneLen {
return nil, fleet.NewInvalidArgumentError("script_contents", "Error: Script is too large. It's limited to 10,000 characters (approximately 125 lines).")
return nil, fleet.NewInvalidArgumentError("script_contents", "Script is too large. It's limited to 10,000 characters (approximately 125 lines).")
}
// script must be a "text file", but that's not so simple to validate, so we
// assume that if it is valid utf8 encoding, it is a text file (binary files
// will often have invalid utf8 byte sequences).
if !utf8.ValidString(request.ScriptContents) {
return nil, fleet.NewInvalidArgumentError("script_contents", "Error: Wrong data format. Only plain text allowed.")
return nil, fleet.NewInvalidArgumentError("script_contents", "Wrong data format. Only plain text allowed.")
}
if strings.HasPrefix(request.ScriptContents, "#!") {
// read the first line in a portable way
s := bufio.NewScanner(strings.NewReader(request.ScriptContents))
// if a hashbang is present, it can only be `/bin/sh` for now
if s.Scan() && !scriptHashbangValidation.MatchString(s.Text()) {
return nil, fleet.NewInvalidArgumentError("script_contents", `Error: Interpreter not supported. Bash scripts must run in "#!/bin/sh”.`)
return nil, fleet.NewInvalidArgumentError("script_contents", `Interpreter not supported. Bash scripts must run in "#!/bin/sh”.`)
}
}
// host must be online
if host.Status(time.Now()) != fleet.StatusOnline {
return nil, fleet.NewInvalidArgumentError("host_id", "Error: Script can't run on offline host.")
return nil, fleet.NewInvalidArgumentError("host_id", "Script can't run on offline host.")
}
pending, err := svc.ds.ListPendingHostScriptExecutions(ctx, request.HostID, maxPendingScriptAge)
@ -96,7 +96,7 @@ func (svc *Service) RunHostScript(ctx context.Context, request *fleet.HostScript
}
if len(pending) > 0 {
return nil, fleet.NewInvalidArgumentError(
"script_contents", "Error: A script is already running on this host. Please wait about 1 minute to let it finish.",
"script_contents", "A script is already running on this host. Please wait about 1 minute to let it finish.",
).WithStatus(http.StatusConflict)
}

View file

@ -1137,11 +1137,13 @@ func (hsr HostScriptResult) AuthzType() string {
func (hsr HostScriptResult) UserMessage(hostTimeout bool) string {
switch {
case hostTimeout:
return "Error: Fleet hasn't heard from the host in over 1 minute because it went offline. Run the script again when the host comes back online."
return "Fleet hasn't heard from the host in over 1 minute because it went offline. Run the script again when the host comes back online."
case !hostTimeout && time.Since(hsr.CreatedAt) > time.Minute:
return "Error: Fleet hasn't heard from the host in over 1 minute because it went offline. Run the script again when the host comes back online."
return "Fleet hasn't heard from the host in over 1 minute because it went offline. Run the script again when the host comes back online."
case hsr.ExitCode.Int64 == -1:
return "Error: Timeout. Fleet stopped the script after 30 seconds to protect host performance."
return "Timeout. Fleet stopped the script after 30 seconds to protect host performance."
case hsr.ExitCode.Int64 == -2:
return "Scripts are disabled for this host. To run scripts, deploy a Fleet installer with scripts enabled."
case !hsr.ExitCode.Valid:
return "Script is running. To see if the script finished, close this modal and open it again."
}

View file

@ -3774,6 +3774,7 @@ func (s *integrationEnterpriseTestSuite) TestRunHostScript() {
ctx, cancel := context.WithTimeout(ctx, testRunScriptWaitForResult)
defer cancel()
resultsCh := make(chan *fleet.HostScriptResultPayload, 1)
go func() {
for range time.Tick(300 * time.Millisecond) {
pending, err := s.ds.ListPendingHostScriptExecutions(ctx, host.ID, 10*time.Second)
@ -3782,22 +3783,28 @@ func (s *integrationEnterpriseTestSuite) TestRunHostScript() {
return
}
if len(pending) > 0 {
// ignoring errors in this goroutine, the HTTP request below will fail if this fails
err = s.ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
HostID: host.ID,
ExecutionID: pending[0].ExecutionID,
Output: "ok",
Runtime: 1,
ExitCode: 0,
})
if err != nil {
t.Log(err)
select {
case <-ctx.Done():
return
case r := <-resultsCh:
r.ExecutionID = pending[0].ExecutionID
// ignoring errors in this goroutine, the HTTP request below will fail if this fails
err = s.ds.SetHostScriptExecutionResult(ctx, r)
if err != nil {
t.Log(err)
}
}
return
}
}
}()
// simulate a successful script result
resultsCh <- &fleet.HostScriptResultPayload{
HostID: host.ID,
Output: "ok",
Runtime: 1,
ExitCode: 0,
}
runSyncResp = runScriptSyncResponse{}
s.DoJSON("POST", "/api/latest/fleet/scripts/run/sync", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"}, http.StatusOK, &runSyncResp)
require.Equal(t, host.ID, runSyncResp.HostID)
@ -3806,7 +3813,23 @@ func (s *integrationEnterpriseTestSuite) TestRunHostScript() {
require.True(t, runSyncResp.ExitCode.Valid)
require.Equal(t, int64(0), runSyncResp.ExitCode.Int64)
require.False(t, runSyncResp.HostTimeout)
require.Empty(t, runSyncResp.Message)
// simulate a scripts disabled result
resultsCh <- &fleet.HostScriptResultPayload{
HostID: host.ID,
Output: "",
Runtime: 0,
ExitCode: -2,
}
runSyncResp = runScriptSyncResponse{}
s.DoJSON("POST", "/api/latest/fleet/scripts/run/sync", fleet.HostScriptRequestPayload{HostID: host.ID, ScriptContents: "echo"}, http.StatusOK, &runSyncResp)
require.Equal(t, host.ID, runSyncResp.HostID)
require.NotEmpty(t, runSyncResp.ExecutionID)
require.Empty(t, runSyncResp.Output)
require.True(t, runSyncResp.ExitCode.Valid)
require.Equal(t, int64(-2), runSyncResp.ExitCode.Int64)
require.False(t, runSyncResp.HostTimeout)
require.Contains(t, runSyncResp.Message, "Scripts are disabled")
// make the host "offline"
err = s.ds.MarkHostsSeen(ctx, []uint{host.ID}, time.Now().Add(-time.Hour))