diff --git a/cyral/client/client.go b/cyral/client/client.go index 9bd703c8..d19e1cb2 100644 --- a/cyral/client/client.go +++ b/cyral/client/client.go @@ -5,7 +5,7 @@ import ( "crypto/tls" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "os" "strconv" @@ -14,6 +14,9 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "golang.org/x/oauth2" cc "golang.org/x/oauth2/clientcredentials" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/oauth" ) const redactedString = "**********" @@ -30,7 +33,8 @@ const ( type Client struct { ControlPlane string TokenSource oauth2.TokenSource - client *http.Client + httpClient *http.Client + grpcClient grpc.ClientConnInterface } // New configures and returns a fully initialized Client. @@ -41,12 +45,13 @@ func New(clientID, clientSecret, controlPlane string, tlsSkipVerify bool) (*Clie if clientID == "" || clientSecret == "" || controlPlane == "" { return nil, fmt.Errorf("clientID, clientSecret and controlPlane must have non-empty values") } + tlsConfig := &tls.Config{ + InsecureSkipVerify: tlsSkipVerify, + } - client := &http.Client{ + httpClient := &http.Client{ Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: tlsSkipVerify, - }, + TLSClientConfig: tlsConfig, }, } @@ -59,12 +64,24 @@ func New(clientID, clientSecret, controlPlane string, tlsSkipVerify bool) (*Clie tokenSource := tokenConfig.TokenSource(ctx) tflog.Debug(ctx, fmt.Sprintf("TokenSource: %v", tokenSource)) + + grpcClient, err := grpc.NewClient( + fmt.Sprintf("dns:///%s", controlPlane), + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: tokenSource}), + ) + if err != nil { + // we don't really expect this to happen (even if the server is unreachable!). + return nil, fmt.Errorf("error creating grpc client: %v", err) + } + tflog.Debug(ctx, "End client.New") return &Client{ ControlPlane: controlPlane, TokenSource: tokenSource, - client: client, + httpClient: httpClient, + grpcClient: grpcClient, }, nil } @@ -110,7 +127,7 @@ func (c *Client) DoRequest(ctx context.Context, url, httpMethod string, resource } tflog.Debug(ctx, fmt.Sprintf("==> Executing %s", httpMethod)) - res, err := c.client.Do(req) + res, err := c.httpClient.Do(req) if err != nil { tflog.Debug(ctx, "=> End DoRequest - Error") return nil, fmt.Errorf("unable to execute request. Check the control plane address; err: %v", err) @@ -125,7 +142,7 @@ func (c *Client) DoRequest(ctx context.Context, url, httpMethod string, resource res.StatusCode) } - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) if err != nil { tflog.Debug(ctx, "=> End DoRequest - Error") return nil, NewHttpError( diff --git a/cyral/client/client_test.go b/cyral/client/client_test.go index 0e07d7e1..6c58b3cb 100644 --- a/cyral/client/client_test.go +++ b/cyral/client/client_test.go @@ -28,7 +28,7 @@ func TestNewClient_WhenTLSSkipVerifyIsEnabled_ThenInsecureSkipVerifyIsTrue(t *te } assert.Equal(t, controlPlane, client.ControlPlane) - assert.Equal(t, expectedClient, client.client) + assert.Equal(t, expectedClient, client.httpClient) } func TestNewClient_WhenTLSSkipVerifyIsDisabled_ThenInsecureSkipVerifyIsFalse(t *testing.T) { @@ -50,7 +50,7 @@ func TestNewClient_WhenTLSSkipVerifyIsDisabled_ThenInsecureSkipVerifyIsFalse(t * } assert.Equal(t, controlPlane, client.ControlPlane) - assert.Equal(t, expectedClient, client.client) + assert.Equal(t, expectedClient, client.httpClient) } func TestNewClient_WhenClientIDIsEmpty_ThenThrowError(t *testing.T) { diff --git a/cyral/core/generic_context_handler.go b/cyral/core/generic_context_handler.go new file mode 100644 index 00000000..77c20d13 --- /dev/null +++ b/cyral/core/generic_context_handler.go @@ -0,0 +1,173 @@ +package core + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" +) + +type ResourceMethod func(context.Context, *client.Client, *schema.ResourceData) error + +// GenericContextHandler can be used by resource implementations to ensure that +// the recommended best practices are consistently followed (e.g., handling of +// 404 errors, following a create/update with a get etc). The resource implementation +// needs to supply functions that implement the basic CRUD operations on the resource +// using gRPC or whatever else. Note that if REST APIs are used, it is recommended +// to use the DefaultContextHandler instead. +type GenericContextHandler struct { + ResourceName string + ResourceType resourcetype.ResourceType + Create ResourceMethod + Read ResourceMethod + Update ResourceMethod + Delete ResourceMethod +} + +type method struct { + method ResourceMethod + name string + errorHandler func(context.Context, *schema.ResourceData, error) error +} + +func (gch *GenericContextHandler) handleResourceNotFoundError( + ctx context.Context, rd *schema.ResourceData, err error, +) error { + var isNotFoundError bool + if status.Code(err) == codes.NotFound { + isNotFoundError = true + } else if httpError, ok := err.(*client.HttpError); ok && + httpError.StatusCode == http.StatusNotFound { + isNotFoundError = true + } + if isNotFoundError { + tflog.Debug( + ctx, + fmt.Sprintf( + "==> Resource %s not found, marking for recreation or deletion.", + gch.ResourceName, + ), + ) + rd.SetId("") + return nil + } + return err +} + +// CreateContext is used to create a resource instance. +func (gch *GenericContextHandler) CreateContext( + ctx context.Context, + rd *schema.ResourceData, + pd any, +) diag.Diagnostics { + c := pd.(*client.Client) + return gch.executeMethods( + ctx, c, rd, []method{ + { + method: gch.Create, + name: "create", + }, + { + method: gch.Read, + name: "read", + errorHandler: gch.handleResourceNotFoundError, + }, + }, + ) +} + +// UpdateContext is used to update a resource instance. +func (gch *GenericContextHandler) UpdateContext( + ctx context.Context, + rd *schema.ResourceData, + pd any, +) diag.Diagnostics { + c := pd.(*client.Client) + return gch.executeMethods( + ctx, c, rd, []method{ + { + method: gch.Update, + name: "update", + }, + { + method: gch.Read, + name: "read", + errorHandler: gch.handleResourceNotFoundError, + }, + }, + ) +} + +// ReadContext is used to read a resource instance. +func (gch *GenericContextHandler) ReadContext( + ctx context.Context, + rd *schema.ResourceData, + pd any, +) diag.Diagnostics { + c := pd.(*client.Client) + return gch.executeMethods( + ctx, c, rd, []method{ + { + method: gch.Read, + name: "read", + errorHandler: gch.handleResourceNotFoundError, + }, + }, + ) +} + +// DeleteContext is used to delete a resource instance. +func (gch *GenericContextHandler) DeleteContext( + ctx context.Context, + rd *schema.ResourceData, + pd any, +) diag.Diagnostics { + c := pd.(*client.Client) + return gch.executeMethods( + ctx, c, rd, []method{ + { + method: gch.Delete, + name: "delete", + errorHandler: gch.handleResourceNotFoundError, + }, + }, + ) +} + +func (gch *GenericContextHandler) executeMethods( + ctx context.Context, c *client.Client, rd *schema.ResourceData, methods []method, +) diag.Diagnostics { + for _, m := range methods { + tflog.Debug(ctx, fmt.Sprintf("resource %s: operation %s", gch.ResourceName, m.name)) + err := m.method(ctx, c, rd) + if err != nil { + tflog.Debug( + ctx, + fmt.Sprintf("resource %s: operation %s - error: %v", gch.ResourceName, m.name, err), + ) + if m.errorHandler != nil { + err = m.errorHandler(ctx, rd, err) + } + } + if err != nil { + return utils.CreateError( + fmt.Sprintf("error in operation %s on resource %s", m.name, gch.ResourceName), + err.Error(), + ) + } + tflog.Debug( + ctx, + fmt.Sprintf("resource %s: operation %s - success", gch.ResourceName, m.name), + ) + } + return nil +}