Skip to content

Commit

Permalink
Simplify core package (#474)
Browse files Browse the repository at this point in the history
* Rename ResourceData and ResponseData

* Add default response handling for id-based responses

* Stable version - few resources refactored

* Re-establish tests to normal

* Sync up docs with latest changes

* Migrate hcvault to new framework

* Simplify way further :)

* Migrate slack, teams and samlcertificate to new framework

* Bump go to 1.21

* Address review comments
  • Loading branch information
wcmjunior authored Dec 6, 2023
1 parent 328aac1 commit 16d284e
Show file tree
Hide file tree
Showing 121 changed files with 2,341 additions and 2,381 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
go-version: 1.21
- uses: actions/setup-python@v2
- uses: pre-commit/[email protected]
37 changes: 19 additions & 18 deletions cyral/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"

"github.com/hashicorp/terraform-plugin-log/tflog"
"golang.org/x/oauth2"
cc "golang.org/x/oauth2/clientcredentials"
)
Expand All @@ -35,7 +35,8 @@ type Client struct {

// New configures and returns a fully initialized Client.
func New(clientID, clientSecret, controlPlane string, tlsSkipVerify bool) (*Client, error) {
log.Printf("[DEBUG] Init client.New")
ctx := context.Background()
tflog.Debug(ctx, "Init client.New")

if clientID == "" || clientSecret == "" || controlPlane == "" {
return nil, fmt.Errorf("clientID, clientSecret and controlPlane must have non-empty values")
Expand All @@ -55,10 +56,10 @@ func New(clientID, clientSecret, controlPlane string, tlsSkipVerify bool) (*Clie
TokenURL: fmt.Sprintf("https://%s/v1/users/oidc/token", controlPlane),
AuthStyle: oauth2.AuthStyleInParams,
}
tokenSource := tokenConfig.TokenSource(context.Background())
tokenSource := tokenConfig.TokenSource(ctx)

log.Printf("[DEBUG] TokenSource: %v", tokenSource)
log.Printf("[DEBUG] End client.New")
tflog.Debug(ctx, fmt.Sprintf("TokenSource: %v", tokenSource))
tflog.Debug(ctx, "End client.New")

return &Client{
ControlPlane: controlPlane,
Expand All @@ -69,10 +70,10 @@ func New(clientID, clientSecret, controlPlane string, tlsSkipVerify bool) (*Clie

// 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(url, httpMethod string, resourceData interface{}) ([]byte, error) {
log.Printf("[DEBUG] Init DoRequest")
log.Printf("[DEBUG] Resource info: %#v", resourceData)
log.Printf("[DEBUG] %s URL: %s", httpMethod, url)
func (c *Client) DoRequest(ctx context.Context, url, httpMethod string, resourceData interface{}) ([]byte, error) {
tflog.Debug(ctx, "Init DoRequest")
tflog.Debug(ctx, fmt.Sprintf("Resource info: %#v", resourceData))
tflog.Debug(ctx, fmt.Sprintf("%s URL: %s", httpMethod, url))
var req *http.Request
var err error
if resourceData != nil {
Expand All @@ -81,7 +82,7 @@ func (c *Client) DoRequest(url, httpMethod string, resourceData interface{}) ([]
return nil, fmt.Errorf("failed to encode payload: %v", err)
}
payload := string(payloadBytes)
log.Printf("[DEBUG] %s payload: %s", httpMethod, payload)
tflog.Debug(ctx, fmt.Sprintf("%s payload: %s", httpMethod, payload))
if req, err = http.NewRequest(httpMethod, url, strings.NewReader(payload)); err != nil {
return nil, fmt.Errorf("unable to create request; err: %v", err)
}
Expand All @@ -97,14 +98,14 @@ func (c *Client) DoRequest(url, httpMethod string, resourceData interface{}) ([]
if token, err = c.TokenSource.Token(); err != nil {
return nil, fmt.Errorf("unable to retrieve authorization token. error: %v", err)
} else {
log.Printf("[DEBUG] Token Type: %s", token.Type())
log.Printf("[DEBUG] Access Token: %s", redactContent(token.AccessToken))
log.Printf("[DEBUG] Token Expiry: %s", token.Expiry)
tflog.Debug(ctx, fmt.Sprintf("Token Type: %s", token.Type()))
tflog.Debug(ctx, fmt.Sprintf("Access Token: %s", redactContent(token.AccessToken)))
tflog.Debug(ctx, fmt.Sprintf("Token Expiry: %s", token.Expiry))
req.Header.Add("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
}
}

log.Printf("[DEBUG] Executing %s", httpMethod)
tflog.Debug(ctx, fmt.Sprintf("Executing %s", httpMethod))
res, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("unable to execute request. Check the control plane address; err: %v", err)
Expand All @@ -128,9 +129,9 @@ func (c *Client) DoRequest(url, httpMethod string, resourceData interface{}) ([]
// Redact token before logging the request
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type(), redactContent(token.AccessToken)))

log.Printf("[DEBUG] Request: %#v", req)
log.Printf("[DEBUG] Response status code: %d", res.StatusCode)
log.Printf("[DEBUG] Response body: %s", string(body))
tflog.Debug(ctx, fmt.Sprintf("Request: %#v", req))
tflog.Debug(ctx, fmt.Sprintf("Response status code: %d", res.StatusCode))
tflog.Debug(ctx, fmt.Sprintf("Response body: %s", string(body)))

if !(res.StatusCode >= 200 && res.StatusCode < 300) {
return nil, NewHttpError(
Expand All @@ -139,7 +140,7 @@ func (c *Client) DoRequest(url, httpMethod string, resourceData interface{}) ([]
res.StatusCode)
}

log.Printf("[DEBUG] End DoRequest")
tflog.Debug(ctx, "End DoRequest")

return body, nil
}
Expand Down
116 changes: 56 additions & 60 deletions cyral/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,34 @@ for resources and data sources.

## How to use to create new resources and data sources

There are some main types that must be used to create a new resources and data sources:
`SchemaDescriptor`, `PackageSchema`, `ResourceData`, `ResponseData` and
There are some main types that must be used to create new resources and data sources:
`SchemaDescriptor`, `PackageSchema`, `SchemaReader`, `SchemaWriter` and
`ResourceOperationConfig`. In a nutshell, these abstractions provide the means to
teach the provider how to interact with the API, how to describe the feature as a
Terraform resource/data source and finally teach the provider how to perform the
translation from API to Terraform schema and vice-versa.
teach the provider how to:

- interact with the API;
- describe the feature as a Terraform resource/data source;
- perform the translation from API to Terraform schema and vice-versa.

Use the files below as examples to create your own implementation. It is advised that
you follow the same naming convention for all the files to simplify future code changes.
you create a single package to group both the resource and data sources for a given
feature/category and that you follow the same naming convention for all the files
to simplify future code changes by adopting a single code convention.

### contants.go

```go
// contants.go
package newfeature

const (
accessTokenSettingsID = "settings/access_token"
// The resource and data source names are identical in this example,
// but this may not always hold true
resourceName = "cyral_new_feature"
dataSourceName = "cyral_new_feature"
)
```

### model.go

Expand Down Expand Up @@ -47,23 +66,28 @@ func (r *NewFeature) ReadFromSchema(d *schema.ResourceData) error {

### datasource.go

Even though the `GET` url for this new feature is `https://<CP>/v1/NewFeature/<ID>`,
the `BaseURLFactory` provided does not provide the `ID` as it will be automatically
added by the default read handler returned in `contextHandler.ReadContext()`.

```go
// datasource.go
package newfeature

var dsContextHandler = core.DefaultContextHandler{
ResourceName: dataSourceName,
ResourceType: resourcetype.DataSource,
SchemaReaderFactory: func() core.SchemaReader { return &NewFeature{} },
SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &NewFeature{} },
BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string {
return fmt.Sprintf("https://%s/v1/NewFeature", c.ControlPlane)
},
}

func dataSourceSchema() *schema.Resource {
return &schema.Resource{
Description: "Some description.",
ReadContext: core.ReadResource(core.ResourceOperationConfig{
Name: "NewFeatureRead",
HttpMethod: http.MethodGet,
CreateURL: func(d *schema.ResourceData, c *client.Client) string {
return fmt.Sprintf("https://%s/v1/NewFeature/%s", c.ControlPlane, d.Get("name").(string))
},
NewResponseData: func(d *schema.ResourceData) core.ResponseData {
return &NewFeature{}
},
}),
ReadContext: dsContextHandler.ReadContext(),
Schema: map[string]*schema.Schema{
"name": {
Description: "Retrieve the unique label with this name, if it exists.",
Expand All @@ -86,41 +110,23 @@ func dataSourceSchema() *schema.Resource {
// resource.go
package newfeature

var resourceContextHandler = core.DefaultContextHandler{
ResourceName: resourceName,
ResourceType: resourcetype.Resource,
SchemaReaderFactory: func() core.SchemaReader { return &NewFeature{} },
SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &NewFeature{} },
BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string {
return fmt.Sprintf("https://%s/v1/NewFeature", c.ControlPlane)
},
}

func resourceSchema() *schema.Resource {
return &schema.Resource{
Description: "Some description.",
CreateContext: core.CreateResource(
core.ResourceOperationConfig{
Name: "NewFeatureResourceRead",
HttpMethod: http.MethodPost,
CreateURL: func(d *schema.ResourceData, c *client.Client) string {
return fmt.Sprintf("https://%s/v1/NewFeature", c.ControlPlane)
},
NewResponseData: func(d *schema.ResourceData) core.ResponseData {
return &NewFeature{}
},
}, ReadNewFeatureConfig,
),
ReadContext: core.ReadResource(ReadNewFeatureConfig),
UpdateContext: core.UpdateResource(
core.ResourceOperationConfig{
Name: "NewFeatureUpdate",
HttpMethod: http.MethodPut,
CreateURL: func(d *schema.ResourceData, c *client.Client) string {
return fmt.Sprintf("https://%s/v1/NewFeature/%s", c.ControlPlane, d.Id())
},
NewResourceData: func() core.ResourceData { return &NewFeature{} },
}, ReadNewFeatureConfig,
),
DeleteContext: core.DeleteResource(
core.ResourceOperationConfig{
Name: "NewFeatureDelete",
HttpMethod: http.MethodDelete,
CreateURL: func(d *schema.ResourceData, c *client.Client) string {
return fmt.Sprintf("https://%s/v1/NewFeature/%s", c.ControlPlane, d.Id())
},
},
),
CreateContext: resourceContextHandler.CreateContext(),
ReadContext: resourceContextHandler.ReadContext(),
UpdateContext: resourceContextHandler.UpdateContext(),
DeleteContext: resourceContextHandler.DeleteContext(),
Schema: map[string]*schema.Schema{
"name": {
Description: "...",
Expand All @@ -135,16 +141,6 @@ func resourceSchema() *schema.Resource {
},
}
}

var ReadNewFeatureConfig = core.ResourceOperationConfig{
Name: "NewFeatureRead",
HttpMethod: http.MethodGet,
CreateURL: func(d *schema.ResourceData, c *client.Client) string {
return fmt.Sprintf("https://%s/v1/NewFeature/%s", c.ControlPlane, d.Id())
},
NewResponseData: func(_ *schema.ResourceData) core.ResponseData { return &NewFeature{} },
RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "NewFeature"},
}
```

### schema_loader.go
Expand All @@ -163,12 +159,12 @@ func (p *packageSchema) Name() string {
func (p *packageSchema) Schemas() []*core.SchemaDescriptor {
return []*core.SchemaDescriptor{
{
Name: "cyral_newfeature",
Name: dataSourceName,
Type: core.DataSourceSchemaType,
Schema: dataSourceSchema,
},
{
Name: "cyral_newfeature",
Name: resourceName,
Type: core.ResourceSchemaType,
Schema: resourceSchema,
},
Expand Down
10 changes: 0 additions & 10 deletions cyral/core/constants.go

This file was deleted.

102 changes: 102 additions & 0 deletions cyral/core/default_context_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package core

import (
"context"
"fmt"
"net/http"

"github.com/cyralinc/terraform-provider-cyral/cyral/client"
ot "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype"
rt "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// Implementation of a default context handler that can be used by all resources
// which API follows these principles:
// 1. The resource is backed by an ID coming from the API.
// 2. The creation is a POST that returns a JSON with an `id` field, meaning
// it can be used with the `IDBasedResponse` struct.
// 3. The endpoint to perform GET, PUT and DELETE calls are composed by the
// POST endpoint plus the ID specification like the following:
// - POST: https://<CP>/<apiVersion>/<featureName>
// - GET: https://<CP>/<apiVersion>/<featureName>/<id>
// - PUT: https://<CP>/<apiVersion>/<featureName>/<id>
// - DELETE: https://<CP>/<apiVersion>/<featureName>/<id>
type DefaultContextHandler struct {
ResourceName string
ResourceType rt.ResourceType
SchemaReaderFactory SchemaReaderFactoryFunc
SchemaWriterFactory SchemaWriterFactoryFunc
BaseURLFactory URLFactoryFunc
}

func defaultSchemaWriterFactory(d *schema.ResourceData) SchemaWriter {
return &IDBasedResponse{}
}

func defaultOperationHandler(
resourceName string,
resourceType rt.ResourceType,
operationType ot.OperationType,
baseURLFactory URLFactoryFunc,
httpMethod string,
schemaReaderFactory SchemaReaderFactoryFunc,
schemaWriterFactory SchemaWriterFactoryFunc,
) ResourceOperationConfig {
// POST = https://<CP>/<apiVersion>/<feature>
// GET, PUT and DELETE = https://<CP>/<apiVersion>/<feature>/<id>
endpoint := func(d *schema.ResourceData, c *client.Client) string {
url := baseURLFactory(d, c)
if d.Id() != "" {
url = fmt.Sprintf("%s/%s", baseURLFactory(d, c), d.Id())
}
tflog.Debug(context.Background(), fmt.Sprintf("Returning base URL for %s '%s' operation '%s' and httpMethod %s: %s",
resourceType, resourceName, operationType, httpMethod, url))
return url
}

var errorHandler RequestErrorHandler
if httpMethod == http.MethodGet {
errorHandler = &ReadIgnoreHttpNotFound{ResName: resourceName}
} else if httpMethod == http.MethodDelete {
errorHandler = &DeleteIgnoreHttpNotFound{ResName: resourceName}
}
result := ResourceOperationConfig{
ResourceName: resourceName,
Type: operationType,
ResourceType: resourceType,
HttpMethod: httpMethod,
URLFactory: endpoint,
SchemaReaderFactory: schemaReaderFactory,
SchemaWriterFactory: schemaWriterFactory,
RequestErrorHandler: errorHandler,
}

return result
}

func (dch DefaultContextHandler) CreateContext() schema.CreateContextFunc {
return CreateResource(
defaultOperationHandler(dch.ResourceName, dch.ResourceType, ot.Create, dch.BaseURLFactory, http.MethodPost, dch.SchemaReaderFactory, nil),
defaultOperationHandler(dch.ResourceName, dch.ResourceType, ot.Create, dch.BaseURLFactory, http.MethodGet, nil, dch.SchemaWriterFactory),
)
}

func (dch DefaultContextHandler) ReadContext() schema.ReadContextFunc {
return ReadResource(dch.ReadResourceOperationConfig())
}
func (dch DefaultContextHandler) ReadResourceOperationConfig() ResourceOperationConfig {
return defaultOperationHandler(dch.ResourceName, dch.ResourceType, ot.Read, dch.BaseURLFactory, http.MethodGet, nil, dch.SchemaWriterFactory)
}

func (dch DefaultContextHandler) UpdateContext() schema.UpdateContextFunc {
return UpdateResource(
defaultOperationHandler(dch.ResourceName, dch.ResourceType, ot.Update, dch.BaseURLFactory, http.MethodPut, dch.SchemaReaderFactory, nil),
defaultOperationHandler(dch.ResourceName, dch.ResourceType, ot.Update, dch.BaseURLFactory, http.MethodGet, nil, dch.SchemaWriterFactory))
}

func (dch DefaultContextHandler) DeleteContext() schema.DeleteContextFunc {
return DeleteResource(defaultOperationHandler(
dch.ResourceName, dch.ResourceType, ot.Delete, dch.BaseURLFactory, http.MethodDelete, nil, nil))
}
Loading

0 comments on commit 16d284e

Please sign in to comment.