Skip to content

Commit

Permalink
Implement PolicyV2 resource using gRPC. (#584)
Browse files Browse the repository at this point in the history
* bump dependencies

* Implement generic context handler

* implement policy v2 resource using gRPC

* minor changes

* use generated protobuf code from label main on buf

* address review comments

* rename GenericContextHandler to ContextHandler

* rename DefaultContextHandler as HTTPContextHandler
  • Loading branch information
yoursnerdly authored Dec 9, 2024
1 parent 39635f2 commit b8d78a0
Show file tree
Hide file tree
Showing 37 changed files with 494 additions and 230 deletions.
39 changes: 30 additions & 9 deletions cyral/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"io"
"net/http"
"os"
"strconv"
Expand All @@ -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 = "**********"
Expand All @@ -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.
Expand All @@ -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,
},
}

Expand All @@ -59,15 +64,31 @@ 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
}

func (c *Client) GRPCClient() grpc.ClientConnInterface {
return c.grpcClient
}

// DoRequest calls the httpMethod informed and delivers the resourceData as a payload,
// filling the response parameter (if not nil) with the response body.
func (c *Client) DoRequest(ctx context.Context, url, httpMethod string, resourceData interface{}) ([]byte, error) {
Expand Down Expand Up @@ -110,7 +131,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)
Expand All @@ -125,7 +146,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(
Expand Down
4 changes: 2 additions & 2 deletions cyral/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
173 changes: 173 additions & 0 deletions cyral/core/context_handler.go
Original file line number Diff line number Diff line change
@@ -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

// ContextHandler 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 HTTPContextHandler instead.
type ContextHandler 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 *ContextHandler) 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 *ContextHandler) 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 *ContextHandler) 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 *ContextHandler) 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 *ContextHandler) 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 *ContextHandler) 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// Implementation of a default context handler that can be used by all resources.
// HTTPContextHandler facilitates easy resource and data source implementation
// for any resource that is accessed and modified using an HTTP/REST API.
//
// 1. `SchemaWriterFactoryGetMethod“ must be provided.
// 2. In case `SchemaWriterFactoryPostMethod“ is not provided,
Expand All @@ -31,7 +32,7 @@ import (
// - PUT: https://<CP>/<apiVersion>/<featureName>/<id>
// - PATCH: https://<CP>/<apiVersion>/<featureName>/<id>
// - DELETE: https://<CP>/<apiVersion>/<featureName>/<id>
type DefaultContextHandler struct {
type HTTPContextHandler struct {
ResourceName string
ResourceType rt.ResourceType
SchemaReaderFactory SchemaReaderFactoryFunc
Expand All @@ -55,7 +56,7 @@ func DefaultSchemaWriterFactory(d *schema.ResourceData) SchemaWriter {
return &IDBasedResponse{}
}

func (dch DefaultContextHandler) defaultOperationHandler(
func (dch HTTPContextHandler) defaultOperationHandler(
operationType ot.OperationType,
httpMethod string,
schemaReaderFactory SchemaReaderFactoryFunc,
Expand Down Expand Up @@ -92,11 +93,11 @@ func (dch DefaultContextHandler) defaultOperationHandler(
return result
}

func (dch DefaultContextHandler) CreateContext() schema.CreateContextFunc {
func (dch HTTPContextHandler) CreateContext() schema.CreateContextFunc {
return dch.CreateContextCustomErrorHandling(&IgnoreHttpNotFound{ResName: dch.ResourceName}, nil)
}

func (dch DefaultContextHandler) CreateContextCustomErrorHandling(getErrorHandler RequestErrorHandler,
func (dch HTTPContextHandler) CreateContextCustomErrorHandling(getErrorHandler RequestErrorHandler,
postErrorHandler RequestErrorHandler) schema.CreateContextFunc {
// By default, assumes that if no SchemaWriterFactoryPostMethod is provided,
// the POST api will return an ID
Expand All @@ -110,21 +111,21 @@ func (dch DefaultContextHandler) CreateContextCustomErrorHandling(getErrorHandle
)
}

func (dch DefaultContextHandler) ReadContext() schema.ReadContextFunc {
func (dch HTTPContextHandler) ReadContext() schema.ReadContextFunc {
return dch.ReadContextCustomErrorHandling(&IgnoreHttpNotFound{ResName: dch.ResourceName})
}

func (dch DefaultContextHandler) ReadContextCustomErrorHandling(getErrorHandler RequestErrorHandler) schema.ReadContextFunc {
func (dch HTTPContextHandler) ReadContextCustomErrorHandling(getErrorHandler RequestErrorHandler) schema.ReadContextFunc {
return ReadResource(
dch.defaultOperationHandler(ot.Read, http.MethodGet, nil, dch.SchemaWriterFactoryGetMethod, getErrorHandler),
)
}

func (dch DefaultContextHandler) UpdateContext() schema.UpdateContextFunc {
func (dch HTTPContextHandler) UpdateContext() schema.UpdateContextFunc {
return dch.UpdateContextCustomErrorHandling(&IgnoreHttpNotFound{ResName: dch.ResourceName}, nil)
}

func (dch DefaultContextHandler) UpdateContextCustomErrorHandling(getErrorHandler RequestErrorHandler,
func (dch HTTPContextHandler) UpdateContextCustomErrorHandling(getErrorHandler RequestErrorHandler,
putErrorHandler RequestErrorHandler) schema.UpdateContextFunc {
updateMethod := http.MethodPut
if dch.UpdateMethod != "" {
Expand All @@ -136,10 +137,10 @@ func (dch DefaultContextHandler) UpdateContextCustomErrorHandling(getErrorHandle
)
}

func (dch DefaultContextHandler) DeleteContext() schema.DeleteContextFunc {
func (dch HTTPContextHandler) DeleteContext() schema.DeleteContextFunc {
return dch.DeleteContextCustomErrorHandling(&IgnoreHttpNotFound{ResName: dch.ResourceName})
}

func (dch DefaultContextHandler) DeleteContextCustomErrorHandling(deleteErrorHandler RequestErrorHandler) schema.DeleteContextFunc {
func (dch HTTPContextHandler) DeleteContextCustomErrorHandling(deleteErrorHandler RequestErrorHandler) schema.DeleteContextFunc {
return DeleteResource(dch.defaultOperationHandler(ot.Delete, http.MethodDelete, nil, nil, deleteErrorHandler))
}
2 changes: 1 addition & 1 deletion cyral/internal/datalabel/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var readUpdateDeleteURLFactory = func(d *schema.ResourceData, c *client.Client)
}

func resourceSchema() *schema.Resource {
contextHandler := core.DefaultContextHandler{
contextHandler := core.HTTPContextHandler{
ResourceName: resourceName,
ResourceType: resourcetype.Resource,
SchemaReaderFactory: func() core.SchemaReader { return &DataLabel{} },
Expand Down
2 changes: 1 addition & 1 deletion cyral/internal/deprecated/policy/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var resourceContextHandler = core.DefaultContextHandler{
var resourceContextHandler = core.HTTPContextHandler{
ResourceName: resourceName,
ResourceType: resourcetype.Resource,
SchemaReaderFactory: func() core.SchemaReader { return &Policy{} },
Expand Down
2 changes: 1 addition & 1 deletion cyral/internal/deprecated/policy/rule/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

var resourceContextHandler = core.DefaultContextHandler{
var resourceContextHandler = core.HTTPContextHandler{
ResourceName: resourceName,
ResourceType: resourcetype.Resource,
SchemaReaderFactory: func() core.SchemaReader { return &PolicyRule{} },
Expand Down
Loading

0 comments on commit b8d78a0

Please sign in to comment.