-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #26694 from hashicorp/jbardin/plugin-panics
Handle panics in plugins
- Loading branch information
Showing
7 changed files
with
299 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package logging | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func TestPanicRecorder(t *testing.T) { | ||
rec := panics.registerPlugin("test") | ||
|
||
output := []string{ | ||
"panic: test", | ||
" stack info", | ||
} | ||
|
||
for _, line := range output { | ||
rec(line) | ||
} | ||
|
||
expected := fmt.Sprintf(pluginPanicOutput, "test", strings.Join(output, "\n")) | ||
|
||
res := PluginPanics() | ||
if len(res) == 0 { | ||
t.Fatal("no output") | ||
} | ||
|
||
if res[0] != expected { | ||
t.Fatalf("expected: %q\ngot: %q", expected, res[0]) | ||
} | ||
} | ||
|
||
func TestPanicLimit(t *testing.T) { | ||
rec := panics.registerPlugin("test") | ||
|
||
rec("panic: test") | ||
|
||
for i := 0; i < 200; i++ { | ||
rec(fmt.Sprintf("LINE: %d", i)) | ||
} | ||
|
||
res := PluginPanics() | ||
// take the extra content into account | ||
max := strings.Count(pluginPanicOutput, "\n") + panics.maxLines | ||
for _, out := range res { | ||
found := strings.Count(out, "\n") | ||
if found > max { | ||
t.Fatalf("expected no more than %d lines, got: %d", max, found) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package plugin | ||
|
||
import ( | ||
"fmt" | ||
"path" | ||
"runtime" | ||
|
||
"github.com/hashicorp/terraform/tfdiags" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
) | ||
|
||
// grpcErr extracts some known error types and formats them into better | ||
// representations for core. This must only be called from plugin methods. | ||
// Since we don't use RPC status errors for the plugin protocol, these do not | ||
// contain any useful details, and we can return some text that at least | ||
// indicates the plugin call and possible error condition. | ||
func grpcErr(err error) (diags tfdiags.Diagnostics) { | ||
if err == nil { | ||
return | ||
} | ||
|
||
// extract the method name from the caller. | ||
pc, _, _, ok := runtime.Caller(1) | ||
if !ok { | ||
logger.Error("unknown grpc call", "error", err) | ||
return diags.Append(err) | ||
} | ||
|
||
f := runtime.FuncForPC(pc) | ||
|
||
// Function names will contain the full import path. Take the last | ||
// segment, which will let users know which method was being called. | ||
_, requestName := path.Split(f.Name()) | ||
|
||
// Here we can at least correlate the error in the logs to a particular binary. | ||
logger.Error(requestName, "error", err) | ||
|
||
// TODO: while this expands the error codes into somewhat better messages, | ||
// this still does not easily link the error to an actual user-recognizable | ||
// plugin. The grpc plugin does not know its configured name, and the | ||
// errors are in a list of diagnostics, making it hard for the caller to | ||
// annotate the returned errors. | ||
switch status.Code(err) { | ||
case codes.Unavailable: | ||
// This case is when the plugin has stopped running for some reason, | ||
// and is usually the result of a crash. | ||
diags = diags.Append(tfdiags.Sourceless( | ||
tfdiags.Error, | ||
"Plugin did not respond", | ||
fmt.Sprintf("The plugin encountered an error, and failed to respond to the %s call. "+ | ||
"The plugin logs may contain more details.", requestName), | ||
)) | ||
case codes.Canceled: | ||
diags = diags.Append(tfdiags.Sourceless( | ||
tfdiags.Error, | ||
"Request cancelled", | ||
fmt.Sprintf("The %s request was cancelled.", requestName), | ||
)) | ||
case codes.Unimplemented: | ||
diags = diags.Append(tfdiags.Sourceless( | ||
tfdiags.Error, | ||
"Unsupported plugin method", | ||
fmt.Sprintf("The %s method is not supported by this plugin.", requestName), | ||
)) | ||
default: | ||
diags = diags.Append(tfdiags.Sourceless( | ||
tfdiags.Error, | ||
"Plugin error", | ||
fmt.Sprintf("The plugin returned an unexpected error from %s: %v", requestName, err), | ||
)) | ||
} | ||
return | ||
} |
Oops, something went wrong.