Skip to content

Commit

Permalink
Implement generic context handler
Browse files Browse the repository at this point in the history
  • Loading branch information
yoursnerdly committed Dec 2, 2024
1 parent 0e58411 commit 1bab53a
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 11 deletions.
35 changes: 26 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,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
}

Expand Down Expand Up @@ -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)
Expand All @@ -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(
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/generic_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

// 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
}

0 comments on commit 1bab53a

Please sign in to comment.