Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEVTOOLING-926: Optional stack trace recovery & logging #1516

Draft
wants to merge 15 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ provider "genesyscloud" {
- `access_token` (String) A string that the OAuth client uses to make requests. Can be set with the `GENESYSCLOUD_ACCESS_TOKEN` environment variable.
- `aws_region` (String) AWS region where org exists. e.g. us-east-1. Can be set with the `GENESYSCLOUD_REGION` environment variable.
- `gateway` (Block Set) (see [below for nested schema](#nestedblock--gateway))
- `log_stack_traces` (Boolean) If set to true the provider will log stack traces to a file instead of crashing, where possible. Can be set with the `GENESYSCLOUD_LOG_STACK_TRACES` environment variable.
- `log_stack_traces_file_path` (String) Specifies the file path for the stack trace logs. Can be set with the `GENESYSCLOUD_LOG_STACK_TRACES_FILE_PATH` environment variable. Default value is genesyscloud_stack_traces.log
- `oauthclient_id` (String) OAuthClient ID found on the OAuth page of Admin UI. Can be set with the `GENESYSCLOUD_OAUTHCLIENT_ID` environment variable.
- `oauthclient_secret` (String, Sensitive) OAuthClient secret found on the OAuth page of Admin UI. Can be set with the `GENESYSCLOUD_OAUTHCLIENT_SECRET` environment variable.
- `proxy` (Block Set, Max: 1) (see [below for nested schema](#nestedblock--proxy))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ func GenerateGrammarResource(
description string,
) string {
return fmt.Sprintf(`
resource "genesyscloud_architect_grammar" "%s" {
resource "%s" "%s" {
name = "%s"
description = "%s"
}
`, resourceLabel, name, description)
`, ResourceType, resourceLabel, name, description)
}
22 changes: 19 additions & 3 deletions genesyscloud/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"regexp"
"strings"
prl "terraform-provider-genesyscloud/genesyscloud/util/panic_recovery_logger"
"time"

"terraform-provider-genesyscloud/genesyscloud/platform"
Expand Down Expand Up @@ -106,7 +107,7 @@ func New(version string, providerResources map[string]*schema.Resource, provider
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("GENESYSCLOUD_SDK_DEBUG_FILE_PATH", "sdk_debug.log"),
Description: "Specifies the file path for the log file. Can be set with the `GENESYSCLOUD_SDK_DEBUG_FILE_PATH` environment variable. Default value is sdk_debug.log",
ValidateFunc: validation.StringDoesNotMatch(regexp.MustCompile("^(|\\s+)$"), "Invalid File path "),
ValidateFunc: validation.StringDoesNotMatch(regexp.MustCompile(`^(|\s+)$`), "Invalid File path "),
},
"token_pool_size": {
Type: schema.TypeInt,
Expand All @@ -115,6 +116,19 @@ func New(version string, providerResources map[string]*schema.Resource, provider
Description: "Max number of OAuth tokens in the token pool. Can be set with the `GENESYSCLOUD_TOKEN_POOL_SIZE` environment variable.",
ValidateFunc: validation.IntBetween(1, 20),
},
"log_stack_traces": {
Type: schema.TypeBool,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("GENESYSCLOUD_LOG_STACK_TRACES", false),
Description: "If set to true the provider will log stack traces to a file instead of crashing, where possible. Can be set with the `GENESYSCLOUD_LOG_STACK_TRACES` environment variable.",
},
"log_stack_traces_file_path": {
Type: schema.TypeString,
Optional: true,
Description: "Specifies the file path for the stack trace logs. Can be set with the `GENESYSCLOUD_LOG_STACK_TRACES_FILE_PATH` environment variable. Default value is genesyscloud_stack_traces.log",
DefaultFunc: schema.EnvDefaultFunc("GENESYSCLOUD_LOG_STACK_TRACES_FILE_PATH", "genesyscloud_stack_traces.log"),
ValidateDiagFunc: validateLogFilePath,
},
"gateway": {
Type: schema.TypeSet,
Optional: true,
Expand Down Expand Up @@ -271,6 +285,8 @@ func configure(version string) schema.ConfigureContextFunc {
}
orgDefaultCountryCode = *currentOrg.DefaultCountryCode

prl.InitPanicRecoveryLoggerInstance(data.Get("log_stack_traces").(bool), data.Get("log_stack_traces_file_path").(string))

return &ProviderMeta{
Version: version,
Platform: &platform,
Expand Down Expand Up @@ -397,7 +413,7 @@ func InitClientConfig(data *schema.ResourceData, version string, config *platfor
if err != nil {
log.Printf("WARNING: Unable to log RequestLogHook: %s", err)
}
log.Printf(jsonStr)
log.Println(jsonStr)
},
ResponseLogHook: func(response *http.Response) {
sdkDebugResponse := newSDKDebugResponse(response)
Expand All @@ -406,7 +422,7 @@ func InitClientConfig(data *schema.ResourceData, version string, config *platfor
if err != nil {
log.Printf("WARNING: Unable to log ResponseLogHook: %s", err)
}
log.Printf(jsonStr)
log.Println(jsonStr)
},
}

Expand Down
33 changes: 32 additions & 1 deletion genesyscloud/provider/provider_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package provider

import (
"fmt"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand All @@ -21,7 +24,7 @@ func GetProviderFactories(providerResources map[string]*schema.Resource, provide
}
}

// Verify default division is home division
// TestDefaultHomeDivision Verify default division is home division
func TestDefaultHomeDivision(resource string) resource.TestCheckFunc {
return func(state *terraform.State) error {
homeDivID, err := getHomeDivisionID()
Expand All @@ -44,6 +47,34 @@ func TestDefaultHomeDivision(resource string) resource.TestCheckFunc {
}
}

// validateLogFilePath validates that a log file path is not empty, does
// not contain any whitespaces, and that it ends with ".log"
// (Keeping this inside validators causes import cycle)
func validateLogFilePath(filepath any, _ cty.Path) (err diag.Diagnostics) {
defer func() {
if err != nil {
err = diag.Errorf("validateLogFilePath failed: %v", err)
}
}()

val, ok := filepath.(string)
if !ok {
return diag.Errorf("expected type of %v to be string, got %T", filepath, filepath)
}

// Check if the string is empty or contains any whitespace
if val == "" || strings.ContainsAny(val, " \t\n\r") {
return diag.Errorf("filepath must not be empty or contain whitespace, got: %s", val)
}

// Check if the file ends with .log
if !strings.HasSuffix(val, ".log") {
return diag.Errorf("%s must end with .log extension", val)
}

return err
}

func GetOrgDefaultCountryCode() string {
return orgDefaultCountryCode
}
66 changes: 66 additions & 0 deletions genesyscloud/provider/provider_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package provider

import (
"testing"
)

func TestUnitValidateLogFilePath(t *testing.T) {
testCases := []struct {
name string
path interface{}
expectError bool
}{
{
name: "Valid log file path",
path: "logs/application.log",
expectError: false,
},
{
name: "Empty path",
path: "",
expectError: true,
},
{
name: "Non-string value",
path: 123,
expectError: true,
},
{
name: "Relative path with directory",
path: "./logs/currentTestCase.log",
expectError: false,
},
{
name: "Absolute path",
path: "/var/logs/currentTestCase.log",
expectError: false,
},
{
name: "Path with spaces",
path: "logs/current TestCase.log",
expectError: true,
},
{
name: "Incorrect file extension (.tfstate)",
path: "terraform.tfstate",
expectError: true,
},
{
name: "Incorrect file extension (.go)",
path: "main.go",
expectError: true,
},
}

for _, currentTestCase := range testCases {
t.Run(currentTestCase.name, func(t *testing.T) {
diagErr := validateLogFilePath(currentTestCase.path, nil)
if currentTestCase.expectError && diagErr == nil {
t.Fatalf("Expected an error, but got none")
}
if !currentTestCase.expectError && diagErr != nil {
t.Fatalf("Unexpected error: %v", diagErr)
}
})
}
}
34 changes: 30 additions & 4 deletions genesyscloud/provider/sdk_client_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"log"
"sync"
resourceExporter "terraform-provider-genesyscloud/genesyscloud/resource_exporter"
"terraform-provider-genesyscloud/genesyscloud/util/constants"
prl "terraform-provider-genesyscloud/genesyscloud/util/panic_recovery_logger"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -99,19 +101,43 @@ type GetAllConfigFunc func(context.Context, *platformclientv2.Configuration) (re
type GetCustomConfigFunc func(context.Context, *platformclientv2.Configuration) (resourceExporter.ResourceIDMetaMap, *resourceExporter.DependencyResource, diag.Diagnostics)

func CreateWithPooledClient(method resContextFunc) schema.CreateContextFunc {
return schema.CreateContextFunc(runWithPooledClient(method))
methodWrappedWithRecover := wrapWithRecover(method, constants.Create)
return schema.CreateContextFunc(runWithPooledClient(methodWrappedWithRecover))
}

func ReadWithPooledClient(method resContextFunc) schema.ReadContextFunc {
return schema.ReadContextFunc(runWithPooledClient(method))
methodWrappedWithRecover := wrapWithRecover(method, constants.Read)
return schema.ReadContextFunc(runWithPooledClient(methodWrappedWithRecover))
}

func UpdateWithPooledClient(method resContextFunc) schema.UpdateContextFunc {
return schema.UpdateContextFunc(runWithPooledClient(method))
methodWrappedWithRecover := wrapWithRecover(method, constants.Update)
return schema.UpdateContextFunc(runWithPooledClient(methodWrappedWithRecover))
}

func DeleteWithPooledClient(method resContextFunc) schema.DeleteContextFunc {
return schema.DeleteContextFunc(runWithPooledClient(method))
methodWrappedWithRecover := wrapWithRecover(method, constants.Delete)
return schema.DeleteContextFunc(runWithPooledClient(methodWrappedWithRecover))
}

func wrapWithRecover(method resContextFunc, operation constants.CRUDOperation) resContextFunc {
return func(ctx context.Context, r *schema.ResourceData, meta any) (diagErr diag.Diagnostics) {
panicRecoverLogger := prl.GetPanicRecoveryLoggerInstance()
if !panicRecoverLogger.LoggerEnabled {
return method(ctx, r, meta)
}

defer func() {
if r := recover(); r != nil {
err := panicRecoverLogger.HandleRecovery(r, operation)
if err != nil {
diagErr = diag.FromErr(err)
}
}
}()

return method(ctx, r, meta)
}
}

// Inject a pooled SDK client connection into a resource method's meta argument
Expand Down
4 changes: 2 additions & 2 deletions genesyscloud/tfexporter/genesyscloud_resource_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,7 @@ func (g *GenesysCloudResourceExporter) getResourcesForType(resType string, schem
// This calls into the resource's ReadContext method which
// will block until it can acquire a pooled client config object.
ctyType := res.CoreConfigSchema().ImpliedType()
instanceState, err := getResourceState(ctx, res, id, resMeta, meta, exportComputed)
instanceState, err := getResourceState(ctx, res, id, resMeta, meta)

if err != nil {
log.Printf("Error while fetching read context type %s and instance %s : %v", resType, id, err)
Expand Down Expand Up @@ -1149,7 +1149,7 @@ func (g *GenesysCloudResourceExporter) getResourcesForType(resType string, schem
}
}

func getResourceState(ctx context.Context, resource *schema.Resource, resID string, resMeta *resourceExporter.ResourceMeta, meta interface{}, exportComputed bool) (*terraform.InstanceState, diag.Diagnostics) {
func getResourceState(ctx context.Context, resource *schema.Resource, resID string, resMeta *resourceExporter.ResourceMeta, meta interface{}) (*terraform.InstanceState, diag.Diagnostics) {
// If defined, pass the full ID through the import method to generate a readable state
instanceState := &terraform.InstanceState{ID: resMeta.IdPrefix + resID}
if resource.Importer != nil && resource.Importer.StateContext != nil {
Expand Down
4 changes: 2 additions & 2 deletions genesyscloud/tfexporter/resource_genesyscloud_tf_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

registrar "terraform-provider-genesyscloud/genesyscloud/resource_register"

"terraform-provider-genesyscloud/genesyscloud/tfexporter_state"
tfExporterState "terraform-provider-genesyscloud/genesyscloud/tfexporter_state"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -159,7 +159,7 @@ func ResourceTfExport() *schema.Resource {
}

func createTfExport(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
tfexporter_state.ActivateExporterState()
tfExporterState.ActivateExporterState()

if _, ok := d.GetOk("include_filter_resources"); ok {
gre, _ := NewGenesysCloudResourceExporter(ctx, d, meta, IncludeResources)
Expand Down
24 changes: 24 additions & 0 deletions genesyscloud/util/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,27 @@ func ConsistencyChecks() int {

return defaultChecks
}

type CRUDOperation int

const (
Create CRUDOperation = iota
Read
Update
Delete
)

func (o CRUDOperation) String() string {
switch o {
case Create:
return "Create"
case Read:
return "Read"
case Update:
return "Update"
case Delete:
return "Delete"
default:
return "Unknown"
}
}
Loading
Loading