diff --git a/.gitignore b/.gitignore index bc4c4646..a84ead0f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .terraform .terraform.lock.hcl terraform.tfstate* +*.tf # Provider binary terraform-provider-cyral* diff --git a/cyral/client/client.go b/cyral/client/client.go index 30f2317f..9bd703c8 100644 --- a/cyral/client/client.go +++ b/cyral/client/client.go @@ -71,23 +71,26 @@ 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(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)) + 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 { payloadBytes, err := json.Marshal(resourceData) if err != nil { + tflog.Debug(ctx, "=> End DoRequest - Error") return nil, fmt.Errorf("failed to encode payload: %v", err) } payload := string(payloadBytes) tflog.Debug(ctx, fmt.Sprintf("%s payload: %s", httpMethod, payload)) if req, err = http.NewRequest(httpMethod, url, strings.NewReader(payload)); err != nil { + tflog.Debug(ctx, "=> End DoRequest - Error") return nil, fmt.Errorf("unable to create request; err: %v", err) } } else { if req, err = http.NewRequest(httpMethod, url, nil); err != nil { + tflog.Debug(ctx, "=> End DoRequest - Error") return nil, fmt.Errorf("unable to create request; err: %v", err) } } @@ -96,24 +99,27 @@ func (c *Client) DoRequest(ctx context.Context, url, httpMethod string, resource token := &oauth2.Token{} if c.TokenSource != nil { if token, err = c.TokenSource.Token(); err != nil { + tflog.Debug(ctx, "=> End DoRequest - Error") return nil, fmt.Errorf("unable to retrieve authorization token. error: %v", err) } else { - 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)) + 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)) } } - tflog.Debug(ctx, fmt.Sprintf("Executing %s", httpMethod)) + tflog.Debug(ctx, fmt.Sprintf("==> Executing %s", httpMethod)) res, err := c.client.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) } defer res.Body.Close() if res.StatusCode == http.StatusConflict || (httpMethod == http.MethodPost && strings.Contains(strings.ToLower(res.Status), "already exists")) { + tflog.Debug(ctx, "=> End DoRequest - Error") return nil, NewHttpError( fmt.Sprintf("resource possibly exists in the control plane. Response status: %s", res.Status), res.StatusCode) @@ -121,6 +127,7 @@ func (c *Client) DoRequest(ctx context.Context, url, httpMethod string, resource body, err := ioutil.ReadAll(res.Body) if err != nil { + tflog.Debug(ctx, "=> End DoRequest - Error") return nil, NewHttpError( fmt.Sprintf("unable to read data from request body; err: %v", err), res.StatusCode) @@ -129,18 +136,19 @@ func (c *Client) DoRequest(ctx context.Context, url, httpMethod string, resource // Redact token before logging the request req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type(), redactContent(token.AccessToken))) - 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))) + 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) { + tflog.Debug(ctx, "=> End DoRequest - Error") return nil, NewHttpError( fmt.Sprintf("error executing %s request; status code: %d; body: %q", httpMethod, res.StatusCode, body), res.StatusCode) } - tflog.Debug(ctx, "End DoRequest") + tflog.Debug(ctx, "=> End DoRequest - Success") return body, nil } diff --git a/cyral/core/README.md b/cyral/core/README.md index fc815dd8..8108faf4 100644 --- a/cyral/core/README.md +++ b/cyral/core/README.md @@ -45,7 +45,7 @@ type NewFeature struct { Description string `json:"description,omitempty"` } -func (r *NewFeature) WriteToSchema(d *schema.ResourceData) error { +func (r NewFeature) WriteToSchema(d *schema.ResourceData) error { if err := d.Set("description", r.Description); err != nil { return fmt.Errorf("error setting 'description' field: %w", err) } @@ -65,9 +65,7 @@ func (r *NewFeature) ReadFromSchema(d *schema.ResourceData) error { ### datasource.go -Even though the `GET` url for this new feature is `https:///v1/NewFeature/`, -the `BaseURLFactory` provided does not provide the `ID` as it will be automatically -added by the default read handler returned in `contextHandler.ReadContext()`. +Use the `GetPutDeleteURLFactory` to provide the URL factory to read the data source from the API. ```go // datasource.go @@ -76,10 +74,9 @@ 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) + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &NewFeature{} }, + GetPutDeleteURLFactory: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/NewFeature/%s", c.ControlPlane, d.Get("my_id_field").(string)) }, } @@ -113,7 +110,7 @@ var resourceContextHandler = core.DefaultContextHandler{ ResourceName: resourceName, ResourceType: resourcetype.Resource, SchemaReaderFactory: func() core.SchemaReader { return &NewFeature{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &NewFeature{} }, + SchemaWriterFactoryGetMethod: 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) }, diff --git a/cyral/core/default_context_handler.go b/cyral/core/default_context_handler.go index 952c3d10..02064dc6 100644 --- a/cyral/core/default_context_handler.go +++ b/cyral/core/default_context_handler.go @@ -12,13 +12,20 @@ import ( "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: +// Implementation of a default context handler that can be used by all resources. +// +// 1. `SchemaWriterFactoryGetMethod“ must be provided. +// 2. In case `SchemaWriterFactoryPostMethod“ is not provided, +// it will assume that a call to POST returns a JSON with +// an `id` field, meaning it will use the +// `IDBasedResponse` struct in such cases. +// 3. `BaseURLFactory` must be provided for resources. It will be used to +// create the POST endpoint and others in case `GetPutDeleteURLFactory` +// is not provided. +// 4. `GetPutDeleteURLFactory` must be provided for data sources. +// 5. If `GetPutDeleteURLFactory` is NOT provided (data sources or resources), +// the endpoint to perform GET, PUT and DELETE calls are composed by the +// `BaseURLFactory` endpoint plus the ID specification as follows: // - POST: https://// // - GET: https:///// // - PUT: https:///// @@ -27,76 +34,104 @@ type DefaultContextHandler struct { ResourceName string ResourceType rt.ResourceType SchemaReaderFactory SchemaReaderFactoryFunc - SchemaWriterFactory SchemaWriterFactoryFunc - BaseURLFactory URLFactoryFunc + // SchemaWriterFactoryGetMethod defines how the schema will be + // written in GET operations. + SchemaWriterFactoryGetMethod SchemaWriterFactoryFunc + // SchemaWriterFactoryPostMethod defines how the schema will be + // written in POST operations. + SchemaWriterFactoryPostMethod SchemaWriterFactoryFunc + // BaseURLFactory provides the URL used for POSTs and that + // will also be used to compose the ID URL for GET, PUT and + // DELETE in case `GetPutDeleteURLFactory` is not provided. + BaseURLFactory URLFactoryFunc + GetPutDeleteURLFactory URLFactoryFunc } -func defaultSchemaWriterFactory(d *schema.ResourceData) SchemaWriter { +func DefaultSchemaWriterFactory(d *schema.ResourceData) SchemaWriter { return &IDBasedResponse{} } -func defaultOperationHandler( - resourceName string, - resourceType rt.ResourceType, +func (dch DefaultContextHandler) defaultOperationHandler( operationType ot.OperationType, - baseURLFactory URLFactoryFunc, httpMethod string, schemaReaderFactory SchemaReaderFactoryFunc, schemaWriterFactory SchemaWriterFactoryFunc, + requestErrorHandler RequestErrorHandler, ) ResourceOperationConfig { // POST = https://// // GET, PUT and DELETE = https:///// 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()) + var url string + if httpMethod == http.MethodPost { + url = dch.BaseURLFactory(d, c) + } else if dch.GetPutDeleteURLFactory != nil { + url = dch.GetPutDeleteURLFactory(d, c) + } else { + url = fmt.Sprintf("%s/%s", dch.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)) + dch.ResourceType, dch.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, + ResourceName: dch.ResourceName, Type: operationType, - ResourceType: resourceType, + ResourceType: dch.ResourceType, HttpMethod: httpMethod, URLFactory: endpoint, SchemaReaderFactory: schemaReaderFactory, SchemaWriterFactory: schemaWriterFactory, - RequestErrorHandler: errorHandler, + RequestErrorHandler: requestErrorHandler, } return result } func (dch DefaultContextHandler) CreateContext() schema.CreateContextFunc { + return dch.CreateContextCustomErrorHandling(&IgnoreHttpNotFound{ResName: dch.ResourceName}, nil) +} + +func (dch DefaultContextHandler) CreateContextCustomErrorHandling(getErrorHandler RequestErrorHandler, + postErrorHandler RequestErrorHandler) schema.CreateContextFunc { + // By default, assumes that if no SchemaWriterFactoryPostMethod is provided, + // the POST api will return an ID + schemaWriterPost := DefaultSchemaWriterFactory + if dch.SchemaWriterFactoryPostMethod != nil { + schemaWriterPost = dch.SchemaWriterFactoryPostMethod + } 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), + dch.defaultOperationHandler(ot.Create, http.MethodPost, dch.SchemaReaderFactory, schemaWriterPost, postErrorHandler), + dch.defaultOperationHandler(ot.Create, http.MethodGet, nil, dch.SchemaWriterFactoryGetMethod, getErrorHandler), ) } func (dch DefaultContextHandler) ReadContext() schema.ReadContextFunc { - return ReadResource(dch.ReadResourceOperationConfig()) + return dch.ReadContextCustomErrorHandling(&IgnoreHttpNotFound{ResName: dch.ResourceName}) } -func (dch DefaultContextHandler) ReadResourceOperationConfig() ResourceOperationConfig { - return defaultOperationHandler(dch.ResourceName, dch.ResourceType, ot.Read, dch.BaseURLFactory, http.MethodGet, nil, dch.SchemaWriterFactory) + +func (dch DefaultContextHandler) ReadContextCustomErrorHandling(getErrorHandler RequestErrorHandler) schema.ReadContextFunc { + return ReadResource( + dch.defaultOperationHandler(ot.Read, http.MethodGet, nil, dch.SchemaWriterFactoryGetMethod, getErrorHandler), + ) } func (dch DefaultContextHandler) UpdateContext() schema.UpdateContextFunc { + return dch.UpdateContextCustomErrorHandling(&IgnoreHttpNotFound{ResName: dch.ResourceName}, nil) +} + +func (dch DefaultContextHandler) UpdateContextCustomErrorHandling(getErrorHandler RequestErrorHandler, + putErrorHandler RequestErrorHandler) 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)) + dch.defaultOperationHandler(ot.Update, http.MethodPut, dch.SchemaReaderFactory, nil, putErrorHandler), + dch.defaultOperationHandler(ot.Update, http.MethodGet, nil, dch.SchemaWriterFactoryGetMethod, getErrorHandler), + ) } func (dch DefaultContextHandler) DeleteContext() schema.DeleteContextFunc { - return DeleteResource(defaultOperationHandler( - dch.ResourceName, dch.ResourceType, ot.Delete, dch.BaseURLFactory, http.MethodDelete, nil, nil)) + return dch.DeleteContextCustomErrorHandling(&IgnoreHttpNotFound{ResName: dch.ResourceName}) +} + +func (dch DefaultContextHandler) DeleteContextCustomErrorHandling(deleteErrorHandler RequestErrorHandler) schema.DeleteContextFunc { + return DeleteResource(dch.defaultOperationHandler(ot.Delete, http.MethodDelete, nil, nil, deleteErrorHandler)) } diff --git a/cyral/core/error_handlers.go b/cyral/core/error_handlers.go index c28f6e99..acb373cd 100644 --- a/cyral/core/error_handlers.go +++ b/cyral/core/error_handlers.go @@ -4,46 +4,69 @@ import ( "context" "fmt" "net/http" + "regexp" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" ) -type DeleteIgnoreHttpNotFound struct { - ResName string +type IgnoreNotFoundByMessage struct { + ResName string + MessageMatches string + OperationType operationtype.OperationType } -func (h *DeleteIgnoreHttpNotFound) HandleError( +func (h *IgnoreNotFoundByMessage) HandleError( ctx context.Context, - _ *schema.ResourceData, + r *schema.ResourceData, _ *client.Client, err error, ) error { - httpError, ok := err.(*client.HttpError) - if !ok || httpError.StatusCode != http.StatusNotFound { - return err + tflog.Debug(ctx, "==> Init HandleError core.IgnoreNotFoundByMessage") + + matched, regexpError := regexp.MatchString( + h.MessageMatches, + err.Error(), + ) + + if regexpError != nil { + return fmt.Errorf("regex failed to compile trying to match '%s' in '%w'. Error: %w", + h.MessageMatches, err, regexpError) } - tflog.Debug(ctx, fmt.Sprintf("%s not found. Skipping deletion.", h.ResName)) - return nil + + if matched { + tflog.Debug(ctx, fmt.Sprintf("===> %s not found. Skipping %s operation. Error: %v", + h.ResName, h.OperationType, err)) + r.SetId("") + tflog.Debug(ctx, "==> End HandleError core.IgnoreNotFoundByMessage - Success") + return nil + } + + tflog.Debug(ctx, "==> End HandleError core.IgnoreNotFoundByMessage - No match found, thus returning the original error") + return err } -type ReadIgnoreHttpNotFound struct { +type IgnoreHttpNotFound struct { ResName string } -func (h *ReadIgnoreHttpNotFound) HandleError( +func (h *IgnoreHttpNotFound) HandleError( ctx context.Context, r *schema.ResourceData, _ *client.Client, err error, ) error { + tflog.Debug(ctx, "==> Init HandleError core.IgnoreHttpNotFound") httpError, ok := err.(*client.HttpError) if !ok || httpError.StatusCode != http.StatusNotFound { + tflog.Debug(ctx, "==> End HandleError core.IgnoreHttpNotFound - Did not find a 404, thus returning the original error") return err } r.SetId("") - tflog.Debug(ctx, fmt.Sprintf("%s not found. Marking resource for recreation.", h.ResName)) + tflog.Debug(ctx, fmt.Sprintf( + "==> End HandleError core.IgnoreHttpNotFound - %s not found. Marking resource for recreation or deletion.", h.ResName)) return nil } diff --git a/cyral/core/resource.go b/cyral/core/resource.go index 7256de2f..ac4a121f 100644 --- a/cyral/core/resource.go +++ b/cyral/core/resource.go @@ -109,67 +109,63 @@ func DeleteResource(deleteConfig ResourceOperationConfig) schema.DeleteContextFu func handleRequests(operations []ResourceOperationConfig) func(context.Context, *schema.ResourceData, any) diag.Diagnostics { return func(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { for _, operation := range operations { - tflog.Debug(ctx, fmt.Sprintf("Init %s - %s", operation.ResourceName, operation.Type)) + tflog.Debug(ctx, fmt.Sprintf("Init handleRequests to %s %s %s", operation.Type, operation.ResourceType, operation.ResourceName)) c := m.(*client.Client) var resourceData SchemaReader if operation.SchemaReaderFactory != nil { + tflog.Debug(ctx, "=> Calling SchemaReaderFactory") if resourceData = operation.SchemaReaderFactory(); resourceData != nil { - tflog.Debug(ctx, fmt.Sprintf("Calling ReadFromSchema. Schema: %#v", d)) + tflog.Debug(ctx, fmt.Sprintf("=> Calling ReadFromSchema. Schema: %#v", d)) if err := resourceData.ReadFromSchema(d); err != nil { + tflog.Debug(ctx, fmt.Sprintf("End handleRequests to %s %s %s - Error: %s", operation.Type, operation.ResourceType, operation.ResourceName, err.Error())) return utils.CreateError( - fmt.Sprintf("Unable to %s resource %s", operation.Type, operation.ResourceName), + fmt.Sprintf("Unable to %s %s %s", operation.Type, operation.ResourceType, operation.ResourceName), err.Error(), ) } - tflog.Debug(ctx, fmt.Sprintf("Succesful call to ReadFromSchema. resourceData: %#v", resourceData)) + tflog.Debug(ctx, fmt.Sprintf("=> Succesful call to ReadFromSchema. resourceData: %#v", resourceData)) } } url := operation.URLFactory(d, c) body, err := c.DoRequest(ctx, url, operation.HttpMethod, resourceData) - if operation.RequestErrorHandler != nil { + if err != nil && operation.RequestErrorHandler != nil { + tflog.Debug(ctx, "=> Calling operation.RequestErrorHandler.HandleError") err = operation.RequestErrorHandler.HandleError(ctx, d, c, err) } if err != nil { + tflog.Debug(ctx, fmt.Sprintf("End handleRequests to %s %s %s - Error: %s", operation.Type, operation.ResourceType, operation.ResourceName, err.Error())) return utils.CreateError( - fmt.Sprintf("Unable to %s resource %s", operation.Type, operation.ResourceName), + fmt.Sprintf("Unable to %s %s %s", operation.Type, operation.ResourceType, operation.ResourceName), err.Error(), ) } - // If a `SchemaWriterFactory` implementation is NOT provided and this is a creation operation, - // use the `defaultSchemaWriterFactory`, assuming the response is a JSON with an `id` field. - /// TODO: Remove this feature after refactoring all resources to use the `DefaultContext`. - var responseDataFunc SchemaWriterFactoryFunc - if body != nil { - if operation.SchemaWriterFactory == nil && operation.Type == operationtype.Create { - responseDataFunc = defaultSchemaWriterFactory - tflog.Debug(ctx, "NewResponseData function set to defaultSchemaWriterFactory.") - } else { - responseDataFunc = operation.SchemaWriterFactory - } - } - if responseDataFunc != nil { - if responseData := responseDataFunc(d); responseData != nil { - tflog.Debug(ctx, fmt.Sprintf("NewResponseData function call performed. d: %#v", d)) + if operation.SchemaWriterFactory == nil { + tflog.Debug(ctx, "=> No SchemaWriterFactory found.") + } else if body != nil { + if responseData := operation.SchemaWriterFactory(d); responseData != nil { + tflog.Debug(ctx, fmt.Sprintf("=> operation.SchemaWriterFactory function call performed. d: %#v", d)) if err := json.Unmarshal(body, responseData); err != nil { + tflog.Debug(ctx, fmt.Sprintf("End handleRequests to %s %s %s - Error: %s", operation.Type, operation.ResourceType, operation.ResourceName, err.Error())) return utils.CreateError("Unable to unmarshall JSON", err.Error()) } - tflog.Debug(ctx, fmt.Sprintf("Response body (unmarshalled): %#v", responseData)) - tflog.Debug(ctx, fmt.Sprintf("Calling WriteToSchema: responseData: %#v", responseData)) + tflog.Debug(ctx, fmt.Sprintf("=> Response body (unmarshalled): %#v", responseData)) + tflog.Debug(ctx, fmt.Sprintf("=> Calling WriteToSchema: responseData: %#v", responseData)) if err := responseData.WriteToSchema(d); err != nil { + tflog.Debug(ctx, fmt.Sprintf("End handleRequests to %s %s %s - Error: %s", operation.Type, operation.ResourceType, operation.ResourceName, err.Error())) return utils.CreateError( - fmt.Sprintf("Unable to %s resource %s", operation.Type, operation.ResourceName), + fmt.Sprintf("Unable to %s %s %s", operation.Type, operation.ResourceType, operation.ResourceName), err.Error(), ) } - tflog.Debug(ctx, fmt.Sprintf("Succesful call to WriteToSchema. d: %#v", d)) + tflog.Debug(ctx, fmt.Sprintf("=> Succesful call to WriteToSchema. d: %#v", d)) } } - tflog.Debug(ctx, fmt.Sprintf("End %s - %s", operation.ResourceName, operation.Type)) + tflog.Debug(ctx, fmt.Sprintf("End handleRequests to %s %s %s - Success", operation.Type, operation.ResourceType, operation.ResourceName)) } return diag.Diagnostics{} } diff --git a/cyral/internal/datalabel/resource.go b/cyral/internal/datalabel/resource.go index e3953636..46a58437 100644 --- a/cyral/internal/datalabel/resource.go +++ b/cyral/internal/datalabel/resource.go @@ -15,29 +15,28 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) +var getPutDeleteURLFactory = func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/datalabels/%s", + c.ControlPlane, + d.Get("name").(string)) +} + func resourceSchema() *schema.Resource { contextHandler := core.DefaultContextHandler{ - ResourceName: resourceName, - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &DataLabel{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &DataLabel{} }, - BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/datalabels", - c.ControlPlane) - }, + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &DataLabel{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &DataLabel{} }, + GetPutDeleteURLFactory: getPutDeleteURLFactory, } return &schema.Resource{ Description: "Manages data labels. Data labels are part of the Cyral [Data Map](https://cyral.com/docs/policy/datamap).", CreateContext: core.CreateResource( core.ResourceOperationConfig{ - ResourceName: resourceName, - Type: operationtype.Create, - HttpMethod: http.MethodPut, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/datalabels/%s", - c.ControlPlane, - d.Get("name").(string)) - }, + ResourceName: resourceName, + Type: operationtype.Create, + HttpMethod: http.MethodPut, + URLFactory: getPutDeleteURLFactory, SchemaReaderFactory: func() core.SchemaReader { return &DataLabel{} }, SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &DataLabel{} }, }, readDataLabelConfig, @@ -114,14 +113,10 @@ func resourceSchema() *schema.Resource { } var readDataLabelConfig = core.ResourceOperationConfig{ - ResourceName: "DataLabelResourceRead", - Type: operationtype.Read, - HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/datalabels/%s", - c.ControlPlane, - d.Get("name").(string)) - }, + ResourceName: resourceName, + Type: operationtype.Read, + HttpMethod: http.MethodGet, + URLFactory: getPutDeleteURLFactory, SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &DataLabel{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Data Label"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: "Data Label"}, } diff --git a/cyral/internal/deprecated/resource_cyral_integration_datadog.go b/cyral/internal/deprecated/resource_cyral_integration_datadog.go index ebc3bf19..f1a067ae 100644 --- a/cyral/internal/deprecated/resource_cyral_integration_datadog.go +++ b/cyral/internal/deprecated/resource_cyral_integration_datadog.go @@ -30,10 +30,10 @@ func (data *DatadogIntegration) ReadFromSchema(d *schema.ResourceData) error { func ResourceIntegrationDatadog() *schema.Resource { contextHandler := core.DefaultContextHandler{ - ResourceName: "Datadog Integration", - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &DatadogIntegration{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &DatadogIntegration{} }, + ResourceName: "Datadog Integration", + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &DatadogIntegration{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &DatadogIntegration{} }, BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/datadog", c.ControlPlane) }, diff --git a/cyral/internal/deprecated/resource_cyral_integration_elk.go b/cyral/internal/deprecated/resource_cyral_integration_elk.go index 0293c427..f0031f9a 100644 --- a/cyral/internal/deprecated/resource_cyral_integration_elk.go +++ b/cyral/internal/deprecated/resource_cyral_integration_elk.go @@ -33,10 +33,10 @@ func (data *ELKIntegration) ReadFromSchema(d *schema.ResourceData) error { func ResourceIntegrationELK() *schema.Resource { contextHandler := core.DefaultContextHandler{ - ResourceName: "ELK Integration", - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &ELKIntegration{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &ELKIntegration{} }, + ResourceName: "ELK Integration", + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &ELKIntegration{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &ELKIntegration{} }, BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/elk", c.ControlPlane) }, diff --git a/cyral/internal/deprecated/resource_cyral_integration_logstash.go b/cyral/internal/deprecated/resource_cyral_integration_logstash.go index a4198d0e..88ba38a0 100644 --- a/cyral/internal/deprecated/resource_cyral_integration_logstash.go +++ b/cyral/internal/deprecated/resource_cyral_integration_logstash.go @@ -37,10 +37,10 @@ func (data *LogstashIntegration) ReadFromSchema(d *schema.ResourceData) error { func ResourceIntegrationLogstash() *schema.Resource { contextHandler := core.DefaultContextHandler{ - ResourceName: "Logstash Integration", - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &LogstashIntegration{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &LogstashIntegration{} }, + ResourceName: "Logstash Integration", + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &LogstashIntegration{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &LogstashIntegration{} }, BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/logstash", c.ControlPlane) }, diff --git a/cyral/internal/deprecated/resource_cyral_integration_looker.go b/cyral/internal/deprecated/resource_cyral_integration_looker.go index 34751e29..f67f6cf8 100644 --- a/cyral/internal/deprecated/resource_cyral_integration_looker.go +++ b/cyral/internal/deprecated/resource_cyral_integration_looker.go @@ -31,10 +31,10 @@ func (data *LookerIntegration) ReadFromSchema(d *schema.ResourceData) error { func ResourceIntegrationLooker() *schema.Resource { contextHandler := core.DefaultContextHandler{ - ResourceName: "Looker Integration", - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &LookerIntegration{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &LookerIntegration{} }, + ResourceName: "Looker Integration", + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &LookerIntegration{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &LookerIntegration{} }, BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/looker", c.ControlPlane) }, diff --git a/cyral/internal/deprecated/resource_cyral_integration_splunk.go b/cyral/internal/deprecated/resource_cyral_integration_splunk.go index 8db2ff53..50c41d96 100644 --- a/cyral/internal/deprecated/resource_cyral_integration_splunk.go +++ b/cyral/internal/deprecated/resource_cyral_integration_splunk.go @@ -41,10 +41,10 @@ func (data *SplunkIntegration) ReadFromSchema(d *schema.ResourceData) error { func ResourceIntegrationSplunk() *schema.Resource { contextHandler := core.DefaultContextHandler{ - ResourceName: "Splunk Integration", - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &SplunkIntegration{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &SplunkIntegration{} }, + ResourceName: "Splunk Integration", + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &SplunkIntegration{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &SplunkIntegration{} }, BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/splunk", c.ControlPlane) }, diff --git a/cyral/internal/deprecated/resource_cyral_integration_sumo_logic.go b/cyral/internal/deprecated/resource_cyral_integration_sumo_logic.go index 4a4b1d05..9459bef1 100644 --- a/cyral/internal/deprecated/resource_cyral_integration_sumo_logic.go +++ b/cyral/internal/deprecated/resource_cyral_integration_sumo_logic.go @@ -28,10 +28,10 @@ func (data *SumoLogicIntegration) ReadFromSchema(d *schema.ResourceData) error { func ResourceIntegrationSumoLogic() *schema.Resource { contextHandler := core.DefaultContextHandler{ - ResourceName: "SumoLogic Integration", - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &SumoLogicIntegration{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &SumoLogicIntegration{} }, + ResourceName: "SumoLogic Integration", + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &SumoLogicIntegration{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &SumoLogicIntegration{} }, BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/sumologic", c.ControlPlane) }, diff --git a/cyral/internal/integration/awsiam/resource_cyral_integration_aws_iam.go b/cyral/internal/integration/awsiam/resource_cyral_integration_aws_iam.go index 49b2e5c9..81eaaeed 100644 --- a/cyral/internal/integration/awsiam/resource_cyral_integration_aws_iam.go +++ b/cyral/internal/integration/awsiam/resource_cyral_integration_aws_iam.go @@ -64,10 +64,10 @@ func (wrapper *AWSIAMIntegrationWrapper) ReadFromSchema(d *schema.ResourceData) func ResourceIntegrationAWSIAM() *schema.Resource { contextHandler := core.DefaultContextHandler{ - ResourceName: "AWS IAM Integration", - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &AWSIAMIntegrationWrapper{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &AWSIAMIntegrationWrapper{} }, + ResourceName: "AWS IAM Integration", + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &AWSIAMIntegrationWrapper{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &AWSIAMIntegrationWrapper{} }, BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/aws/iam", c.ControlPlane) }, diff --git a/cyral/internal/integration/confextension/model_integration_confextension.go b/cyral/internal/integration/confextension/model_integration_confextension.go index 8bdc93a1..3fa3e8fa 100644 --- a/cyral/internal/integration/confextension/model_integration_confextension.go +++ b/cyral/internal/integration/confextension/model_integration_confextension.go @@ -102,6 +102,7 @@ func ConfExtensionIntegrationCreate(templateType string) core.ResourceOperationC SchemaReaderFactory: func() core.SchemaReader { return NewIntegrationConfExtension(templateType) }, + SchemaWriterFactory: core.DefaultSchemaWriterFactory, } } diff --git a/cyral/internal/integration/hcvault/resource.go b/cyral/internal/integration/hcvault/resource.go index d717859e..f0fd8b77 100644 --- a/cyral/internal/integration/hcvault/resource.go +++ b/cyral/internal/integration/hcvault/resource.go @@ -10,10 +10,10 @@ import ( ) var resourceContextHandler = core.DefaultContextHandler{ - ResourceName: resourceName, - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &HCVaultIntegration{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &HCVaultIntegration{} }, + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &HCVaultIntegration{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &HCVaultIntegration{} }, BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/secretProviders/hcvault", c.ControlPlane) }, diff --git a/cyral/internal/integration/hcvault/schema_loader.go b/cyral/internal/integration/hcvault/schema_loader.go index 9499b202..159aa501 100644 --- a/cyral/internal/integration/hcvault/schema_loader.go +++ b/cyral/internal/integration/hcvault/schema_loader.go @@ -6,7 +6,7 @@ type packageSchema struct { } func (p *packageSchema) Name() string { - return "HC Vault Integration" + return "hcvault" } func (p *packageSchema) Schemas() []*core.SchemaDescriptor { diff --git a/cyral/internal/integration/idpsaml/resource_cyral_integration_idp_saml.go b/cyral/internal/integration/idpsaml/resource_cyral_integration_idp_saml.go index 8d3523e8..a3efdf20 100644 --- a/cyral/internal/integration/idpsaml/resource_cyral_integration_idp_saml.go +++ b/cyral/internal/integration/idpsaml/resource_cyral_integration_idp_saml.go @@ -93,7 +93,7 @@ func ReadGenericSAMLConfig() core.ResourceOperationConfig { return fmt.Sprintf("https://%s/v1/integrations/generic-saml/sso/%s", c.ControlPlane, d.Id()) }, SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &ReadGenericSAMLResponse{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Generic SAML"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: "Generic SAML"}, } } diff --git a/cyral/internal/integration/idpsaml/resource_cyral_integration_idp_saml_draft.go b/cyral/internal/integration/idpsaml/resource_cyral_integration_idp_saml_draft.go index 9e80cfde..61b84443 100644 --- a/cyral/internal/integration/idpsaml/resource_cyral_integration_idp_saml_draft.go +++ b/cyral/internal/integration/idpsaml/resource_cyral_integration_idp_saml_draft.go @@ -122,7 +122,7 @@ func DeleteGenericSAMLDraftConfig() core.ResourceOperationConfig { URLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/generic-saml/drafts/%s", c.ControlPlane, d.Id()) }, - RequestErrorHandler: &core.DeleteIgnoreHttpNotFound{ResName: "SAML draft"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: "SAML draft"}, } } diff --git a/cyral/internal/integration/logging/resource_cyral_integration_logging.go b/cyral/internal/integration/logging/resource_cyral_integration_logging.go index d03988c5..d62934d3 100644 --- a/cyral/internal/integration/logging/resource_cyral_integration_logging.go +++ b/cyral/internal/integration/logging/resource_cyral_integration_logging.go @@ -181,6 +181,7 @@ func CreateLoggingIntegration() core.ResourceOperationConfig { return fmt.Sprintf("https://%s/v1/integrations/logging", c.ControlPlane) }, SchemaReaderFactory: func() core.SchemaReader { return &LoggingIntegration{} }, + SchemaWriterFactory: core.DefaultSchemaWriterFactory, } } @@ -192,7 +193,7 @@ var ReadLoggingIntegration = core.ResourceOperationConfig{ return fmt.Sprintf(loggingApiUrl, c.ControlPlane, d.Id()) }, SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &LoggingIntegration{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Integration logging"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: "Integration logging"}, } func UpdateLoggingIntegration() core.ResourceOperationConfig { diff --git a/cyral/internal/integration/slack/resource.go b/cyral/internal/integration/slack/resource.go index fe711b33..9bfb8ff3 100644 --- a/cyral/internal/integration/slack/resource.go +++ b/cyral/internal/integration/slack/resource.go @@ -10,10 +10,10 @@ import ( ) var resourceContextHandler = core.DefaultContextHandler{ - ResourceName: resourceName, - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &SlackAlertsIntegration{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &SlackAlertsIntegration{} }, + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &SlackAlertsIntegration{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &SlackAlertsIntegration{} }, BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/notifications/slack", c.ControlPlane) }, diff --git a/cyral/internal/integration/slack/resource_test.go b/cyral/internal/integration/slack/resource_test.go index 30a3d332..04c5e1b3 100644 --- a/cyral/internal/integration/slack/resource_test.go +++ b/cyral/internal/integration/slack/resource_test.go @@ -26,7 +26,7 @@ var updatedSlackAlertsConfig slack.SlackAlertsIntegration = slack.SlackAlertsInt func TestAccSlackAlertsIntegrationResource(t *testing.T) { testConfig, testFunc := setupSlackAlertTest(initialSlackAlertsConfig) - testUpdateConfig, testUpdateFunc := setupSlackAlertTest(initialSlackAlertsConfig) + testUpdateConfig, testUpdateFunc := setupSlackAlertTest(updatedSlackAlertsConfig) resource.ParallelTest(t, resource.TestCase{ ProviderFactories: provider.ProviderFactories, diff --git a/cyral/internal/integration/slack/schema_loader.go b/cyral/internal/integration/slack/schema_loader.go index 458a3e91..c08b2fcb 100644 --- a/cyral/internal/integration/slack/schema_loader.go +++ b/cyral/internal/integration/slack/schema_loader.go @@ -6,7 +6,7 @@ type packageSchema struct { } func (p *packageSchema) Name() string { - return "Slack Integration" + return "slack" } func (p *packageSchema) Schemas() []*core.SchemaDescriptor { diff --git a/cyral/internal/integration/teams/resource.go b/cyral/internal/integration/teams/resource.go index 4d513a66..945a2082 100644 --- a/cyral/internal/integration/teams/resource.go +++ b/cyral/internal/integration/teams/resource.go @@ -2,36 +2,23 @@ package teams import ( "fmt" - "net/http" "github.com/cyralinc/terraform-provider-cyral/cyral/client" "github.com/cyralinc/terraform-provider-cyral/cyral/core" - "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) var resourceContextHandler = core.DefaultContextHandler{ - ResourceName: resourceName, - ResourceType: resourcetype.Resource, - SchemaReaderFactory: func() core.SchemaReader { return &MsTeamsIntegration{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &MsTeamsIntegration{} }, + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &MsTeamsIntegration{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &MsTeamsIntegration{} }, BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/integrations/notifications/teams", c.ControlPlane) }, } -var ReadMsTeamsConfig = core.ResourceOperationConfig{ - ResourceName: resourceName, - Type: operationtype.Read, - HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/integrations/notifications/teams/%s", c.ControlPlane, d.Id()) - }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &MsTeamsIntegration{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Integration Teams"}, -} - func resourceSchema() *schema.Resource { return &schema.Resource{ Description: "Manages [integration with Microsoft Teams](https://cyral.com/docs/integrations/messaging/microsoft-teams/).", diff --git a/cyral/internal/integration/teams/resource_test.go b/cyral/internal/integration/teams/resource_test.go index e53b575f..21374322 100644 --- a/cyral/internal/integration/teams/resource_test.go +++ b/cyral/internal/integration/teams/resource_test.go @@ -26,7 +26,7 @@ var updatedTeamsConfig teams.MsTeamsIntegration = teams.MsTeamsIntegration{ func TestAccMsTeamsIntegrationResource(t *testing.T) { testConfig, testFunc := setupTeamsTest(initialTeamsConfig) - testUpdateConfig, testUpdateFunc := setupTeamsTest(initialTeamsConfig) + testUpdateConfig, testUpdateFunc := setupTeamsTest(updatedTeamsConfig) resource.ParallelTest(t, resource.TestCase{ ProviderFactories: provider.ProviderFactories, diff --git a/cyral/internal/integration/teams/schema_loader.go b/cyral/internal/integration/teams/schema_loader.go index 75c24700..196bcaf8 100644 --- a/cyral/internal/integration/teams/schema_loader.go +++ b/cyral/internal/integration/teams/schema_loader.go @@ -6,7 +6,7 @@ type packageSchema struct { } func (p *packageSchema) Name() string { - return "Microsoft Teams Integration" + return "teams" } func (p *packageSchema) Schemas() []*core.SchemaDescriptor { diff --git a/cyral/internal/policy/rule/resource_cyral_policy_rule.go b/cyral/internal/policy/rule/resource_cyral_policy_rule.go index e777ab5f..4b0e0f80 100644 --- a/cyral/internal/policy/rule/resource_cyral_policy_rule.go +++ b/cyral/internal/policy/rule/resource_cyral_policy_rule.go @@ -420,7 +420,7 @@ func policyRuleDeleteConfig() core.ResourceOperationConfig { return fmt.Sprintf("https://%s/v1/policies/%s/rules/%s", c.ControlPlane, policyID, policyRuleID) }, - RequestErrorHandler: &core.DeleteIgnoreHttpNotFound{ResName: "Policy Rule"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: "Policy Rule"}, } } diff --git a/cyral/internal/regopolicy/resource_cyral_rego_policy_instance.go b/cyral/internal/regopolicy/resource_cyral_rego_policy_instance.go index 367f9482..865514e8 100644 --- a/cyral/internal/regopolicy/resource_cyral_rego_policy_instance.go +++ b/cyral/internal/regopolicy/resource_cyral_rego_policy_instance.go @@ -50,7 +50,7 @@ var ( SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &RegoPolicyInstance{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Rego policy instance"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: "Rego policy instance"}, } regoPolicyChangeInformation = &schema.Resource{ diff --git a/cyral/internal/repository/accessgateway/constants.go b/cyral/internal/repository/accessgateway/constants.go new file mode 100644 index 00000000..2c8c78c2 --- /dev/null +++ b/cyral/internal/repository/accessgateway/constants.go @@ -0,0 +1,5 @@ +package accessgateway + +const ( + resourceName = "cyral_repository_access_gateway" +) diff --git a/cyral/internal/repository/accessgateway/model.go b/cyral/internal/repository/accessgateway/model.go new file mode 100644 index 00000000..6c17e7b4 --- /dev/null +++ b/cyral/internal/repository/accessgateway/model.go @@ -0,0 +1,30 @@ +package accessgateway + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type AGData struct { + SidecarId string `json:"sidecarId,omitempty"` + BindingId string `json:"bindingId,omitempty"` +} + +type AccessGateway struct { + AGData *AGData `json:"accessGateway,omitempty"` +} + +func (r *AccessGateway) WriteToSchema(d *schema.ResourceData) error { + d.SetId(d.Get(utils.RepositoryIDKey).(string)) + d.Set(utils.SidecarIDKey, r.AGData.SidecarId) + d.Set(utils.BindingIDKey, r.AGData.BindingId) + return nil +} + +func (r *AccessGateway) ReadFromSchema(d *schema.ResourceData) error { + r.AGData = &AGData{ + BindingId: d.Get(utils.BindingIDKey).(string), + SidecarId: d.Get(utils.SidecarIDKey).(string), + } + return nil +} diff --git a/cyral/internal/repository/accessgateway/resource.go b/cyral/internal/repository/accessgateway/resource.go new file mode 100644 index 00000000..760991a4 --- /dev/null +++ b/cyral/internal/repository/accessgateway/resource.go @@ -0,0 +1,101 @@ +package accessgateway + +import ( + "context" + "fmt" + "net/http" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var urlFactory = func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf( + "https://%s/v1/repos/%s/accessGateway", + c.ControlPlane, + d.Get(utils.RepositoryIDKey).(string), + ) +} + +var readRepositoryAccessGatewayConfig = core.ResourceOperationConfig{ + ResourceName: resourceName, + Type: operationtype.Read, + HttpMethod: http.MethodGet, + URLFactory: urlFactory, + SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { + return &AccessGateway{} + }, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: resourceName}, +} + +func resourceSchema() *schema.Resource { + return &schema.Resource{ + Description: "Manages the sidecar and binding set as the access gateway for [cyral_repositories](./repositories.md).", + CreateContext: core.CreateResource( + core.ResourceOperationConfig{ + ResourceName: resourceName, + Type: operationtype.Create, + HttpMethod: http.MethodPut, + URLFactory: urlFactory, + SchemaReaderFactory: func() core.SchemaReader { return &AccessGateway{} }, + }, + readRepositoryAccessGatewayConfig, + ), + ReadContext: core.ReadResource(readRepositoryAccessGatewayConfig), + UpdateContext: core.UpdateResource( + core.ResourceOperationConfig{ + ResourceName: resourceName, + Type: operationtype.Update, + HttpMethod: http.MethodPut, + URLFactory: urlFactory, + SchemaReaderFactory: func() core.SchemaReader { return &AccessGateway{} }, + }, + readRepositoryAccessGatewayConfig, + ), + DeleteContext: core.DeleteResource( + core.ResourceOperationConfig{ + ResourceName: resourceName, + Type: operationtype.Delete, + HttpMethod: http.MethodDelete, + URLFactory: urlFactory, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: resourceName}, + }, + ), + + Schema: map[string]*schema.Schema{ + utils.RepositoryIDKey: { + Description: "ID of the repository the access gateway is associated with. This is also the " + + "import ID for this resource.", + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + utils.SidecarIDKey: { + Description: "ID of the sidecar that will be set as the access gateway for the given repository.", + Type: schema.TypeString, + Required: true, + }, + utils.BindingIDKey: { + Description: "ID of the binding that will be set as the access gateway for the given repository. " + + "Note that modifications to this field will result in terraform replacing the given " + + "access gateway resource, since the access gateway must be deleted before binding. ", + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: func( + ctx context.Context, + d *schema.ResourceData, + m interface{}, + ) ([]*schema.ResourceData, error) { + d.Set(utils.RepositoryIDKey, d.Id()) + return []*schema.ResourceData{d}, nil + }, + }, + } +} diff --git a/cyral/internal/repository/accessgateway/resource_cyral_repository_access_gateway.go b/cyral/internal/repository/accessgateway/resource_cyral_repository_access_gateway.go deleted file mode 100644 index 0fa03651..00000000 --- a/cyral/internal/repository/accessgateway/resource_cyral_repository_access_gateway.go +++ /dev/null @@ -1,140 +0,0 @@ -package accessgateway - -import ( - "context" - "fmt" - "net/http" - - "github.com/cyralinc/terraform-provider-cyral/cyral/client" - "github.com/cyralinc/terraform-provider-cyral/cyral/core" - "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" - "github.com/cyralinc/terraform-provider-cyral/cyral/utils" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -type AGData struct { - SidecarId string `json:"sidecarId,omitempty"` - BindingId string `json:"bindingId,omitempty"` -} - -type AccessGateway struct { - AGData *AGData `json:"accessGateway,omitempty"` -} - -func (r *AccessGateway) WriteToSchema(d *schema.ResourceData) error { - d.SetId(d.Get(utils.RepositoryIDKey).(string)) - d.Set(utils.SidecarIDKey, r.AGData.SidecarId) - d.Set(utils.BindingIDKey, r.AGData.BindingId) - return nil -} - -func (r *AccessGateway) ReadFromSchema(d *schema.ResourceData) error { - r.AGData = &AGData{ - BindingId: d.Get(utils.BindingIDKey).(string), - SidecarId: d.Get(utils.SidecarIDKey).(string), - } - return nil -} - -var ReadRepositoryAccessGatewayConfig = core.ResourceOperationConfig{ - ResourceName: "RepositoryAccessGatewayRead", - Type: operationtype.Read, - HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf( - "https://%s/v1/repos/%s/accessGateway", - c.ControlPlane, - d.Get(utils.RepositoryIDKey).(string), - ) - }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { - return &AccessGateway{} - }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Repository access gateway"}, -} - -func ResourceRepositoryAccessGateway() *schema.Resource { - return &schema.Resource{ - Description: "Manages the sidecar and binding set as the access gateway for [cyral_repositories](./repositories.md).", - CreateContext: core.CreateResource( - core.ResourceOperationConfig{ - ResourceName: "RepositoryAccessGatewayCreate", - Type: operationtype.Update, - HttpMethod: http.MethodPut, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf( - "https://%s/v1/repos/%s/accessGateway", - c.ControlPlane, - d.Get(utils.RepositoryIDKey).(string), - ) - }, - SchemaReaderFactory: func() core.SchemaReader { return &AccessGateway{} }, - }, - ReadRepositoryAccessGatewayConfig, - ), - ReadContext: core.ReadResource(ReadRepositoryAccessGatewayConfig), - UpdateContext: core.UpdateResource( - core.ResourceOperationConfig{ - ResourceName: "RepositoryAccessGatewayUpdate", - Type: operationtype.Update, - HttpMethod: http.MethodPut, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf( - "https://%s/v1/repos/%s/accessGateway", - c.ControlPlane, - d.Get(utils.RepositoryIDKey).(string), - ) - }, - SchemaReaderFactory: func() core.SchemaReader { return &AccessGateway{} }, - }, - ReadRepositoryAccessGatewayConfig, - ), - DeleteContext: core.DeleteResource( - core.ResourceOperationConfig{ - ResourceName: "RepositoryAccessGatewayDelete", - Type: operationtype.Delete, - HttpMethod: http.MethodDelete, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf( - "https://%s/v1/repos/%s/accessGateway", - c.ControlPlane, - d.Get(utils.RepositoryIDKey).(string), - ) - }, - }, - ), - - Schema: map[string]*schema.Schema{ - utils.RepositoryIDKey: { - Description: "ID of the repository the access gateway is associated with. This is also the " + - "import ID for this resource.", - Type: schema.TypeString, - ForceNew: true, - Required: true, - }, - utils.SidecarIDKey: { - Description: "ID of the sidecar that will be set as the access gateway for the given repository.", - Type: schema.TypeString, - Required: true, - }, - utils.BindingIDKey: { - Description: "ID of the binding that will be set as the access gateway for the given repository. " + - "Note that modifications to this field will result in terraform replacing the given " + - "access gateway resource, since the access gateway must be deleted before binding. ", - Type: schema.TypeString, - ForceNew: true, - Required: true, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: func( - ctx context.Context, - d *schema.ResourceData, - m interface{}, - ) ([]*schema.ResourceData, error) { - d.Set(utils.RepositoryIDKey, d.Id()) - return []*schema.ResourceData{d}, nil - }, - }, - } -} diff --git a/cyral/internal/repository/accessgateway/resource_cyral_repository_access_gateway_test.go b/cyral/internal/repository/accessgateway/resource_test.go similarity index 100% rename from cyral/internal/repository/accessgateway/resource_cyral_repository_access_gateway_test.go rename to cyral/internal/repository/accessgateway/resource_test.go diff --git a/cyral/internal/repository/accessgateway/schema_loader.go b/cyral/internal/repository/accessgateway/schema_loader.go new file mode 100644 index 00000000..ce0734f9 --- /dev/null +++ b/cyral/internal/repository/accessgateway/schema_loader.go @@ -0,0 +1,26 @@ +package accessgateway + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/core" +) + +type packageSchema struct { +} + +func (p *packageSchema) Name() string { + return "accessgateway" +} + +func (p *packageSchema) Schemas() []*core.SchemaDescriptor { + return []*core.SchemaDescriptor{ + { + Name: resourceName, + Type: core.ResourceSchemaType, + Schema: resourceSchema, + }, + } +} + +func PackageSchema() core.PackageSchema { + return &packageSchema{} +} diff --git a/cyral/internal/repository/accessrules/constants.go b/cyral/internal/repository/accessrules/constants.go new file mode 100644 index 00000000..ec0bee53 --- /dev/null +++ b/cyral/internal/repository/accessrules/constants.go @@ -0,0 +1,5 @@ +package accessrules + +const ( + resourceName = "cyral_repository_access_rules" +) diff --git a/cyral/internal/repository/accessrules/model.go b/cyral/internal/repository/accessrules/model.go new file mode 100644 index 00000000..216bc93f --- /dev/null +++ b/cyral/internal/repository/accessrules/model.go @@ -0,0 +1,123 @@ +package accessrules + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type AccessRulesIdentity struct { + Type string `json:"type"` + Name string `json:"name"` +} + +type AccessRulesConfig struct { + AuthorizationPolicyInstanceIDs []string `json:"authorizationPolicyInstanceIDs"` +} + +type AccessRule struct { + Identity *AccessRulesIdentity `json:"identity"` + ValidFrom *string `json:"validFrom"` + ValidUntil *string `json:"validUntil"` + Config *AccessRulesConfig `json:"config"` +} + +type AccessRulesResource struct { + AccessRules []*AccessRule `json:"accessRules"` +} + +type AccessRulesResponse struct { + AccessRules []*AccessRule `json:"accessRules"` +} + +// WriteToSchema is used when reading a resource. It takes whatever the API +// read call returned and translates it into the Terraform schema. +func (arr *AccessRulesResponse) WriteToSchema(d *schema.ResourceData) error { + d.SetId( + utils.MarshalComposedID( + []string{ + d.Get("repository_id").(string), + d.Get("user_account_id").(string), + }, + "/", + ), + ) + // We'll have to build the access rule set in the format expected by Terraform, + // which boils down to doing a bunch of type casts + rules := make([]interface{}, 0, len(arr.AccessRules)) + for _, rule := range arr.AccessRules { + m := make(map[string]interface{}) + + m["identity"] = []interface{}{ + map[string]interface{}{ + "type": rule.Identity.Type, + "name": rule.Identity.Name, + }, + } + + m["valid_from"] = rule.ValidFrom + m["valid_until"] = rule.ValidUntil + + if rule.Config != nil && len(rule.Config.AuthorizationPolicyInstanceIDs) > 0 { + m["config"] = []interface{}{ + map[string]interface{}{ + "policy_ids": rule.Config.AuthorizationPolicyInstanceIDs, + }, + } + } + + rules = append(rules, m) + } + return d.Set("rule", rules) +} + +// ReadFromSchema is called when *creating* or *updating* a resource. +// Essentially, it translates the stuff from the .tf file into whatever the +// API expects. The `AccessRulesResource` will be marshalled verbatim, so +// make sure that it matches *exactly* what the API needs. +func (arr *AccessRulesResource) ReadFromSchema(d *schema.ResourceData) error { + rules := d.Get("rule").([]interface{}) + var accessRules []*AccessRule + + for _, rule := range rules { + ruleMap := rule.(map[string]interface{}) + + accessRule := &AccessRule{} + + identity := ruleMap["identity"].(*schema.Set).List()[0].(map[string]interface{}) + accessRule.Identity = &AccessRulesIdentity{ + Type: identity["type"].(string), + Name: identity["name"].(string), + } + + validFrom := ruleMap["valid_from"].(string) + if validFrom != "" { + accessRule.ValidFrom = &validFrom + } + + validUntil := ruleMap["valid_until"].(string) + if validUntil != "" { + accessRule.ValidUntil = &validUntil + } + + conf, ok := ruleMap["config"] + if ok { + confList := conf.(*schema.Set).List() + if len(confList) > 0 { + config := confList[0].(map[string]interface{}) + policyIDs := config["policy_ids"].([]interface{}) + ids := make([]string, 0, len(policyIDs)) + for _, policyID := range policyIDs { + ids = append(ids, policyID.(string)) + } + accessRule.Config = &AccessRulesConfig{ + AuthorizationPolicyInstanceIDs: ids, + } + } + } + + accessRules = append(accessRules, accessRule) + } + + arr.AccessRules = accessRules + return nil +} diff --git a/cyral/internal/repository/accessrules/resource_cyral_repository_access_rules.go b/cyral/internal/repository/accessrules/resource.go similarity index 52% rename from cyral/internal/repository/accessrules/resource_cyral_repository_access_rules.go rename to cyral/internal/repository/accessrules/resource.go index 81ff32ed..8f8fd8e2 100644 --- a/cyral/internal/repository/accessrules/resource_cyral_repository_access_rules.go +++ b/cyral/internal/repository/accessrules/resource.go @@ -12,187 +12,59 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -type AccessRulesIdentity struct { - Type string `json:"type"` - Name string `json:"name"` -} - -type AccessRulesConfig struct { - AuthorizationPolicyInstanceIDs []string `json:"authorizationPolicyInstanceIDs"` -} - -type AccessRule struct { - Identity *AccessRulesIdentity `json:"identity"` - ValidFrom *string `json:"validFrom"` - ValidUntil *string `json:"validUntil"` - Config *AccessRulesConfig `json:"config"` -} - -type AccessRulesResource struct { - AccessRules []*AccessRule `json:"accessRules"` -} - -type AccessRulesResponse struct { - AccessRules []*AccessRule `json:"accessRules"` -} - -// WriteToSchema is used when reading a resource. It takes whatever the API -// read call returned and translates it into the Terraform schema. -func (arr *AccessRulesResponse) WriteToSchema(d *schema.ResourceData) error { - d.SetId( - utils.MarshalComposedID( - []string{ - d.Get("repository_id").(string), - d.Get("user_account_id").(string), - }, - "/", - ), +var urlFactory = func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/repos/%s/userAccounts/%s/accessRules", + c.ControlPlane, + d.Get("repository_id").(string), + d.Get("user_account_id").(string), ) - // We'll have to build the access rule set in the format expected by Terraform, - // which boils down to doing a bunch of type casts - rules := make([]interface{}, 0, len(arr.AccessRules)) - for _, rule := range arr.AccessRules { - m := make(map[string]interface{}) - - m["identity"] = []interface{}{ - map[string]interface{}{ - "type": rule.Identity.Type, - "name": rule.Identity.Name, - }, - } - - m["valid_from"] = rule.ValidFrom - m["valid_until"] = rule.ValidUntil - - if rule.Config != nil && len(rule.Config.AuthorizationPolicyInstanceIDs) > 0 { - m["config"] = []interface{}{ - map[string]interface{}{ - "policy_ids": rule.Config.AuthorizationPolicyInstanceIDs, - }, - } - } - - rules = append(rules, m) - } - return d.Set("rule", rules) -} - -// ReadFromSchema is called when *creating* or *updating* a resource. -// Essentially, it translates the stuff from the .tf file into whatever the -// API expects. The `AccessRulesResource` will be marshalled verbatim, so -// make sure that it matches *exactly* what the API needs. -func (arr *AccessRulesResource) ReadFromSchema(d *schema.ResourceData) error { - rules := d.Get("rule").([]interface{}) - var accessRules []*AccessRule - - for _, rule := range rules { - ruleMap := rule.(map[string]interface{}) - - accessRule := &AccessRule{} - - identity := ruleMap["identity"].(*schema.Set).List()[0].(map[string]interface{}) - accessRule.Identity = &AccessRulesIdentity{ - Type: identity["type"].(string), - Name: identity["name"].(string), - } - - validFrom := ruleMap["valid_from"].(string) - if validFrom != "" { - accessRule.ValidFrom = &validFrom - } - - validUntil := ruleMap["valid_until"].(string) - if validUntil != "" { - accessRule.ValidUntil = &validUntil - } - - conf, ok := ruleMap["config"] - if ok { - confList := conf.(*schema.Set).List() - if len(confList) > 0 { - config := confList[0].(map[string]interface{}) - policyIDs := config["policy_ids"].([]interface{}) - ids := make([]string, 0, len(policyIDs)) - for _, policyID := range policyIDs { - ids = append(ids, policyID.(string)) - } - accessRule.Config = &AccessRulesConfig{ - AuthorizationPolicyInstanceIDs: ids, - } - } - } - - accessRules = append(accessRules, accessRule) - } - - arr.AccessRules = accessRules - return nil } -var ReadRepositoryAccessRulesConfig = core.ResourceOperationConfig{ - ResourceName: "RepositoryAccessRulesRead", +var readRepositoryAccessRulesConfig = core.ResourceOperationConfig{ + ResourceName: resourceName, Type: operationtype.Read, HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/repos/%s/userAccounts/%s/accessRules", - c.ControlPlane, - d.Get("repository_id").(string), - d.Get("user_account_id").(string), - ) - }, + URLFactory: urlFactory, SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &AccessRulesResponse{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Repository access rule"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: "Repository access rule"}, } -func ResourceRepositoryAccessRules() *schema.Resource { +func resourceSchema() *schema.Resource { return &schema.Resource{ Description: "Manage access rules", CreateContext: core.CreateResource( core.ResourceOperationConfig{ - ResourceName: "RepositoryAccessRulesCreate", - Type: operationtype.Create, - HttpMethod: http.MethodPut, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - repoID := d.Get("repository_id").(string) - userAccountID := d.Get("user_account_id").(string) - return fmt.Sprintf("https://%s/v1/repos/%s/userAccounts/%s/accessRules", - c.ControlPlane, - repoID, - userAccountID, - ) - }, + ResourceName: resourceName, + Type: operationtype.Create, + HttpMethod: http.MethodPut, + URLFactory: urlFactory, SchemaReaderFactory: func() core.SchemaReader { return &AccessRulesResource{} }, SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &AccessRulesResponse{} }, }, - ReadRepositoryAccessRulesConfig, + readRepositoryAccessRulesConfig, ), - ReadContext: core.ReadResource(ReadRepositoryAccessRulesConfig), + ReadContext: core.ReadResource(readRepositoryAccessRulesConfig), UpdateContext: core.UpdateResource( core.ResourceOperationConfig{ - ResourceName: "RepositoryAccessRulesUpdate", - Type: operationtype.Update, - HttpMethod: http.MethodPut, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/repos/%s/userAccounts/%s/accessRules", - c.ControlPlane, - d.Get("repository_id").(string), - d.Get("user_account_id").(string), - ) - }, + ResourceName: resourceName, + Type: operationtype.Update, + HttpMethod: http.MethodPut, + URLFactory: urlFactory, SchemaReaderFactory: func() core.SchemaReader { return &AccessRulesResource{} }, SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &AccessRulesResponse{} }, }, - ReadRepositoryAccessRulesConfig, + readRepositoryAccessRulesConfig, ), DeleteContext: core.DeleteResource( core.ResourceOperationConfig{ - ResourceName: "RepositoryAccessRulesDelete", + ResourceName: resourceName, Type: operationtype.Delete, HttpMethod: http.MethodDelete, URLFactory: func(d *schema.ResourceData, c *client.Client) string { - + // TODO Discuss why this is really necessary. We should be able + // to use the same factory for all operations. idPieces, err := utils.UnMarshalComposedID(d.Id(), "/", 2) if err != nil { panic(fmt.Sprintf("Failed to unmarshal access rules ID: %v", err)) @@ -206,6 +78,7 @@ func ResourceRepositoryAccessRules() *schema.Resource { userAccountID, ) }, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: resourceName}, }, ), diff --git a/cyral/internal/repository/accessrules/resource_cyral_repository_access_rules_test.go b/cyral/internal/repository/accessrules/resource_test.go similarity index 100% rename from cyral/internal/repository/accessrules/resource_cyral_repository_access_rules_test.go rename to cyral/internal/repository/accessrules/resource_test.go diff --git a/cyral/internal/repository/accessrules/schema_loader.go b/cyral/internal/repository/accessrules/schema_loader.go new file mode 100644 index 00000000..314fb9a5 --- /dev/null +++ b/cyral/internal/repository/accessrules/schema_loader.go @@ -0,0 +1,26 @@ +package accessrules + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/core" +) + +type packageSchema struct { +} + +func (p *packageSchema) Name() string { + return "accessrules" +} + +func (p *packageSchema) Schemas() []*core.SchemaDescriptor { + return []*core.SchemaDescriptor{ + { + Name: resourceName, + Type: core.ResourceSchemaType, + Schema: resourceSchema, + }, + } +} + +func PackageSchema() core.PackageSchema { + return &packageSchema{} +} diff --git a/cyral/internal/repository/binding/constants.go b/cyral/internal/repository/binding/constants.go new file mode 100644 index 00000000..90517f12 --- /dev/null +++ b/cyral/internal/repository/binding/constants.go @@ -0,0 +1,9 @@ +package binding + +const ( + resourceName = "cyral_repository_binding" + + BindingEnabledKey = "enabled" + ListenerBindingKey = "listener_binding" + NodeIndexKey = "node_index" +) diff --git a/cyral/internal/repository/binding/model.go b/cyral/internal/repository/binding/model.go new file mode 100644 index 00000000..0d58d899 --- /dev/null +++ b/cyral/internal/repository/binding/model.go @@ -0,0 +1,97 @@ +package binding + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type ListenerBindings []*ListenerBinding + +type Binding struct { + BindingID string `json:"id,omitempty"` + RepoId string `json:"repoId,omitempty"` + Enabled bool `json:"enabled,omitempty"` + ListenerBindings ListenerBindings `json:"listenerBindings,omitempty"` +} + +type ListenerBinding struct { + ListenerID string `json:"listenerId,omitempty"` + NodeIndex uint32 `json:"nodeIndex,omitempty"` +} + +type CreateBindingRequest struct { + SidecarID string `json:"sidecarId,omitempty"` + Binding *Binding `json:"binding,omitempty"` +} + +type CreateBindingResponse struct { + BindingID string `json:"bindingId,omitempty"` +} + +type GetBindingResponse struct { + Binding *Binding `json:"binding,omitempty"` +} + +func (r *CreateBindingResponse) WriteToSchema(d *schema.ResourceData) error { + d.Set(utils.BindingIDKey, r.BindingID) + d.SetId(utils.MarshalComposedID( + []string{ + d.Get(utils.SidecarIDKey).(string), + r.BindingID, + }, "/")) + return nil +} + +func (r *GetBindingResponse) WriteToSchema(d *schema.ResourceData) error { + return r.Binding.WriteToSchema(d) +} + +func (r *Binding) WriteToSchema(d *schema.ResourceData) error { + d.Set(utils.BindingIDKey, r.BindingID) + d.Set(BindingEnabledKey, r.Enabled) + d.Set(utils.RepositoryIDKey, r.RepoId) + d.Set(ListenerBindingKey, r.ListenerBindings.AsInterface()) + return nil +} + +func (r *CreateBindingRequest) ReadFromSchema(d *schema.ResourceData) error { + r.SidecarID = d.Get(utils.SidecarIDKey).(string) + r.Binding = &Binding{} + return r.Binding.ReadFromSchema(d) +} + +func (r *Binding) ReadFromSchema(d *schema.ResourceData) error { + r.BindingID = d.Get(utils.BindingIDKey).(string) + r.Enabled = d.Get(BindingEnabledKey).(bool) + r.RepoId = d.Get(utils.RepositoryIDKey).(string) + r.ListenerBindingsFromInterface(d.Get(ListenerBindingKey).([]interface{})) + return nil +} + +func (r *ListenerBindings) AsInterface() []interface{} { + if r == nil { + return nil + } + listenerBindings := make([]interface{}, len(*r)) + for i, listenerBinding := range *r { + listenerBindings[i] = map[string]interface{}{ + utils.ListenerIDKey: listenerBinding.ListenerID, + NodeIndexKey: listenerBinding.NodeIndex, + } + } + return listenerBindings +} + +func (r *Binding) ListenerBindingsFromInterface(i []interface{}) { + if len(i) == 0 { + return + } + listenerBindings := make([]*ListenerBinding, len(i)) + for index, listenerBinding := range i { + listenerBindings[index] = &ListenerBinding{ + ListenerID: listenerBinding.(map[string]interface{})[utils.ListenerIDKey].(string), + NodeIndex: uint32(listenerBinding.(map[string]interface{})[NodeIndexKey].(int)), + } + } + r.ListenerBindings = listenerBindings +} diff --git a/cyral/internal/repository/binding/resource.go b/cyral/internal/repository/binding/resource.go new file mode 100644 index 00000000..466a6118 --- /dev/null +++ b/cyral/internal/repository/binding/resource.go @@ -0,0 +1,103 @@ +package binding + +import ( + "context" + "fmt" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var resourceContextHandler = core.DefaultContextHandler{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &CreateBindingRequest{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &GetBindingResponse{} }, + SchemaWriterFactoryPostMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &CreateBindingResponse{} }, + BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/sidecars/%s/bindings", + c.ControlPlane, + d.Get(utils.SidecarIDKey).(string)) + }, + GetPutDeleteURLFactory: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/sidecars/%s/bindings/%s", + c.ControlPlane, + d.Get(utils.SidecarIDKey).(string), + d.Get(utils.BindingIDKey).(string), + ) + }, +} + +func resourceSchema() *schema.Resource { + return &schema.Resource{ + Description: "Manages [cyral repository to sidecar bindings](https://cyral.com/docs/sidecars/sidecar-assign-repo).", + CreateContext: resourceContextHandler.CreateContext(), + ReadContext: resourceContextHandler.ReadContext(), + UpdateContext: resourceContextHandler.UpdateContext(), + DeleteContext: resourceContextHandler.DeleteContext(), + SchemaVersion: 2, + Schema: map[string]*schema.Schema{ + utils.BindingIDKey: { + Description: "ID of the binding. Computed and assigned to binding at the time of creation.", + Computed: true, + Type: schema.TypeString, + }, + utils.SidecarIDKey: { + Description: "ID of the sidecar that will be bound to the given repository.", + Required: true, + ForceNew: true, + Type: schema.TypeString, + }, + utils.RepositoryIDKey: { + Description: "ID of the repository that will be bound to the sidecar.", + Required: true, + ForceNew: true, + Type: schema.TypeString, + }, + BindingEnabledKey: { + Description: "Enable or disable all listener bindings.", + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + ListenerBindingKey: { + Description: "The configuration for listeners associated with the binding. At least one `listener_binding` is required.", + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + utils.ListenerIDKey: { + Description: "The sidecar listener that this binding is associated with.", + Required: true, + Type: schema.TypeString, + }, + + NodeIndexKey: { + Description: "The index of the repo node that this binding is associated with.", + Optional: true, + Type: schema.TypeInt, + }, + }, + }, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: func( + ctx context.Context, + d *schema.ResourceData, + m interface{}, + ) ([]*schema.ResourceData, error) { + ids, err := utils.UnMarshalComposedID(d.Id(), "/", 2) + if err != nil { + return nil, err + } + d.Set(utils.SidecarIDKey, ids[0]) + d.Set(utils.BindingIDKey, ids[1]) + return []*schema.ResourceData{d}, nil + }, + }, + } +} diff --git a/cyral/internal/repository/binding/resource_cyral_repository_binding.go b/cyral/internal/repository/binding/resource_cyral_repository_binding.go deleted file mode 100644 index 217702a8..00000000 --- a/cyral/internal/repository/binding/resource_cyral_repository_binding.go +++ /dev/null @@ -1,239 +0,0 @@ -package binding - -import ( - "context" - "fmt" - "net/http" - - "github.com/cyralinc/terraform-provider-cyral/cyral/client" - "github.com/cyralinc/terraform-provider-cyral/cyral/core" - "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" - "github.com/cyralinc/terraform-provider-cyral/cyral/utils" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -const ( - BindingEnabledKey = "enabled" - ListenerBindingKey = "listener_binding" - NodeIndexKey = "node_index" -) - -type Binding struct { - BindingID string `json:"id,omitempty"` - RepoId string `json:"repoId,omitempty"` - Enabled bool `json:"enabled,omitempty"` - ListenerBindings []*ListenerBinding `json:"listenerBindings,omitempty"` -} - -type ListenerBinding struct { - ListenerID string `json:"listenerId,omitempty"` - NodeIndex uint32 `json:"nodeIndex,omitempty"` -} - -type CreateBindingRequest struct { - SidecarID string `json:"sidecarId,omitempty"` - Binding *Binding `json:"binding,omitempty"` -} - -type CreateBindingResponse struct { - BindingID string `json:"bindingId,omitempty"` -} - -type GetBindingResponse struct { - Binding *Binding `json:"binding,omitempty"` -} - -func (r *CreateBindingResponse) WriteToSchema(d *schema.ResourceData) error { - d.Set(utils.BindingIDKey, r.BindingID) - d.SetId(utils.MarshalComposedID( - []string{ - d.Get(utils.SidecarIDKey).(string), - r.BindingID, - }, "/")) - return nil -} - -func (r *GetBindingResponse) WriteToSchema(d *schema.ResourceData) error { - return r.Binding.WriteToSchema(d) -} - -func (r *Binding) WriteToSchema(d *schema.ResourceData) error { - d.Set(utils.BindingIDKey, r.BindingID) - d.Set(BindingEnabledKey, r.Enabled) - d.Set(utils.RepositoryIDKey, r.RepoId) - d.Set(ListenerBindingKey, r.ListenerBindingsAsInterface()) - return nil -} - -func (r *CreateBindingRequest) ReadFromSchema(d *schema.ResourceData) error { - r.SidecarID = d.Get(utils.SidecarIDKey).(string) - r.Binding = &Binding{} - return r.Binding.ReadFromSchema(d) -} - -func (r *Binding) ReadFromSchema(d *schema.ResourceData) error { - r.BindingID = d.Get(utils.BindingIDKey).(string) - r.Enabled = d.Get(BindingEnabledKey).(bool) - r.RepoId = d.Get(utils.RepositoryIDKey).(string) - r.ListenerBindingsFromInterface(d.Get(ListenerBindingKey).([]interface{})) - return nil -} - -func (r *Binding) ListenerBindingsAsInterface() []interface{} { - if r.ListenerBindings == nil { - return nil - } - listenerBindings := make([]interface{}, len(r.ListenerBindings)) - for i, listenerBinding := range r.ListenerBindings { - listenerBindings[i] = map[string]interface{}{ - utils.ListenerIDKey: listenerBinding.ListenerID, - NodeIndexKey: listenerBinding.NodeIndex, - } - } - return listenerBindings -} - -func (r *Binding) ListenerBindingsFromInterface(i []interface{}) { - if len(i) == 0 { - return - } - listenerBindings := make([]*ListenerBinding, len(i)) - for index, listenerBinding := range i { - listenerBindings[index] = &ListenerBinding{ - ListenerID: listenerBinding.(map[string]interface{})[utils.ListenerIDKey].(string), - NodeIndex: uint32(listenerBinding.(map[string]interface{})[NodeIndexKey].(int)), - } - } - r.ListenerBindings = listenerBindings -} - -var ReadRepositoryBindingConfig = core.ResourceOperationConfig{ - ResourceName: "RepositoryBindingResourceRead", - Type: operationtype.Read, - HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/sidecars/%s/bindings/%s", - c.ControlPlane, - d.Get(utils.SidecarIDKey).(string), - d.Get(utils.BindingIDKey).(string), - ) - }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { - return &GetBindingResponse{} - }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Repository binding"}, -} - -func ResourceRepositoryBinding() *schema.Resource { - return &schema.Resource{ - Description: "Manages [cyral repository to sidecar bindings](https://cyral.com/docs/sidecars/sidecar-assign-repo).", - CreateContext: core.CreateResource( - core.ResourceOperationConfig{ - ResourceName: "RepositoryBindingResourceCreate", - Type: operationtype.Create, - HttpMethod: http.MethodPost, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/sidecars/%s/bindings", - c.ControlPlane, - d.Get(utils.SidecarIDKey).(string)) - - }, - SchemaReaderFactory: func() core.SchemaReader { return &CreateBindingRequest{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &CreateBindingResponse{} }, - }, ReadRepositoryBindingConfig, - ), - ReadContext: core.ReadResource(ReadRepositoryBindingConfig), - UpdateContext: core.UpdateResource( - core.ResourceOperationConfig{ - ResourceName: "RepositoryBindingResourceUpdate", - Type: operationtype.Update, - HttpMethod: http.MethodPut, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/sidecars/%s/bindings/%s", - c.ControlPlane, - d.Get(utils.SidecarIDKey).(string), - d.Get(utils.BindingIDKey).(string), - ) - - }, - SchemaReaderFactory: func() core.SchemaReader { return &CreateBindingRequest{} }, - }, ReadRepositoryBindingConfig, - ), - DeleteContext: core.DeleteResource( - core.ResourceOperationConfig{ - ResourceName: "RepositoryBindingResourceDelete", - Type: operationtype.Delete, - HttpMethod: http.MethodDelete, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/sidecars/%s/bindings/%s", - c.ControlPlane, - d.Get(utils.SidecarIDKey).(string), - d.Get(utils.BindingIDKey).(string), - ) - }, - }, - ), - - SchemaVersion: 2, - Schema: map[string]*schema.Schema{ - utils.BindingIDKey: { - Description: "ID of the binding. Computed and assigned to binding at the time of creation.", - Computed: true, - Type: schema.TypeString, - }, - utils.SidecarIDKey: { - Description: "ID of the sidecar that will be bound to the given repository.", - Required: true, - ForceNew: true, - Type: schema.TypeString, - }, - utils.RepositoryIDKey: { - Description: "ID of the repository that will be bound to the sidecar.", - Required: true, - ForceNew: true, - Type: schema.TypeString, - }, - BindingEnabledKey: { - Description: "Enable or disable all listener bindings.", - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - ListenerBindingKey: { - Description: "The configuration for listeners associated with the binding. At least one `listener_binding` is required.", - Type: schema.TypeList, - Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - utils.ListenerIDKey: { - Description: "The sidecar listener that this binding is associated with.", - Required: true, - Type: schema.TypeString, - }, - - NodeIndexKey: { - Description: "The index of the repo node that this binding is associated with.", - Optional: true, - Type: schema.TypeInt, - }, - }, - }, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: func( - ctx context.Context, - d *schema.ResourceData, - m interface{}, - ) ([]*schema.ResourceData, error) { - ids, err := utils.UnMarshalComposedID(d.Id(), "/", 2) - if err != nil { - return nil, err - } - d.Set(utils.SidecarIDKey, ids[0]) - d.Set(utils.BindingIDKey, ids[1]) - return []*schema.ResourceData{d}, nil - }, - }, - } -} diff --git a/cyral/internal/repository/binding/resource_cyral_repository_binding_test.go b/cyral/internal/repository/binding/resource_test.go similarity index 100% rename from cyral/internal/repository/binding/resource_cyral_repository_binding_test.go rename to cyral/internal/repository/binding/resource_test.go diff --git a/cyral/internal/repository/binding/schema_loader.go b/cyral/internal/repository/binding/schema_loader.go new file mode 100644 index 00000000..6f551f7b --- /dev/null +++ b/cyral/internal/repository/binding/schema_loader.go @@ -0,0 +1,26 @@ +package binding + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/core" +) + +type packageSchema struct { +} + +func (p *packageSchema) Name() string { + return "binding" +} + +func (p *packageSchema) Schemas() []*core.SchemaDescriptor { + return []*core.SchemaDescriptor{ + { + Name: resourceName, + Type: core.ResourceSchemaType, + Schema: resourceSchema, + }, + } +} + +func PackageSchema() core.PackageSchema { + return &packageSchema{} +} diff --git a/cyral/internal/repository/confanalysis/constants.go b/cyral/internal/repository/confanalysis/constants.go new file mode 100644 index 00000000..5cb9c494 --- /dev/null +++ b/cyral/internal/repository/confanalysis/constants.go @@ -0,0 +1,5 @@ +package confanalysis + +const ( + resourceName = "cyral_repository_conf_analysis" +) diff --git a/cyral/internal/repository/confanalysis/model.go b/cyral/internal/repository/confanalysis/model.go new file mode 100644 index 00000000..e304c76a --- /dev/null +++ b/cyral/internal/repository/confanalysis/model.go @@ -0,0 +1,89 @@ +package confanalysis + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// TODO: v2 of this API should either return the repository ID +// or something else as an ID. Currently it accepts a `UserConfig` +// for the PUT payload, but returns a `RepositoryConfAnalysisData`. +// This makes the whole API utilization quite confusing. + +type RepositoryConfAnalysisData struct { + UserConfig UserConfig `json:"userConfig"` +} + +type UserConfig struct { + AlertOnViolation bool `json:"alertOnViolation"` + BlockOnViolation bool `json:"blockOnViolation"` + CommentAnnotationGroups []string `json:"commentAnnotationGroups,omitempty"` + DisableFilterAnalysis bool `json:"disableFilterAnalysis"` + DisablePreConfiguredAlerts bool `json:"disablePreConfiguredAlerts"` + EnableDataMasking bool `json:"enableDataMasking"` + LogGroups []string `json:"logGroups,omitempty"` + Redact string `json:"redact"` + EnableDatasetRewrites bool `json:"enableDatasetRewrites"` +} + +func (r *RepositoryConfAnalysisData) WriteToSchema(d *schema.ResourceData) error { + d.SetId(d.Get("repository_id").(string)) + return r.UserConfig.WriteToSchema(d) +} + +func (r *UserConfig) WriteToSchema(d *schema.ResourceData) error { + logGroups := make([]interface{}, len(r.LogGroups)) + for index, logGroupItem := range r.LogGroups { + logGroups[index] = logGroupItem + } + logGroupsSet := schema.NewSet(schema.HashString, logGroups) + + annotationGroups := make([]interface{}, len(r.CommentAnnotationGroups)) + for index, annotationGroupItem := range r.CommentAnnotationGroups { + annotationGroups[index] = annotationGroupItem + } + annotationGroupsSet := schema.NewSet(schema.HashString, annotationGroups) + + d.Set("alert_on_violation", r.AlertOnViolation) + d.Set("block_on_violation", r.BlockOnViolation) + d.Set("comment_annotation_groups", annotationGroupsSet) + d.Set("disable_filter_analysis", r.DisableFilterAnalysis) + d.Set("disable_pre_configured_alerts", r.DisablePreConfiguredAlerts) + d.Set("enable_data_masking", r.EnableDataMasking) + d.Set("log_groups", logGroupsSet) + d.Set("redact", r.Redact) + d.Set("enable_dataset_rewrites", r.EnableDatasetRewrites) + + return nil +} + +func (r *RepositoryConfAnalysisData) ReadFromSchema(d *schema.ResourceData) error { + return r.UserConfig.ReadFromSchema(d) +} + +func (r *UserConfig) ReadFromSchema(d *schema.ResourceData) error { + var logGroups []string + if logGroupsSet, ok := d.GetOk("log_groups"); ok { + for _, logGroupItem := range logGroupsSet.(*schema.Set).List() { + logGroups = append(logGroups, logGroupItem.(string)) + } + } + + var annotationGroups []string + if annotationGroupsSet, ok := d.GetOk("comment_annotation_groups"); ok { + for _, annotationGroupItem := range annotationGroupsSet.(*schema.Set).List() { + annotationGroups = append(annotationGroups, annotationGroupItem.(string)) + } + } + + r.AlertOnViolation = d.Get("alert_on_violation").(bool) + r.BlockOnViolation = d.Get("block_on_violation").(bool) + r.DisableFilterAnalysis = d.Get("disable_filter_analysis").(bool) + r.DisablePreConfiguredAlerts = d.Get("disable_pre_configured_alerts").(bool) + r.EnableDataMasking = d.Get("enable_data_masking").(bool) + r.CommentAnnotationGroups = annotationGroups + r.LogGroups = logGroups + r.Redact = d.Get("redact").(string) + r.EnableDatasetRewrites = d.Get("enable_dataset_rewrites").(bool) + + return nil +} diff --git a/cyral/internal/repository/confanalysis/resource.go b/cyral/internal/repository/confanalysis/resource.go new file mode 100644 index 00000000..c55fe231 --- /dev/null +++ b/cyral/internal/repository/confanalysis/resource.go @@ -0,0 +1,244 @@ +package confanalysis + +import ( + "context" + "fmt" + "net/http" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// TODO This resource is more complex than it should be due to the fact that a call to +// repo creation automatically creates the conf/auth and also the conf/analysis configurations. +// Our API should be refactored so these operations should happen separately. + +var urlFactory = func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/repos/%s/conf/analysis", + c.ControlPlane, + d.Get("repository_id"), + ) +} + +var resourceContextHandler = core.DefaultContextHandler{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &UserConfig{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &RepositoryConfAnalysisData{} }, + BaseURLFactory: urlFactory, + GetPutDeleteURLFactory: urlFactory, +} + +func resourceSchema() *schema.Resource { + return &schema.Resource{ + Description: "Manages Repository Analysis Configuration. This resource allows configuring " + + "[Data Activity Logs](https://cyral.com/docs/data-repos/config/#data-activity-logs), " + + "[Alerts](https://cyral.com/docs/data-repos/config/#alerts) and " + + "[Policy Enforcement](https://cyral.com/docs/data-repos/config/#policy-enforcement) " + + "settings for Data Repositories.", + CreateContext: resourceRepositoryConfAnalysisCreate, + ReadContext: resourceContextHandler.ReadContextCustomErrorHandling(&core.IgnoreNotFoundByMessage{ + ResName: resourceName, + MessageMatches: "Cannot find config data for repo", + OperationType: operationtype.Read, + }), + UpdateContext: resourceContextHandler.UpdateContextCustomErrorHandling(&core.IgnoreNotFoundByMessage{ + ResName: resourceName, + MessageMatches: "Cannot find config data for repo", + OperationType: operationtype.Update, + }, nil), + DeleteContext: resourceContextHandler.DeleteContextCustomErrorHandling(&core.IgnoreNotFoundByMessage{ + ResName: resourceName, + MessageMatches: "Cannot find config data for repo", + OperationType: operationtype.Delete, + }), + + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + Type: repositoryConfAnalysisResourceSchemaV0(). + CoreConfigSchema().ImpliedType(), + Upgrade: UpgradeRepositoryConfAnalysisV0, + }, + }, + + Schema: repositoryConfAnalysisResourceSchemaV0().Schema, + + Importer: &schema.ResourceImporter{ + StateContext: func( + ctx context.Context, + d *schema.ResourceData, + m interface{}, + ) ([]*schema.ResourceData, error) { + d.Set("repository_id", d.Id()) + return []*schema.ResourceData{d}, nil + }, + }, + } +} + +func repositoryConfAnalysisResourceSchemaV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Description: "ID of this resource in Cyral environment", + Type: schema.TypeString, + Computed: true, + }, + "repository_id": { + Description: "The ID of an existing data repository resource that will be configured.", + Type: schema.TypeString, + Required: true, + }, + "redact": { + Description: "Valid values are: `all`, `none` and `watched`. If set to `all` it will enable the redact of all literal values, `none` will disable it, and `watched` will only redact values from tracked fields set in the Datamap.", + Type: schema.TypeString, + Optional: true, + Default: "all", + ValidateFunc: client.ValidateRepositoryConfAnalysisRedact(), + }, + "alert_on_violation": { + Description: "If set to `true` it will enable alert on policy violations.", + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "disable_pre_configured_alerts": { + Description: "If set to `true` it will *disable* preconfigured alerts.", + Type: schema.TypeBool, + Optional: true, + }, + "enable_data_masking": { + Description: "If set to `true` it will allow policies to force the masking " + + " of specified data fields in the results of queries. " + + "[Learn more](https://cyral.com/docs/using-cyral/masking/).", + Type: schema.TypeBool, + Optional: true, + }, + "block_on_violation": { + Description: "If set to `true` it will enable query blocking in case of a " + + "policy violation.", + Type: schema.TypeBool, + Optional: true, + }, + "disable_filter_analysis": { + Description: "If set to `true` it will *disable* filter analysis.", + Type: schema.TypeBool, + Optional: true, + }, + "enable_dataset_rewrites": { + Description: "If set to `true` it will enable rewriting queries.", + Type: schema.TypeBool, + Optional: true, + }, + "comment_annotation_groups": { + Description: "Valid values are: `identity`, `client`, `repo`, `sidecar`. The " + + "default behavior is to set only the `identity` when this option is " + + "enabled, but you can also opt to add the contents of `client`, `repo`, " + + " `sidecar` logging blocks as query comments. " + + " [Learn more](https://support.cyral.com/support/solutions/articles/44002218978).", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: client.ValidateRepositoryConfAnalysisCommentAnnotationGroups(), + }, + }, + "log_groups": { + Description: "Responsible for configuring the Log Settings. Valid values are documented below. The `log_groups` list support the following values: " + + "\n - `everything` - Enables all the Log Settings." + + "\n - `dql` - Enables the `DQLs` setting for `all requests`." + + "\n - `dml` - Enables the `DMLs` setting for `all requests`." + + "\n - `ddl` - Enables the `DDLs` setting for `all requests`." + + "\n - `sensitive & dql` - Enables the `DQLs` setting for `logged fields`." + + "\n - `sensitive & dml` - Enables the `DMLs` setting for `logged fields`." + + "\n - `sensitive & ddl` - Enables the `DDLs` setting for `logged fields`." + + "\n - `privileged` - Enables the `Privileged commands` setting." + + "\n - `port-scan` - Enables the `Port scans` setting." + + "\n - `auth-failure` - Enables the `Authentication failures` setting." + + "\n - `full-table-scan` - Enables the `Full scans` setting." + + "\n - `violations` - Enables the `Policy violations` setting." + + "\n - `connections` - Enables the `Connection activity` setting." + + "\n - `sensitive` - Log all queries manipulating sensitive fields (watches)" + + "\n - `data-classification` - Log all queries whose response was automatically classified as sensitive (credit card numbers, emails and so on)." + + "\n - `audit` - Log `sensitive`, `DQLs`, `DDLs`, `DMLs` and `privileged`." + + "\n - `error` - Log analysis errors." + + "\n - `new-connections` - Log new connections." + + "\n - `closed-connections` - Log closed connections.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: client.ValidateRepositoryConfAnalysisLogSettings(), + }, + }, + }, + } +} + +// Previously, the ID for cyral_repository_conf_analysis had the format +// {repository_id}/ConfAnalysis. The goal of this state upgrade is to remove +// this suffix `ConfAnalysis`. +func UpgradeRepositoryConfAnalysisV0( + _ context.Context, + rawState map[string]interface{}, + _ interface{}, +) (map[string]interface{}, error) { + rawState["id"] = rawState["repository_id"] + return rawState, nil +} + +func resourceRepositoryConfAnalysisCreate( + ctx context.Context, + d *schema.ResourceData, + m interface{}, +) diag.Diagnostics { + tflog.Debug(ctx, "Init resourceRepositoryConfAnalysisCreate") + c := m.(*client.Client) + httpMethod := http.MethodPost + if confAnalysisAlreadyExists(ctx, c, d) { + httpMethod = http.MethodPut + } + tflog.Debug(ctx, "End resourceRepositoryConfAnalysisCreate") + return core.CreateResource( + core.ResourceOperationConfig{ + ResourceName: resourceName, + Type: operationtype.Create, + HttpMethod: httpMethod, + URLFactory: urlFactory, + SchemaReaderFactory: func() core.SchemaReader { return &UserConfig{} }, + SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &RepositoryConfAnalysisData{} }, + }, + core.ResourceOperationConfig{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + Type: operationtype.Read, + HttpMethod: http.MethodGet, + URLFactory: urlFactory, + SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &RepositoryConfAnalysisData{} }, + RequestErrorHandler: &core.IgnoreNotFoundByMessage{ + ResName: resourceName, + MessageMatches: "Cannot find config data for repo", + OperationType: operationtype.Read, + }, + }, + )(ctx, d, m) +} + +func confAnalysisAlreadyExists(ctx context.Context, c *client.Client, d *schema.ResourceData) bool { + _, err := c.DoRequest(ctx, urlFactory(d, c), http.MethodGet, nil) + // See TODO on the top of this file + if err != nil { + + tflog.Debug(ctx, fmt.Sprintf("Unable to read Conf Analysis resource for repository %s: %v", + d.Get("repository_id").(string), err)) + return false + } + return true +} diff --git a/cyral/internal/repository/confanalysis/resource_cyral_repository_conf_analysis.go b/cyral/internal/repository/confanalysis/resource_cyral_repository_conf_analysis.go deleted file mode 100644 index 77762704..00000000 --- a/cyral/internal/repository/confanalysis/resource_cyral_repository_conf_analysis.go +++ /dev/null @@ -1,322 +0,0 @@ -package confanalysis - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/cyralinc/terraform-provider-cyral/cyral/client" - "github.com/cyralinc/terraform-provider-cyral/cyral/utils" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -type RepositoryConfAnalysisData struct { - Config UserFacingConfig `json:"userConfig"` -} - -type UserFacingConfig struct { - AlertOnViolation bool `json:"alertOnViolation"` - BlockOnViolation bool `json:"blockOnViolation"` - CommentAnnotationGroups []string `json:"commentAnnotationGroups,omitempty"` - DisableFilterAnalysis bool `json:"disableFilterAnalysis"` - DisablePreConfiguredAlerts bool `json:"disablePreConfiguredAlerts"` - EnableDataMasking bool `json:"enableDataMasking"` - LogGroups []string `json:"logGroups,omitempty"` - Redact string `json:"redact"` - EnableDatasetRewrites bool `json:"enableDatasetRewrites"` -} - -func repositoryConfAnalysisResourceSchemaV0() *schema.Resource { - return &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": { - Description: "ID of this resource in Cyral environment", - Type: schema.TypeString, - Computed: true, - }, - "repository_id": { - Description: "The ID of an existing data repository resource that will be configured.", - Type: schema.TypeString, - Required: true, - }, - "redact": { - Description: "Valid values are: `all`, `none` and `watched`. If set to `all` it will enable the redact of all literal values, `none` will disable it, and `watched` will only redact values from tracked fields set in the Datamap.", - Type: schema.TypeString, - Optional: true, - Default: "all", - ValidateFunc: client.ValidateRepositoryConfAnalysisRedact(), - }, - "alert_on_violation": { - Description: "If set to `true` it will enable alert on policy violations.", - Type: schema.TypeBool, - Optional: true, - Default: true, - }, - "disable_pre_configured_alerts": { - Description: "If set to `true` it will *disable* preconfigured alerts.", - Type: schema.TypeBool, - Optional: true, - }, - "enable_data_masking": { - Description: "If set to `true` it will allow policies to force the masking " + - " of specified data fields in the results of queries. " + - "[Learn more](https://cyral.com/docs/using-cyral/masking/).", - Type: schema.TypeBool, - Optional: true, - }, - "block_on_violation": { - Description: "If set to `true` it will enable query blocking in case of a " + - "policy violation.", - Type: schema.TypeBool, - Optional: true, - }, - "disable_filter_analysis": { - Description: "If set to `true` it will *disable* filter analysis.", - Type: schema.TypeBool, - Optional: true, - }, - "enable_dataset_rewrites": { - Description: "If set to `true` it will enable rewriting queries.", - Type: schema.TypeBool, - Optional: true, - }, - "comment_annotation_groups": { - Description: "Valid values are: `identity`, `client`, `repo`, `sidecar`. The " + - "default behavior is to set only the `identity` when this option is " + - "enabled, but you can also opt to add the contents of `client`, `repo`, " + - " `sidecar` logging blocks as query comments. " + - " [Learn more](https://support.cyral.com/support/solutions/articles/44002218978).", - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: client.ValidateRepositoryConfAnalysisCommentAnnotationGroups(), - }, - }, - "log_groups": { - Description: "Responsible for configuring the Log Settings. Valid values are documented below. The `log_groups` list support the following values: " + - "\n - `everything` - Enables all the Log Settings." + - "\n - `dql` - Enables the `DQLs` setting for `all requests`." + - "\n - `dml` - Enables the `DMLs` setting for `all requests`." + - "\n - `ddl` - Enables the `DDLs` setting for `all requests`." + - "\n - `sensitive & dql` - Enables the `DQLs` setting for `logged fields`." + - "\n - `sensitive & dml` - Enables the `DMLs` setting for `logged fields`." + - "\n - `sensitive & ddl` - Enables the `DDLs` setting for `logged fields`." + - "\n - `privileged` - Enables the `Privileged commands` setting." + - "\n - `port-scan` - Enables the `Port scans` setting." + - "\n - `auth-failure` - Enables the `Authentication failures` setting." + - "\n - `full-table-scan` - Enables the `Full scans` setting." + - "\n - `violations` - Enables the `Policy violations` setting." + - "\n - `connections` - Enables the `Connection activity` setting." + - "\n - `sensitive` - Log all queries manipulating sensitive fields (watches)" + - "\n - `data-classification` - Log all queries whose response was automatically classified as sensitive (credit card numbers, emails and so on)." + - "\n - `audit` - Log `sensitive`, `DQLs`, `DDLs`, `DMLs` and `privileged`." + - "\n - `error` - Log analysis errors." + - "\n - `new-connections` - Log new connections." + - "\n - `closed-connections` - Log closed connections.", - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: client.ValidateRepositoryConfAnalysisLogSettings(), - }, - }, - }, - } -} - -// Previously, the ID for cyral_repository_conf_analysis had the format -// {repository_id}/ConfAnalysis. The goal of this state upgrade is to remove -// this suffix `ConfAnalysis`. -func UpgradeRepositoryConfAnalysisV0( - _ context.Context, - rawState map[string]interface{}, - _ interface{}, -) (map[string]interface{}, error) { - rawState["id"] = rawState["repository_id"] - return rawState, nil -} - -func ResourceRepositoryConfAnalysis() *schema.Resource { - return &schema.Resource{ - Description: "Manages Repository Analysis Configuration. This resource allows configuring both " + - "[Log Settings](https://cyral.com/docs/manage-repositories/repo-log-volume) " + - "and [Advanced settings](https://cyral.com/docs/manage-repositories/repo-advanced-settings) " + - "(Logs, Alerts, Analysis and Enforcement) configurations for Data Repositories.", - CreateContext: resourceRepositoryConfAnalysisCreate, - ReadContext: resourceRepositoryConfAnalysisRead, - UpdateContext: resourceRepositoryConfAnalysisUpdate, - DeleteContext: resourceRepositoryConfAnalysisDelete, - - SchemaVersion: 1, - StateUpgraders: []schema.StateUpgrader{ - { - Version: 0, - Type: repositoryConfAnalysisResourceSchemaV0(). - CoreConfigSchema().ImpliedType(), - Upgrade: UpgradeRepositoryConfAnalysisV0, - }, - }, - - Schema: repositoryConfAnalysisResourceSchemaV0().Schema, - - Importer: &schema.ResourceImporter{ - StateContext: func( - ctx context.Context, - d *schema.ResourceData, - m interface{}, - ) ([]*schema.ResourceData, error) { - d.Set("repository_id", d.Id()) - return []*schema.ResourceData{d}, nil - }, - }, - } -} - -func resourceRepositoryConfAnalysisCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceRepositoryConfAnalysisCreate") - c := m.(*client.Client) - - resourceData, err := getConfAnalysisDataFromResource(d) - if err != nil { - return utils.CreateError("Unable to create conf analysis", fmt.Sprintf("%v", err)) - } - - url := fmt.Sprintf("https://%s/v1/repos/%s/conf/analysis", c.ControlPlane, d.Get("repository_id")) - - body, err := c.DoRequest(ctx, url, http.MethodPut, resourceData.Config) - if err != nil { - return utils.CreateError("Unable to create conf analysis", fmt.Sprintf("%v", err)) - } - - response := RepositoryConfAnalysisData{} - if err := json.Unmarshal(body, &response); err != nil { - return utils.CreateError("Unable to unmarshall JSON", fmt.Sprintf("%v", err)) - } - - tflog.Debug(ctx, fmt.Sprintf("Response body (unmarshalled): %#v", response)) - - d.SetId(d.Get("repository_id").(string)) - - tflog.Debug(ctx, "End resourceRepositoryConfAnalysisCreate") - - return resourceRepositoryConfAnalysisRead(ctx, d, m) -} - -func resourceRepositoryConfAnalysisRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceRepositoryConfAnalysisRead") - c := m.(*client.Client) - - url := fmt.Sprintf("https://%s/v1/repos/%s/conf/analysis", c.ControlPlane, d.Get("repository_id")) - - body, err := c.DoRequest(ctx, url, http.MethodGet, nil) - if err != nil { - return utils.CreateError(fmt.Sprintf("Unable to read conf analysis. Conf Analysis Id: %s", - d.Id()), fmt.Sprintf("%v", err)) - } - - response := RepositoryConfAnalysisData{} - if err := json.Unmarshal(body, &response); err != nil { - return utils.CreateError("Unable to unmarshall JSON", fmt.Sprintf("%v", err)) - } - - tflog.Debug(ctx, fmt.Sprintf("Response body (unmarshalled): %#v", response)) - - setConfAnalysisDataToResource(d, response) - - tflog.Debug(ctx, "End resourceRepositoryConfAnalysisRead") - - return diag.Diagnostics{} -} - -func resourceRepositoryConfAnalysisUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceRepositoryConfAnalysisUpdate") - c := m.(*client.Client) - - resourceData, err := getConfAnalysisDataFromResource(d) - if err != nil { - return utils.CreateError("Unable to update conf analysis", fmt.Sprintf("%v", err)) - } - - url := fmt.Sprintf("https://%s/v1/repos/%s/conf/analysis", c.ControlPlane, d.Get("repository_id")) - - if _, err := c.DoRequest(ctx, url, http.MethodPut, resourceData.Config); err != nil { - return utils.CreateError("Unable to update conf analysis", fmt.Sprintf("%v", err)) - } - - tflog.Debug(ctx, "End resourceRepositoryConfAnalysisUpdate") - - return resourceRepositoryConfAnalysisRead(ctx, d, m) -} - -func resourceRepositoryConfAnalysisDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceRepositoryConfAnalysisDelete") - c := m.(*client.Client) - - url := fmt.Sprintf("https://%s/v1/repos/%s/conf/analysis", c.ControlPlane, d.Get("repository_id")) - - if _, err := c.DoRequest(ctx, url, http.MethodDelete, nil); err != nil { - return utils.CreateError("Unable to delete conf analysis", fmt.Sprintf("%v", err)) - } - - tflog.Debug(ctx, "End resourceRepositoryConfAnalysisDelete") - - return diag.Diagnostics{} -} - -func getConfAnalysisDataFromResource(d *schema.ResourceData) (RepositoryConfAnalysisData, error) { - var logGroups []string - if logGroupsSet, ok := d.GetOk("log_groups"); ok { - for _, logGroupItem := range logGroupsSet.(*schema.Set).List() { - logGroups = append(logGroups, logGroupItem.(string)) - } - } - - var annotationGroups []string - if annotationGroupsSet, ok := d.GetOk("comment_annotation_groups"); ok { - for _, annotationGroupItem := range annotationGroupsSet.(*schema.Set).List() { - annotationGroups = append(annotationGroups, annotationGroupItem.(string)) - } - } - - return RepositoryConfAnalysisData{ - Config: UserFacingConfig{ - AlertOnViolation: d.Get("alert_on_violation").(bool), - BlockOnViolation: d.Get("block_on_violation").(bool), - DisableFilterAnalysis: d.Get("disable_filter_analysis").(bool), - DisablePreConfiguredAlerts: d.Get("disable_pre_configured_alerts").(bool), - EnableDataMasking: d.Get("enable_data_masking").(bool), - CommentAnnotationGroups: annotationGroups, - LogGroups: logGroups, - Redact: d.Get("redact").(string), - EnableDatasetRewrites: d.Get("enable_dataset_rewrites").(bool), - }, - }, nil -} - -func setConfAnalysisDataToResource(d *schema.ResourceData, resourceData RepositoryConfAnalysisData) { - logGroups := make([]interface{}, len(resourceData.Config.LogGroups)) - for index, logGroupItem := range resourceData.Config.LogGroups { - logGroups[index] = logGroupItem - } - logGroupsSet := schema.NewSet(schema.HashString, logGroups) - - annotationGroups := make([]interface{}, len(resourceData.Config.CommentAnnotationGroups)) - for index, annotationGroupItem := range resourceData.Config.CommentAnnotationGroups { - annotationGroups[index] = annotationGroupItem - } - annotationGroupsSet := schema.NewSet(schema.HashString, annotationGroups) - - d.Set("alert_on_violation", resourceData.Config.AlertOnViolation) - d.Set("block_on_violation", resourceData.Config.BlockOnViolation) - d.Set("comment_annotation_groups", annotationGroupsSet) - d.Set("disable_filter_analysis", resourceData.Config.DisableFilterAnalysis) - d.Set("disable_pre_configured_alerts", resourceData.Config.DisablePreConfiguredAlerts) - d.Set("enable_data_masking", resourceData.Config.EnableDataMasking) - d.Set("log_groups", logGroupsSet) - d.Set("redact", resourceData.Config.Redact) - d.Set("enable_dataset_rewrites", resourceData.Config.EnableDatasetRewrites) -} diff --git a/cyral/internal/repository/confanalysis/resource_cyral_repository_conf_analysis_test.go b/cyral/internal/repository/confanalysis/resource_test.go similarity index 100% rename from cyral/internal/repository/confanalysis/resource_cyral_repository_conf_analysis_test.go rename to cyral/internal/repository/confanalysis/resource_test.go diff --git a/cyral/internal/repository/confanalysis/schema_loader.go b/cyral/internal/repository/confanalysis/schema_loader.go new file mode 100644 index 00000000..5eec9304 --- /dev/null +++ b/cyral/internal/repository/confanalysis/schema_loader.go @@ -0,0 +1,26 @@ +package confanalysis + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/core" +) + +type packageSchema struct { +} + +func (p *packageSchema) Name() string { + return "confanalysis" +} + +func (p *packageSchema) Schemas() []*core.SchemaDescriptor { + return []*core.SchemaDescriptor{ + { + Name: resourceName, + Type: core.ResourceSchemaType, + Schema: resourceSchema, + }, + } +} + +func PackageSchema() core.PackageSchema { + return &packageSchema{} +} diff --git a/cyral/internal/repository/confauth/constants.go b/cyral/internal/repository/confauth/constants.go new file mode 100644 index 00000000..da3f98f8 --- /dev/null +++ b/cyral/internal/repository/confauth/constants.go @@ -0,0 +1,11 @@ +package confauth + +const ( + resourceName = "cyral_repository_conf_auth" + + DefaultClientTLS = "disable" + DefaultRepoTLS = "disable" + AccessTokenAuthType = "ACCESS_TOKEN" + AwsIAMAuthType = "AWS_IAM" + DefaultAuthType = AccessTokenAuthType +) diff --git a/cyral/internal/repository/confauth/model.go b/cyral/internal/repository/confauth/model.go new file mode 100644 index 00000000..f326e3b2 --- /dev/null +++ b/cyral/internal/repository/confauth/model.go @@ -0,0 +1,92 @@ +package confauth + +import ( + "errors" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var authTypes = []string{ + AccessTokenAuthType, + AwsIAMAuthType, +} + +type RepositoryConfAuthData struct { + RepoID *string `json:"-"` + AllowNativeAuth bool `json:"allowNativeAuth"` + ClientTLS string `json:"clientTLS"` + IdentityProvider string `json:"identityProvider"` + RepoTLS string `json:"repoTLS"` + AuthType string `json:"authType"` +} + +func (data RepositoryConfAuthData) WriteToSchema(d *schema.ResourceData) error { + if data.RepoID != nil { + d.Set("repository_id", data.RepoID) + } + + d.Set("allow_native_auth", data.AllowNativeAuth) + + if err := data.isClientTLSValid(); err != nil { + panic(err) + } + + d.Set("client_tls", data.ClientTLS) + + d.Set("identity_provider", data.IdentityProvider) + + if err := data.isRepoTLSValid(); err != nil { + panic(err) + } + + d.Set("repo_tls", data.RepoTLS) + + d.Set("auth_type", data.AuthType) + + return nil +} + +func (data *RepositoryConfAuthData) ReadFromSchema(d *schema.ResourceData) error { + if repoIdData, hasRepoId := d.GetOk("repository_id"); hasRepoId { + repoId := repoIdData.(string) + data.RepoID = &repoId + } + + data.AllowNativeAuth = d.Get("allow_native_auth").(bool) + data.AuthType = d.Get("auth_type").(string) + data.ClientTLS = d.Get("client_tls").(string) + data.IdentityProvider = d.Get("identity_provider").(string) + data.RepoTLS = d.Get("repo_tls").(string) + + return nil +} + +func (data RepositoryConfAuthData) isClientTLSValid() error { + if !(data.ClientTLS == "enable" || data.ClientTLS == "disable" || data.ClientTLS == "enabledAndVerifyCertificate") { + return errors.New("invalid option to client_tls") + } + return nil +} + +func (data RepositoryConfAuthData) isRepoTLSValid() error { + if !(data.RepoTLS == "enable" || data.RepoTLS == "disable" || data.RepoTLS == "enabledAndVerifyCertificate") { + return errors.New("invalid option to repo_tls") + } + return nil +} + +type CreateRepositoryConfAuthResponse struct{} + +func (data CreateRepositoryConfAuthResponse) WriteToSchema(d *schema.ResourceData) error { + d.SetId(d.Get("repository_id").(string)) + return nil +} + +type ReadRepositoryConfAuthResponse struct { + AuthInfo RepositoryConfAuthData `json:"authInfo"` +} + +func (data ReadRepositoryConfAuthResponse) WriteToSchema(d *schema.ResourceData) error { + data.AuthInfo.WriteToSchema(d) + return nil +} diff --git a/cyral/internal/repository/confauth/resource.go b/cyral/internal/repository/confauth/resource.go new file mode 100644 index 00000000..6a457482 --- /dev/null +++ b/cyral/internal/repository/confauth/resource.go @@ -0,0 +1,207 @@ +package confauth + +import ( + "context" + "fmt" + "net/http" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository" + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +// TODO This resource is more complex than it should be due to the fact that a call to +// repo creation automatically creates the conf/auth and also the conf/analysis configurations. +// Our API should be refactored so these operations should happen separately. + +var urlFactory = func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/repos/%s/conf/auth", + c.ControlPlane, + d.Get("repository_id"), + ) +} + +var resourceContextHandler = core.DefaultContextHandler{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &RepositoryConfAuthData{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &ReadRepositoryConfAuthResponse{} }, + BaseURLFactory: urlFactory, + GetPutDeleteURLFactory: urlFactory, +} + +func resourceSchema() *schema.Resource { + return &schema.Resource{ + Description: "Manages Repository Analysis Configuration. This resource allows configuring both " + + "[Log Settings](https://cyral.com/docs/manage-repositories/repo-log-volume) " + + "and [Advanced settings](https://cyral.com/docs/manage-repositories/repo-advanced-settings) " + + "(Logs, Alerts, Analysis and Enforcement) configurations for Data Repositories.", + CreateContext: resourceRepositoryConfAuthCreate, + ReadContext: resourceContextHandler.ReadContextCustomErrorHandling(&core.IgnoreNotFoundByMessage{ + ResName: resourceName, + MessageMatches: "Failed to read repo", + OperationType: operationtype.Read, + }), + UpdateContext: resourceContextHandler.UpdateContextCustomErrorHandling(&core.IgnoreNotFoundByMessage{ + ResName: resourceName, + MessageMatches: "Failed to read repo", + OperationType: operationtype.Update, + }, nil), + DeleteContext: resourceContextHandler.DeleteContextCustomErrorHandling(&core.IgnoreNotFoundByMessage{ + ResName: resourceName, + MessageMatches: "Failed to read repo", + OperationType: operationtype.Delete, + }), + + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + Type: repositoryConfAuthResourceSchemaV0(). + CoreConfigSchema().ImpliedType(), + Upgrade: UpgradeRepositoryConfAuthV0, + }, + }, + + Schema: repositoryConfAuthResourceSchemaV0().Schema, + + Importer: &schema.ResourceImporter{ + StateContext: func( + ctx context.Context, + d *schema.ResourceData, + m interface{}, + ) ([]*schema.ResourceData, error) { + d.Set("repository_id", d.Id()) + return []*schema.ResourceData{d}, nil + }, + }, + } +} + +func repositoryConfAuthResourceSchemaV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Description: "The ID of this resource is set to `repository_id`.", + Type: schema.TypeString, + Computed: true, + }, + "repository_id": { + Description: "The ID of the repository to be configured.", + Type: schema.TypeString, + Required: true, + }, + "allow_native_auth": { + Description: "Should the communication allow native authentication?", + Type: schema.TypeBool, + Optional: true, + }, + "client_tls": { + Description: fmt.Sprintf("Is the repo Client using TLS? Default is %q.", DefaultClientTLS), + Type: schema.TypeString, + Optional: true, + Default: DefaultClientTLS, + }, + "identity_provider": { + Description: fmt.Sprintf( + "The semantics of this field changed in control planes `v4.13` and later. See how "+ + "it should be configured depending on your control plane version:\n"+ + "\t- `v4.12` and below:\n\t\t- Provide the ID (Alias) of the identity provider "+ + "integration to allow user authentication using an IdP.\n"+ + "\t- `v4.13` and later:\n\t\t- If not supplied, then end-user "+ + "authentication is disabled.\n\t\t- If end-user authentication "+ + "with Cyral Access Token is desired, then set to `ACCESS_TOKEN` or any "+ + "other non-empty string.\n\t\t- If end-user authentication with "+ + "AWS IAM is desired, then this must be the ID of an AWS IAM integration, "+ + "and the `auth_type` attribute must be set to `%s`.", + AwsIAMAuthType, + ), + Type: schema.TypeString, + Optional: true, + }, + "repo_tls": { + Description: fmt.Sprintf("Is TLS enabled for the repository? Default is %q.", DefaultRepoTLS), + Type: schema.TypeString, + Optional: true, + Default: DefaultRepoTLS, + }, + "auth_type": { + Description: fmt.Sprintf("Authentication type for this repository. **Note**: `%s` is currently "+ + "only supported by `%s` repo type. List of supported values: %s", + AwsIAMAuthType, repository.MongoDB, utils.SupportedValuesAsMarkdown(authTypes)), + Type: schema.TypeString, + Optional: true, + Default: DefaultAuthType, + ValidateFunc: validation.StringInSlice(authTypes, false), + }, + }, + } +} + +// Previously, the id of the resource `cyral_repository_conf_auth` was hardcoded +// to `repo-conf`, which doesn't make sense. The goal here is to set it to be +// the repository ID. +func UpgradeRepositoryConfAuthV0( + _ context.Context, + rawState map[string]interface{}, + _ interface{}, +) (map[string]interface{}, error) { + rawState["id"] = rawState["repository_id"] + return rawState, nil +} + +func resourceRepositoryConfAuthCreate( + ctx context.Context, + d *schema.ResourceData, + m interface{}, +) diag.Diagnostics { + tflog.Debug(ctx, "Init resourceRepositoryConfAuthCreate") + c := m.(*client.Client) + httpMethod := http.MethodPost + if confAuthAlreadyExists(ctx, c, d) { + httpMethod = http.MethodPut + } + tflog.Debug(ctx, "End resourceRepositoryConfAuthCreate") + return core.CreateResource( + core.ResourceOperationConfig{ + ResourceName: resourceName, + Type: operationtype.Create, + HttpMethod: httpMethod, + URLFactory: urlFactory, + SchemaReaderFactory: func() core.SchemaReader { return &RepositoryConfAuthData{} }, + SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &CreateRepositoryConfAuthResponse{} }, + }, + core.ResourceOperationConfig{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + Type: operationtype.Read, + HttpMethod: http.MethodGet, + URLFactory: urlFactory, + SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &ReadRepositoryConfAuthResponse{} }, + RequestErrorHandler: &core.IgnoreNotFoundByMessage{ + ResName: resourceName, + MessageMatches: "Failed to read repo", + OperationType: operationtype.Read, + }, + }, + )(ctx, d, m) +} + +func confAuthAlreadyExists(ctx context.Context, c *client.Client, d *schema.ResourceData) bool { + _, err := c.DoRequest(ctx, urlFactory(d, c), http.MethodGet, nil) + // See TODO on the top of this file + if err != nil { + + tflog.Debug(ctx, fmt.Sprintf("Unable to read Conf Auth resource for repository %s: %v", + d.Get("repository_id").(string), err)) + return false + } + return true +} diff --git a/cyral/internal/repository/confauth/resource_cyral_repository_conf_auth.go b/cyral/internal/repository/confauth/resource_cyral_repository_conf_auth.go deleted file mode 100644 index 2d8bbb29..00000000 --- a/cyral/internal/repository/confauth/resource_cyral_repository_conf_auth.go +++ /dev/null @@ -1,296 +0,0 @@ -package confauth - -import ( - "context" - "errors" - "fmt" - "net/http" - - "github.com/cyralinc/terraform-provider-cyral/cyral/client" - "github.com/cyralinc/terraform-provider-cyral/cyral/core" - "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository" - "github.com/cyralinc/terraform-provider-cyral/cyral/utils" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -const ( - repositoryConfAuthURLFormat = "https://%s/v1/repos/%s/conf/auth" - - DefaultClientTLS = "disable" - DefaultRepoTLS = "disable" - AccessTokenAuthType = "ACCESS_TOKEN" - AwsIAMAuthType = "AWS_IAM" - DefaultAuthType = AccessTokenAuthType -) - -var authTypes = []string{ - AccessTokenAuthType, - AwsIAMAuthType, -} - -type RepositoryConfAuthData struct { - RepoID *string `json:"-"` - AllowNativeAuth bool `json:"allowNativeAuth"` - ClientTLS string `json:"clientTLS"` - IdentityProvider string `json:"identityProvider"` - RepoTLS string `json:"repoTLS"` - AuthType string `json:"authType"` -} - -func (data RepositoryConfAuthData) WriteToSchema(d *schema.ResourceData) error { - if data.RepoID != nil { - d.Set("repository_id", data.RepoID) - } - - d.Set("allow_native_auth", data.AllowNativeAuth) - - if err := data.isClientTLSValid(); err != nil { - panic(err) - } - - d.Set("client_tls", data.ClientTLS) - - d.Set("identity_provider", data.IdentityProvider) - - if err := data.isRepoTLSValid(); err != nil { - panic(err) - } - - d.Set("repo_tls", data.RepoTLS) - - d.Set("auth_type", data.AuthType) - - return nil -} - -func (data *RepositoryConfAuthData) ReadFromSchema(d *schema.ResourceData) error { - if repoIdData, hasRepoId := d.GetOk("repository_id"); hasRepoId { - repoId := repoIdData.(string) - data.RepoID = &repoId - } - - data.AllowNativeAuth = d.Get("allow_native_auth").(bool) - data.AuthType = d.Get("auth_type").(string) - data.ClientTLS = d.Get("client_tls").(string) - data.IdentityProvider = d.Get("identity_provider").(string) - data.RepoTLS = d.Get("repo_tls").(string) - - return nil -} - -func (data RepositoryConfAuthData) isClientTLSValid() error { - if !(data.ClientTLS == "enable" || data.ClientTLS == "disable" || data.ClientTLS == "enabledAndVerifyCertificate") { - return errors.New("invalid option to client_tls") - } - return nil -} - -func (data RepositoryConfAuthData) isRepoTLSValid() error { - if !(data.RepoTLS == "enable" || data.RepoTLS == "disable" || data.RepoTLS == "enabledAndVerifyCertificate") { - return errors.New("invalid option to repo_tls") - } - return nil -} - -type CreateRepositoryConfAuthResponse struct{} - -func (data CreateRepositoryConfAuthResponse) WriteToSchema(d *schema.ResourceData) error { - d.SetId(d.Get("repository_id").(string)) - return nil -} - -type ReadRepositoryConfAuthResponse struct { - AuthInfo RepositoryConfAuthData `json:"authInfo"` -} - -func (data ReadRepositoryConfAuthResponse) WriteToSchema(d *schema.ResourceData) error { - data.AuthInfo.WriteToSchema(d) - return nil -} - -func resourceRepositoryConfAuthCreate( - ctx context.Context, - d *schema.ResourceData, - m interface{}, -) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceRepositoryConfAuthCreate") - c := m.(*client.Client) - httpMethod := http.MethodPost - if confAuthAlreadyExists(ctx, c, d.Get("repository_id").(string)) { - httpMethod = http.MethodPut - } - tflog.Debug(ctx, "End resourceRepositoryConfAuthCreate") - return core.CreateResource(CreateConfAuthConfig(httpMethod), ReadConfAuthConfig())(ctx, d, m) -} - -func confAuthAlreadyExists(ctx context.Context, c *client.Client, repositoryID string) bool { - url := fmt.Sprintf(repositoryConfAuthURLFormat, c.ControlPlane, repositoryID) - _, err := c.DoRequest(ctx, url, http.MethodGet, nil) - // The GET /v1/repos/{repoID}/conf/auth API currently returns 500 status code for every type - // of error, so its not possible to distinguish if the error is due to a 404 Not Found or not. - // Once the status code returned by this API is fixed we should return false only if it returns - // a 404 Not Found, otherwise, if a different error occurs, this function should return an error. - if err != nil { - tflog.Debug(ctx, fmt.Sprintf("Unable to read Conf Auth resource for repository %s: %v", repositoryID, err)) - return false - } - return true -} - -func CreateConfAuthConfig(httpMethod string) core.ResourceOperationConfig { - return core.ResourceOperationConfig{ - ResourceName: "ConfAuthResourceCreate", - Type: operationtype.Create, - HttpMethod: httpMethod, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf(repositoryConfAuthURLFormat, c.ControlPlane, d.Get("repository_id")) - }, - SchemaReaderFactory: func() core.SchemaReader { return &RepositoryConfAuthData{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &CreateRepositoryConfAuthResponse{} }, - } -} - -func ReadConfAuthConfig() core.ResourceOperationConfig { - return core.ResourceOperationConfig{ - ResourceName: "ConfAuthResourceRead", - Type: operationtype.Read, - HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf(repositoryConfAuthURLFormat, c.ControlPlane, d.Get("repository_id")) - }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &ReadRepositoryConfAuthResponse{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Repository conf auth"}, - } -} - -func UpdateConfAuthConfig() core.ResourceOperationConfig { - return core.ResourceOperationConfig{ - ResourceName: "ConfAuthResourceUpdate", - Type: operationtype.Update, - HttpMethod: http.MethodPut, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf(repositoryConfAuthURLFormat, c.ControlPlane, d.Get("repository_id")) - }, - SchemaReaderFactory: func() core.SchemaReader { return &RepositoryConfAuthData{} }, - } -} - -func DeleteConfAuthConfig() core.ResourceOperationConfig { - return core.ResourceOperationConfig{ - ResourceName: "ConfAuthResourceDelete", - Type: operationtype.Delete, - HttpMethod: http.MethodDelete, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf(repositoryConfAuthURLFormat, c.ControlPlane, d.Get("repository_id")) - }, - } -} - -func repositoryConfAuthResourceSchemaV0() *schema.Resource { - return &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": { - Description: "The ID of this resource is set to `repository_id`.", - Type: schema.TypeString, - Computed: true, - }, - "repository_id": { - Description: "The ID of the repository to be configured.", - Type: schema.TypeString, - Required: true, - }, - "allow_native_auth": { - Description: "Should the communication allow native authentication?", - Type: schema.TypeBool, - Optional: true, - }, - "client_tls": { - Description: fmt.Sprintf("Is the repo Client using TLS? Default is %q.", DefaultClientTLS), - Type: schema.TypeString, - Optional: true, - Default: DefaultClientTLS, - }, - "identity_provider": { - Description: fmt.Sprintf( - "The semantics of this field changed in control planes `v4.13` and later. See how "+ - "it should be configured depending on your control plane version:\n"+ - "\t- `v4.12` and below:\n\t\t- Provide the ID (Alias) of the identity provider "+ - "integration to allow user authentication using an IdP.\n"+ - "\t- `v4.13` and later:\n\t\t- If not supplied, then end-user "+ - "authentication is disabled.\n\t\t- If end-user authentication "+ - "with Cyral Access Token is desired, then set to `ACCESS_TOKEN` or any "+ - "other non-empty string.\n\t\t- If end-user authentication with "+ - "AWS IAM is desired, then this must be the ID of an AWS IAM integration, "+ - "and the `auth_type` attribute must be set to `%s`.", - AwsIAMAuthType, - ), - Type: schema.TypeString, - Optional: true, - }, - "repo_tls": { - Description: fmt.Sprintf("Is TLS enabled for the repository? Default is %q.", DefaultRepoTLS), - Type: schema.TypeString, - Optional: true, - Default: DefaultRepoTLS, - }, - "auth_type": { - Description: fmt.Sprintf("Authentication type for this repository. **Note**: `%s` is currently "+ - "only supported by `%s` repo type. List of supported values: %s", - AwsIAMAuthType, repository.MongoDB, utils.SupportedValuesAsMarkdown(authTypes)), - Type: schema.TypeString, - Optional: true, - Default: DefaultAuthType, - ValidateFunc: validation.StringInSlice(authTypes, false), - }, - }, - } -} - -// Previously, the id of the resource `cyral_repository_conf_auth` was hardcoded -// to `repo-conf`, which doesn't make sense. The goal here is to set it to be -// the repository ID. -func UpgradeRepositoryConfAuthV0( - _ context.Context, - rawState map[string]interface{}, - _ interface{}, -) (map[string]interface{}, error) { - rawState["id"] = rawState["repository_id"] - return rawState, nil -} - -func ResourceRepositoryConfAuth() *schema.Resource { - return &schema.Resource{ - Description: "Manages the [Repository Authentication settings](https://cyral.com/docs/manage-repositories/repo-advanced-settings/#authentication) that is shown in the Advanced tab.", - CreateContext: resourceRepositoryConfAuthCreate, - ReadContext: core.ReadResource(ReadConfAuthConfig()), - UpdateContext: core.UpdateResource(UpdateConfAuthConfig(), ReadConfAuthConfig()), - DeleteContext: core.DeleteResource(DeleteConfAuthConfig()), - - SchemaVersion: 1, - StateUpgraders: []schema.StateUpgrader{ - { - Version: 0, - Type: repositoryConfAuthResourceSchemaV0(). - CoreConfigSchema().ImpliedType(), - Upgrade: UpgradeRepositoryConfAuthV0, - }, - }, - - Schema: repositoryConfAuthResourceSchemaV0().Schema, - - Importer: &schema.ResourceImporter{ - StateContext: func( - ctx context.Context, - d *schema.ResourceData, - m interface{}, - ) ([]*schema.ResourceData, error) { - d.Set("repository_id", d.Id()) - return []*schema.ResourceData{d}, nil - }, - }, - } -} diff --git a/cyral/internal/repository/confauth/resource_cyral_repository_conf_auth_test.go b/cyral/internal/repository/confauth/resource_test.go similarity index 100% rename from cyral/internal/repository/confauth/resource_cyral_repository_conf_auth_test.go rename to cyral/internal/repository/confauth/resource_test.go diff --git a/cyral/internal/repository/confauth/schema_loader.go b/cyral/internal/repository/confauth/schema_loader.go new file mode 100644 index 00000000..4066051c --- /dev/null +++ b/cyral/internal/repository/confauth/schema_loader.go @@ -0,0 +1,26 @@ +package confauth + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/core" +) + +type packageSchema struct { +} + +func (p *packageSchema) Name() string { + return "confauth" +} + +func (p *packageSchema) Schemas() []*core.SchemaDescriptor { + return []*core.SchemaDescriptor{ + { + Name: resourceName, + Type: core.ResourceSchemaType, + Schema: resourceSchema, + }, + } +} + +func PackageSchema() core.PackageSchema { + return &packageSchema{} +} diff --git a/cyral/internal/repository/constants.go b/cyral/internal/repository/constants.go new file mode 100644 index 00000000..851bd085 --- /dev/null +++ b/cyral/internal/repository/constants.go @@ -0,0 +1,91 @@ +package repository + +const ( + resourceName = "cyral_repository" + dataSourceName = "cyral_repository" +) + +const ( + // Schema keys. + RepoIDKey = "id" + RepoTypeKey = "type" + RepoNameKey = "name" + RepoLabelsKey = "labels" + // Connection draining keys. + RepoConnDrainingKey = "connection_draining" + RepoConnDrainingAutoKey = "auto" + RepoConnDrainingWaitTimeKey = "wait_time" + // Repo node keys. + RepoNodesKey = "repo_node" + RepoHostKey = "host" + RepoPortKey = "port" + RepoNodeDynamicKey = "dynamic" + // MongoDB settings keys. + RepoMongoDBSettingsKey = "mongodb_settings" + RepoMongoDBReplicaSetNameKey = "replica_set_name" + RepoMongoDBServerTypeKey = "server_type" + RepoMongoDBSRVRecordName = "srv_record_name" + RepoMongoDBFlavorKey = "flavor" +) + +const ( + Denodo = "denodo" + Dremio = "dremio" + DynamoDB = "dynamodb" + DynamoDBStreams = "dynamodbstreams" + Galera = "galera" + MariaDB = "mariadb" + MongoDB = "mongodb" + MySQL = "mysql" + Oracle = "oracle" + PostgreSQL = "postgresql" + Redshift = "redshift" + S3 = "s3" + Snowflake = "snowflake" + SQLServer = "sqlserver" +) + +func RepositoryTypes() []string { + return []string{ + Denodo, + Dremio, + DynamoDB, + DynamoDBStreams, + Galera, + MariaDB, + MongoDB, + MySQL, + Oracle, + PostgreSQL, + Redshift, + S3, + Snowflake, + SQLServer, + } +} + +const ( + ReplicaSet = "replicaset" + Standalone = "standalone" + Sharded = "sharded" +) + +const ( + MongoDBFlavorMongoDB = "mongodb" + MongoDBFlavorDocumentDB = "documentdb" +) + +func mongoServerTypes() []string { + return []string{ + ReplicaSet, + Standalone, + Sharded, + } +} + +func mongoFlavors() []string { + return []string{ + MongoDBFlavorMongoDB, + MongoDBFlavorDocumentDB, + } +} diff --git a/cyral/internal/repository/datamap/constants.go b/cyral/internal/repository/datamap/constants.go new file mode 100644 index 00000000..5ee30406 --- /dev/null +++ b/cyral/internal/repository/datamap/constants.go @@ -0,0 +1,5 @@ +package datamap + +const ( + resourceName = "cyral_repository_datamap" +) diff --git a/cyral/internal/repository/datamap/resource.go b/cyral/internal/repository/datamap/resource.go index 11f228fd..055693e9 100644 --- a/cyral/internal/repository/datamap/resource.go +++ b/cyral/internal/repository/datamap/resource.go @@ -17,7 +17,7 @@ func resourceSchema() *schema.Resource { Description: "Manages [Data Map](https://cyral.com/docs/policy/datamap).", CreateContext: core.CreateResource( core.ResourceOperationConfig{ - ResourceName: "DataMapResourceCreate", + ResourceName: resourceName, Type: operationtype.Create, HttpMethod: http.MethodPut, URLFactory: func(d *schema.ResourceData, c *client.Client) string { @@ -33,7 +33,7 @@ func resourceSchema() *schema.Resource { ReadContext: core.ReadResource(readDataMapConfig), UpdateContext: core.UpdateResource( core.ResourceOperationConfig{ - ResourceName: "DataMapResourceUpdate", + ResourceName: resourceName, Type: operationtype.Update, HttpMethod: http.MethodPut, URLFactory: func(d *schema.ResourceData, c *client.Client) string { @@ -46,7 +46,7 @@ func resourceSchema() *schema.Resource { ), DeleteContext: core.DeleteResource( core.ResourceOperationConfig{ - ResourceName: "DataMapResourceDelete", + ResourceName: resourceName, Type: operationtype.Delete, HttpMethod: http.MethodDelete, URLFactory: func(d *schema.ResourceData, c *client.Client) string { @@ -113,7 +113,7 @@ func resourceSchema() *schema.Resource { } var readDataMapConfig = core.ResourceOperationConfig{ - ResourceName: "DataMapResourceRead", + ResourceName: resourceName, Type: operationtype.Read, HttpMethod: http.MethodGet, URLFactory: func(d *schema.ResourceData, c *client.Client) string { @@ -122,5 +122,5 @@ var readDataMapConfig = core.ResourceOperationConfig{ d.Get("repository_id").(string)) }, SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &DataMap{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Data Map"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: resourceName}, } diff --git a/cyral/internal/repository/datamap/schema_loader.go b/cyral/internal/repository/datamap/schema_loader.go index 647b5bf1..4810b2e5 100644 --- a/cyral/internal/repository/datamap/schema_loader.go +++ b/cyral/internal/repository/datamap/schema_loader.go @@ -14,7 +14,7 @@ func (p *packageSchema) Name() string { func (p *packageSchema) Schemas() []*core.SchemaDescriptor { return []*core.SchemaDescriptor{ { - Name: "cyral_repository_datamap", + Name: resourceName, Type: core.ResourceSchemaType, Schema: resourceSchema, }, diff --git a/cyral/internal/repository/data_source_cyral_repository.go b/cyral/internal/repository/datasource.go similarity index 83% rename from cyral/internal/repository/data_source_cyral_repository.go rename to cyral/internal/repository/datasource.go index d6a25138..914f92dd 100644 --- a/cyral/internal/repository/data_source_cyral_repository.go +++ b/cyral/internal/repository/datasource.go @@ -2,7 +2,6 @@ package repository import ( "fmt" - "net/http" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -10,7 +9,7 @@ import ( "github.com/cyralinc/terraform-provider-cyral/cyral/client" "github.com/cyralinc/terraform-provider-cyral/cyral/core" - "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" "github.com/cyralinc/terraform-provider-cyral/cyral/utils" ) @@ -21,6 +20,10 @@ const ( // GetReposSubResponse is different from GetRepoByIDResponse. For the by-id // response, we expect the ids to be embedded in the RepoInfo struct. For // GetReposSubResponse, the ids come outside of RepoInfo. +// +// Needles to say we need a new API version to fix these issues. For the +// time being, I'm keeping the model here to isolate it from the rest of +// the code - Wilson. type GetReposSubResponse struct { ID string `json:"id"` Repo RepoInfo `json:"repo"` @@ -37,10 +40,10 @@ func (resp *GetReposResponse) WriteToSchema(d *schema.ResourceData) error { RepoIDKey: repo.ID, RepoNameKey: repo.Repo.Name, RepoTypeKey: repo.Repo.Type, - RepoLabelsKey: repo.Repo.LabelsAsInterface(), - RepoConnDrainingKey: repo.Repo.ConnDrainingAsInterface(), - RepoNodesKey: repo.Repo.RepoNodesAsInterface(), - RepoMongoDBSettingsKey: repo.Repo.MongoDBSettingsAsInterface(), + RepoLabelsKey: repo.Repo.Labels.AsInterface(), + RepoConnDrainingKey: repo.Repo.ConnParams.AsInterface(), + RepoNodesKey: repo.Repo.RepoNodes.AsInterface(), + RepoMongoDBSettingsKey: repo.Repo.MongoDBSettings.AsInterface(), } repoList = append(repoList, argumentVals) } @@ -54,29 +57,26 @@ func (resp *GetReposResponse) WriteToSchema(d *schema.ResourceData) error { return nil } -func dataSourceRepositoryReadConfig() core.ResourceOperationConfig { - return core.ResourceOperationConfig{ - ResourceName: "RepositoryDataSourceRead", - Type: operationtype.Read, - HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - nameFilter := d.Get("name").(string) - typeFilter := d.Get("type").(string) - urlParams := utils.UrlQuery(map[string]string{ - "name": nameFilter, - "type": typeFilter, - }) +var dsContextHandler = core.DefaultContextHandler{ + ResourceName: dataSourceName, + ResourceType: resourcetype.DataSource, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &GetReposResponse{} }, + GetPutDeleteURLFactory: func(d *schema.ResourceData, c *client.Client) string { + nameFilter := d.Get("name").(string) + typeFilter := d.Get("type").(string) + urlParams := utils.UrlQuery(map[string]string{ + "name": nameFilter, + "type": typeFilter, + }) - return fmt.Sprintf("https://%s/v1/repos%s", c.ControlPlane, urlParams) - }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &GetReposResponse{} }, - } + return fmt.Sprintf("https://%s/v1/repos%s", c.ControlPlane, urlParams) + }, } -func DataSourceRepository() *schema.Resource { +func dataSourceSchema() *schema.Resource { return &schema.Resource{ Description: "Retrieves a list of repositories. See [`repository_list`](#nestedatt--repository_list).", - ReadContext: core.ReadResource(dataSourceRepositoryReadConfig()), + ReadContext: dsContextHandler.ReadContext(), Schema: map[string]*schema.Schema{ RepoNameKey: { Description: "Filter the results by a regular expression (regex) that matches names of existing repositories.", diff --git a/cyral/internal/repository/data_source_cyral_repository_test.go b/cyral/internal/repository/datasource_test.go similarity index 97% rename from cyral/internal/repository/data_source_cyral_repository_test.go rename to cyral/internal/repository/datasource_test.go index a576f324..8c79d87c 100644 --- a/cyral/internal/repository/data_source_cyral_repository_test.go +++ b/cyral/internal/repository/datasource_test.go @@ -20,8 +20,8 @@ func repositoryDataSourceTestRepos() []repository.RepoInfo { { Name: utils.AccTestName(repositoryDataSourceName, "sqlserver-1"), Type: "sqlserver", - Labels: []string{"rds", "us-east-2"}, - RepoNodes: []*repository.RepoNode{ + Labels: repository.Labels{"rds", "us-east-2"}, + RepoNodes: repository.RepoNodes{ { Host: "sql.local", Port: 3333, @@ -31,8 +31,8 @@ func repositoryDataSourceTestRepos() []repository.RepoInfo { { Name: utils.AccTestName(repositoryDataSourceName, "mongodb-1"), Type: "mongodb", - Labels: []string{"rds", "us-east-1"}, - RepoNodes: []*repository.RepoNode{ + Labels: repository.Labels{"rds", "us-east-1"}, + RepoNodes: repository.RepoNodes{ { Host: "mongo.local", Port: 27017, diff --git a/cyral/internal/repository/model.go b/cyral/internal/repository/model.go new file mode 100644 index 00000000..494bec6b --- /dev/null +++ b/cyral/internal/repository/model.go @@ -0,0 +1,201 @@ +package repository + +import ( + "fmt" + + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type Labels []string +type RepoNodes []*RepoNode + +type RepoInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Host string `json:"repoHost"` + Port uint32 `json:"repoPort"` + ConnParams *ConnParams `json:"connParams"` + Labels Labels `json:"labels"` + RepoNodes RepoNodes `json:"repoNodes,omitempty"` + MongoDBSettings *MongoDBSettings `json:"mongoDbSettings,omitempty"` +} + +type ConnParams struct { + ConnDraining *ConnDraining `json:"connDraining"` +} + +type ConnDraining struct { + Auto bool `json:"auto"` + WaitTime uint32 `json:"waitTime"` +} + +type MongoDBSettings struct { + ReplicaSetName string `json:"replicaSetName,omitempty"` + ServerType string `json:"serverType,omitempty"` + SRVRecordName string `json:"srvRecordName,omitempty"` + Flavor string `json:"flavor,omitempty"` +} + +type RepoNode struct { + Name string `json:"name"` + Host string `json:"host"` + Port uint32 `json:"port"` + Dynamic bool `json:"dynamic"` +} + +type GetRepoByIDResponse struct { + Repo RepoInfo `json:"repo"` +} + +func (res *GetRepoByIDResponse) WriteToSchema(d *schema.ResourceData) error { + return res.Repo.WriteToSchema(d) +} + +func (res *RepoInfo) WriteToSchema(d *schema.ResourceData) error { + d.Set(RepoTypeKey, res.Type) + d.Set(RepoNameKey, res.Name) + d.Set(RepoLabelsKey, res.Labels.AsInterface()) + d.Set(RepoConnDrainingKey, res.ConnParams.AsInterface()) + d.Set(RepoNodesKey, res.RepoNodes.AsInterface()) + d.Set(RepoMongoDBSettingsKey, res.MongoDBSettings.AsInterface()) + return nil +} + +func (r *RepoInfo) ReadFromSchema(d *schema.ResourceData) error { + r.ID = d.Id() + r.Name = d.Get(RepoNameKey).(string) + r.Type = d.Get(RepoTypeKey).(string) + r.Labels = labelsFromInterface(d.Get(RepoLabelsKey).([]interface{})) + r.RepoNodes = repoNodesFromInterface(d.Get(RepoNodesKey).([]interface{})) + r.ConnParams = connDrainingFromInterface(d.Get(RepoConnDrainingKey).(*schema.Set).List()) + var mongoDBSettings = d.Get(RepoMongoDBSettingsKey).(*schema.Set).List() + if r.Type == MongoDB && len(mongoDBSettings) == 0 { + return fmt.Errorf("'%s' block must be provided when '%s=%s'", RepoMongoDBSettingsKey, utils.TypeKey, MongoDB) + } else if r.Type != MongoDB && len(mongoDBSettings) > 0 { + return fmt.Errorf("'%s' block is only allowed when '%s=%s'", RepoMongoDBSettingsKey, utils.TypeKey, MongoDB) + } + m, err := mongoDBSettingsFromInterface(mongoDBSettings) + r.MongoDBSettings = m + return err +} + +func (l *Labels) AsInterface() []interface{} { + if l == nil { + return nil + } + labels := make([]interface{}, len(*l)) + for i, label := range *l { + labels[i] = label + } + return labels +} + +func labelsFromInterface(i []interface{}) Labels { + labels := make([]string, len(i)) + for index, v := range i { + labels[index] = v.(string) + } + return labels +} + +func (c *ConnParams) AsInterface() []interface{} { + if c == nil || c.ConnDraining == nil { + return nil + } + return []interface{}{map[string]interface{}{ + RepoConnDrainingAutoKey: c.ConnDraining.Auto, + RepoConnDrainingWaitTimeKey: c.ConnDraining.WaitTime, + }} +} + +func connDrainingFromInterface(i []interface{}) *ConnParams { + if len(i) == 0 { + return nil + } + return &ConnParams{ + ConnDraining: &ConnDraining{ + Auto: i[0].(map[string]interface{})[RepoConnDrainingAutoKey].(bool), + WaitTime: uint32(i[0].(map[string]interface{})[RepoConnDrainingWaitTimeKey].(int)), + }, + } +} + +func (r *RepoNodes) AsInterface() []interface{} { + if r == nil { + return nil + } + repoNodes := make([]interface{}, len(*r)) + for i, node := range *r { + repoNodes[i] = map[string]interface{}{ + RepoNameKey: node.Name, + RepoHostKey: node.Host, + RepoPortKey: node.Port, + RepoNodeDynamicKey: node.Dynamic, + } + } + return repoNodes +} + +func repoNodesFromInterface(i []interface{}) RepoNodes { + if len(i) == 0 { + return nil + } + repoNodes := make(RepoNodes, len(i)) + for index, nodeInterface := range i { + nodeMap := nodeInterface.(map[string]interface{}) + node := &RepoNode{ + Name: nodeMap[RepoNameKey].(string), + Host: nodeMap[RepoHostKey].(string), + Port: uint32(nodeMap[RepoPortKey].(int)), + Dynamic: nodeMap[RepoNodeDynamicKey].(bool), + } + repoNodes[index] = node + } + return repoNodes +} + +func (m *MongoDBSettings) AsInterface() []interface{} { + if m == nil { + return nil + } + + return []interface{}{map[string]interface{}{ + RepoMongoDBReplicaSetNameKey: m.ReplicaSetName, + RepoMongoDBServerTypeKey: m.ServerType, + RepoMongoDBSRVRecordName: m.SRVRecordName, + RepoMongoDBFlavorKey: m.Flavor, + }} +} + +func mongoDBSettingsFromInterface(i []interface{}) (*MongoDBSettings, error) { + if len(i) == 0 { + return nil, nil + } + var replicaSetName = i[0].(map[string]interface{})[RepoMongoDBReplicaSetNameKey].(string) + var serverType = i[0].(map[string]interface{})[RepoMongoDBServerTypeKey].(string) + var srvRecordName = i[0].(map[string]interface{})[RepoMongoDBSRVRecordName].(string) + if serverType == ReplicaSet && replicaSetName == "" { + return nil, fmt.Errorf("'%s' must be provided when '%s=\"%s\"'", RepoMongoDBReplicaSetNameKey, + RepoMongoDBServerTypeKey, ReplicaSet) + } + if serverType != ReplicaSet && replicaSetName != "" { + return nil, fmt.Errorf("'%s' cannot be provided when '%s=\"%s\"'", RepoMongoDBReplicaSetNameKey, + RepoMongoDBServerTypeKey, serverType) + } + if serverType == Standalone && srvRecordName != "" { + return nil, fmt.Errorf( + "'%s' cannot be provided when '%s=\"%s\"'", + RepoMongoDBSRVRecordName, + RepoMongoDBServerTypeKey, + Standalone, + ) + } + return &MongoDBSettings{ + ReplicaSetName: i[0].(map[string]interface{})[RepoMongoDBReplicaSetNameKey].(string), + ServerType: i[0].(map[string]interface{})[RepoMongoDBServerTypeKey].(string), + SRVRecordName: i[0].(map[string]interface{})[RepoMongoDBSRVRecordName].(string), + Flavor: i[0].(map[string]interface{})[RepoMongoDBFlavorKey].(string), + }, nil +} diff --git a/cyral/internal/repository/network/constants.go b/cyral/internal/repository/network/constants.go new file mode 100644 index 00000000..dc4af230 --- /dev/null +++ b/cyral/internal/repository/network/constants.go @@ -0,0 +1,17 @@ +package network + +import "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository" + +const ( + resourceName = "cyral_repository_network_access_policy" + + defaultNetworkAccessPolicyEnabled = true + defaultNetworkAccessRulesBlockAccess = false +) + +func repositoryTypesNetworkShield() []string { + return []string{ + repository.SQLServer, + repository.Oracle, + } +} diff --git a/cyral/internal/repository/network/model.go b/cyral/internal/repository/network/model.go new file mode 100644 index 00000000..4df7a70c --- /dev/null +++ b/cyral/internal/repository/network/model.go @@ -0,0 +1,79 @@ +package network + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type NetworkAccessPolicyUpsertResp struct { + Policy NetworkAccessPolicy `json:"policy"` +} + +func (resp NetworkAccessPolicyUpsertResp) WriteToSchema(d *schema.ResourceData) error { + return resp.Policy.WriteToSchema(d) +} + +type NetworkAccessPolicy struct { + Enabled bool `json:"enabled"` + NetworkAccessRules `json:"networkAccessRules,omitempty"` +} + +type NetworkAccessRules struct { + RulesBlockAccess bool `json:"rulesBlockAccess"` + Rules []NetworkAccessRule `json:"rules"` +} + +type NetworkAccessRule struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + DBAccounts []string `json:"dbAccounts,omitempty"` + SourceIPs []string `json:"sourceIPs,omitempty"` +} + +func (nap *NetworkAccessPolicy) ReadFromSchema(d *schema.ResourceData) error { + nap.Enabled = d.Get("enabled").(bool) + + var networkAccessRulesIfaces []interface{} + if set, ok := d.GetOk("network_access_rule"); ok { + networkAccessRulesIfaces = set.(*schema.Set).List() + } else { + return nil + } + + nap.NetworkAccessRules = NetworkAccessRules{ + RulesBlockAccess: d.Get("network_access_rules_block_access").(bool), + Rules: []NetworkAccessRule{}, + } + for _, networkAccessRuleIface := range networkAccessRulesIfaces { + networkAccessRuleMap := networkAccessRuleIface.(map[string]interface{}) + nap.NetworkAccessRules.Rules = append(nap.NetworkAccessRules.Rules, + NetworkAccessRule{ + Name: networkAccessRuleMap["name"].(string), + Description: networkAccessRuleMap["description"].(string), + DBAccounts: utils.GetStrList(networkAccessRuleMap, "db_accounts"), + SourceIPs: utils.GetStrList(networkAccessRuleMap, "source_ips"), + }) + } + + return nil +} + +func (nap *NetworkAccessPolicy) WriteToSchema(d *schema.ResourceData) error { + d.SetId(d.Get("repository_id").(string)) + d.Set("enabled", nap.Enabled) + d.Set("network_access_rules_block_access", nap.NetworkAccessRules.RulesBlockAccess) + + var networkAccessRules []interface{} + for _, rule := range nap.NetworkAccessRules.Rules { + rulesMap := map[string]interface{}{ + "name": rule.Name, + "description": rule.Description, + "db_accounts": rule.DBAccounts, + "source_ips": rule.SourceIPs, + } + networkAccessRules = append(networkAccessRules, rulesMap) + } + + return d.Set("network_access_rule", networkAccessRules) +} diff --git a/cyral/internal/repository/network/resource.go b/cyral/internal/repository/network/resource.go new file mode 100644 index 00000000..69fa3424 --- /dev/null +++ b/cyral/internal/repository/network/resource.go @@ -0,0 +1,124 @@ +package network + +import ( + "context" + "fmt" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var urlFactory = func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/repos/%s/networkAccessPolicy", + c.ControlPlane, + d.Get("repository_id"), + ) +} + +var resourceContextHandler = core.DefaultContextHandler{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &NetworkAccessPolicy{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &NetworkAccessPolicy{} }, + SchemaWriterFactoryPostMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &NetworkAccessPolicyUpsertResp{} }, + BaseURLFactory: urlFactory, + GetPutDeleteURLFactory: urlFactory, +} + +func resourceSchema() *schema.Resource { + return &schema.Resource{ + Description: "Manages the network access policy of a repository. Network access policies are" + + " also known as the [Network Shield](https://cyral.com/docs/manage-repositories/network-shield/)." + + " This feature is supported for the following repository types:" + + utils.SupportedValuesAsMarkdown(repositoryTypesNetworkShield()) + + "\n\n-> **Note** If you also use the resource `cyral_repository_conf_auth` for the same repository," + + " create a `depends_on` relationship from this resource to the `cyral_repository_conf_auth` to" + + " avoid errors when running `terraform destroy`.", + CreateContext: resourceContextHandler.CreateContext(), + ReadContext: resourceContextHandler.ReadContext(), + UpdateContext: resourceContextHandler.UpdateContext(), + DeleteContext: resourceContextHandler.DeleteContext(), + + Schema: map[string]*schema.Schema{ + "repository_id": { + Description: "ID of the repository for which to configure a network access policy.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + // This parameter also exists in the + // v1/repos/{repoID}/conf/auth API, but putting it under + // the `cyral_repository_conf_auth` resource was causing + // a lot of trouble: the resources would get out of sync + // and behave like crazy. + "enabled": { + Description: fmt.Sprintf("Is the network access policy enabled? Default is %t.", defaultNetworkAccessPolicyEnabled), + Type: schema.TypeBool, + Optional: true, + Default: defaultNetworkAccessPolicyEnabled, + }, + + "network_access_rules_block_access": { + Description: fmt.Sprintf("Determines what happens if an incoming connection matches one of the rules in `network_access_rule`. If set to true, the connection is blocked if it matches some rule (and allowed otherwise). Otherwise set to false, the connection is allowed only if it matches some rule. Default is %t.", defaultNetworkAccessRulesBlockAccess), + Type: schema.TypeBool, + Optional: true, + }, + + "network_access_rule": { + Description: "Network access policy that decides whether access should be granted based on a set of rules.", + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Description: "Name of the rule.", + Type: schema.TypeString, + Required: true, + }, + "description": { + Description: "Description of the network access policy.", + Type: schema.TypeString, + Optional: true, + }, + "db_accounts": { + Description: "Specify which accounts this rule applies to. The account name must match an existing account in your database.", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "source_ips": { + Description: "Specify IPs to restrict the range of allowed IP addresses for this rule.", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + + "id": { + Description: "ID of this resource in the Cyral environment.", + Type: schema.TypeString, + Computed: true, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: func( + ctx context.Context, + d *schema.ResourceData, + m interface{}, + ) ([]*schema.ResourceData, error) { + d.Set("repository_id", d.Id()) + return []*schema.ResourceData{d}, nil + }, + }, + } +} diff --git a/cyral/internal/repository/network/resource_cyral_repository_network_access_policy.go b/cyral/internal/repository/network/resource_cyral_repository_network_access_policy.go deleted file mode 100644 index a78ec4b4..00000000 --- a/cyral/internal/repository/network/resource_cyral_repository_network_access_policy.go +++ /dev/null @@ -1,251 +0,0 @@ -package network - -import ( - "context" - "fmt" - "net/http" - - "github.com/cyralinc/terraform-provider-cyral/cyral/client" - "github.com/cyralinc/terraform-provider-cyral/cyral/core" - "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository" - "github.com/cyralinc/terraform-provider-cyral/cyral/utils" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -const ( - repositoryNetworkAccessPolicyURLFormat = "https://%s/v1/repos/%s/networkAccessPolicy" - - defaultNetworkAccessPolicyEnabled = true - defaultNetworkAccessRulesBlockAccess = false -) - -func repositoryTypesNetworkShield() []string { - return []string{ - repository.SQLServer, - repository.Oracle, - } -} - -type NetworkAccessPolicyUpsertResp struct { - Policy NetworkAccessPolicy `json:"policy"` -} - -func (resp NetworkAccessPolicyUpsertResp) WriteToSchema(d *schema.ResourceData) error { - return resp.Policy.WriteToSchema(d) -} - -type NetworkAccessPolicy struct { - Enabled bool `json:"enabled"` - NetworkAccessRules `json:"networkAccessRules,omitempty"` -} - -type NetworkAccessRules struct { - RulesBlockAccess bool `json:"rulesBlockAccess"` - Rules []NetworkAccessRule `json:"rules"` -} - -type NetworkAccessRule struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` - DBAccounts []string `json:"dbAccounts,omitempty"` - SourceIPs []string `json:"sourceIPs,omitempty"` -} - -func (nap *NetworkAccessPolicy) ReadFromSchema(d *schema.ResourceData) error { - nap.Enabled = d.Get("enabled").(bool) - - var networkAccessRulesIfaces []interface{} - if set, ok := d.GetOk("network_access_rule"); ok { - networkAccessRulesIfaces = set.(*schema.Set).List() - } else { - return nil - } - - nap.NetworkAccessRules = NetworkAccessRules{ - RulesBlockAccess: d.Get("network_access_rules_block_access").(bool), - Rules: []NetworkAccessRule{}, - } - for _, networkAccessRuleIface := range networkAccessRulesIfaces { - networkAccessRuleMap := networkAccessRuleIface.(map[string]interface{}) - nap.NetworkAccessRules.Rules = append(nap.NetworkAccessRules.Rules, - NetworkAccessRule{ - Name: networkAccessRuleMap["name"].(string), - Description: networkAccessRuleMap["description"].(string), - DBAccounts: utils.GetStrList(networkAccessRuleMap, "db_accounts"), - SourceIPs: utils.GetStrList(networkAccessRuleMap, "source_ips"), - }) - } - - return nil -} - -func (nap *NetworkAccessPolicy) WriteToSchema(d *schema.ResourceData) error { - d.SetId(d.Get("repository_id").(string)) - d.Set("enabled", nap.Enabled) - d.Set("network_access_rules_block_access", nap.NetworkAccessRules.RulesBlockAccess) - - var networkAccessRules []interface{} - for _, rule := range nap.NetworkAccessRules.Rules { - rulesMap := map[string]interface{}{ - "name": rule.Name, - "description": rule.Description, - "db_accounts": rule.DBAccounts, - "source_ips": rule.SourceIPs, - } - networkAccessRules = append(networkAccessRules, rulesMap) - } - - return d.Set("network_access_rule", networkAccessRules) -} - -func createRepositoryNetworkAccessPolicy() core.ResourceOperationConfig { - return core.ResourceOperationConfig{ - ResourceName: "RepositoryNetworkAccessPolicyCreate", - Type: operationtype.Create, - HttpMethod: http.MethodPost, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf(repositoryNetworkAccessPolicyURLFormat, - c.ControlPlane, d.Get("repository_id")) - }, - SchemaReaderFactory: func() core.SchemaReader { return &NetworkAccessPolicy{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &NetworkAccessPolicyUpsertResp{} }, - } -} - -func readRepositoryNetworkAccessPolicy() core.ResourceOperationConfig { - return core.ResourceOperationConfig{ - ResourceName: "RepositoryNetworkAccessPolicyRead", - Type: operationtype.Read, - HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf(repositoryNetworkAccessPolicyURLFormat, - c.ControlPlane, d.Get("repository_id")) - }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &NetworkAccessPolicy{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Repository network access policy"}, - } -} - -func updateRepositoryNetworkAccessPolicy() core.ResourceOperationConfig { - return core.ResourceOperationConfig{ - ResourceName: "RepositoryNetworkAccessPolicyUpdate", - Type: operationtype.Update, - HttpMethod: http.MethodPut, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf(repositoryNetworkAccessPolicyURLFormat, - c.ControlPlane, d.Get("repository_id")) - }, - SchemaReaderFactory: func() core.SchemaReader { return &NetworkAccessPolicy{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &NetworkAccessPolicyUpsertResp{} }, - } -} - -func deleteRepositoryNetworkAccessPolicy() core.ResourceOperationConfig { - return core.ResourceOperationConfig{ - ResourceName: "RepositoryNetworkAccessPolicyDelete", - Type: operationtype.Delete, - HttpMethod: http.MethodDelete, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf(repositoryNetworkAccessPolicyURLFormat, - c.ControlPlane, d.Get("repository_id")) - }, - RequestErrorHandler: &core.DeleteIgnoreHttpNotFound{ResName: "Network Access Policy"}, - } -} - -func ResourceRepositoryNetworkAccessPolicy() *schema.Resource { - return &schema.Resource{ - Description: "Manages the network access policy of a repository. Network access policies are" + - " also known as the [Network Shield](https://cyral.com/docs/manage-repositories/network-shield/)." + - " This feature is supported for the following repository types:" + - utils.SupportedValuesAsMarkdown(repositoryTypesNetworkShield()) + - "\n\n-> **Note** If you also use the resource `cyral_repository_conf_auth` for the same repository," + - " create a `depends_on` relationship from this resource to the `cyral_repository_conf_auth` to" + - " avoid errors when running `terraform destroy`.", - CreateContext: core.CreateResource(createRepositoryNetworkAccessPolicy(), readRepositoryNetworkAccessPolicy()), - ReadContext: core.ReadResource(readRepositoryNetworkAccessPolicy()), - UpdateContext: core.UpdateResource(updateRepositoryNetworkAccessPolicy(), readRepositoryNetworkAccessPolicy()), - DeleteContext: core.DeleteResource(deleteRepositoryNetworkAccessPolicy()), - - Schema: map[string]*schema.Schema{ - "repository_id": { - Description: "ID of the repository for which to configure a network access policy.", - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - - // This parameter also exists in the - // v1/repos/{repoID}/conf/auth API, but putting it under - // the `cyral_repository_conf_auth` resource was causing - // a lot of trouble: the resources would get out of sync - // and behave like crazy. - "enabled": { - Description: fmt.Sprintf("Is the network access policy enabled? Default is %t.", defaultNetworkAccessPolicyEnabled), - Type: schema.TypeBool, - Optional: true, - Default: defaultNetworkAccessPolicyEnabled, - }, - - "network_access_rules_block_access": { - Description: fmt.Sprintf("Determines what happens if an incoming connection matches one of the rules in `network_access_rule`. If set to true, the connection is blocked if it matches some rule (and allowed otherwise). Otherwise set to false, the connection is allowed only if it matches some rule. Default is %t.", defaultNetworkAccessRulesBlockAccess), - Type: schema.TypeBool, - Optional: true, - }, - - "network_access_rule": { - Description: "Network access policy that decides whether access should be granted based on a set of rules.", - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Description: "Name of the rule.", - Type: schema.TypeString, - Required: true, - }, - "description": { - Description: "Description of the network access policy.", - Type: schema.TypeString, - Optional: true, - }, - "db_accounts": { - Description: "Specify which accounts this rule applies to. The account name must match an existing account in your database.", - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "source_ips": { - Description: "Specify IPs to restrict the range of allowed IP addresses for this rule.", - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - }, - }, - }, - - "id": { - Description: "ID of this resource in the Cyral environment.", - Type: schema.TypeString, - Computed: true, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: func( - ctx context.Context, - d *schema.ResourceData, - m interface{}, - ) ([]*schema.ResourceData, error) { - d.Set("repository_id", d.Id()) - return []*schema.ResourceData{d}, nil - }, - }, - } -} diff --git a/cyral/internal/repository/network/resource_cyral_repository_network_access_policy_test.go b/cyral/internal/repository/network/resource_test.go similarity index 100% rename from cyral/internal/repository/network/resource_cyral_repository_network_access_policy_test.go rename to cyral/internal/repository/network/resource_test.go diff --git a/cyral/internal/repository/network/schema_loader.go b/cyral/internal/repository/network/schema_loader.go new file mode 100644 index 00000000..6444de7c --- /dev/null +++ b/cyral/internal/repository/network/schema_loader.go @@ -0,0 +1,26 @@ +package network + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/core" +) + +type packageSchema struct { +} + +func (p *packageSchema) Name() string { + return "network" +} + +func (p *packageSchema) Schemas() []*core.SchemaDescriptor { + return []*core.SchemaDescriptor{ + { + Name: resourceName, + Type: core.ResourceSchemaType, + Schema: resourceSchema, + }, + } +} + +func PackageSchema() core.PackageSchema { + return &packageSchema{} +} diff --git a/cyral/internal/repository/resource.go b/cyral/internal/repository/resource.go new file mode 100644 index 00000000..37a3ac4c --- /dev/null +++ b/cyral/internal/repository/resource.go @@ -0,0 +1,179 @@ +package repository + +import ( + "fmt" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +var resourceContextHandler = core.DefaultContextHandler{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &RepoInfo{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &GetRepoByIDResponse{} }, + BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf( + "https://%s/v1/repos", + c.ControlPlane, + ) + }, +} + +func resourceSchema() *schema.Resource { + return &schema.Resource{ + Description: "Manages [repositories](https://cyral.com/docs/manage-repositories/repo-track)." + + "\n\nSee also [Cyral Repository Configuration Module](https://github.com/cyralinc/terraform-cyral-repository-config)." + + "\nThis module provides the repository configuration options as shown in Cyral UI.", + CreateContext: resourceContextHandler.CreateContext(), + ReadContext: resourceContextHandler.ReadContext(), + UpdateContext: resourceContextHandler.UpdateContext(), + DeleteContext: resourceContextHandler.DeleteContext(), + Schema: map[string]*schema.Schema{ + RepoIDKey: { + Description: "ID of this resource in Cyral environment.", + Type: schema.TypeString, + Computed: true, + }, + RepoTypeKey: { + Description: "Repository type. List of supported types:" + utils.SupportedValuesAsMarkdown(RepositoryTypes()), + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(RepositoryTypes(), false), + }, + RepoNameKey: { + Description: "Repository name that will be used internally in the control plane (ex: `your_repo_name`).", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + RepoLabelsKey: { + Description: "Labels enable you to categorize your repository.", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + RepoConnDrainingKey: { + Description: "Parameters related to connection draining.", + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + RepoConnDrainingAutoKey: { + Description: "Whether connections should be drained automatically after a listener dies.", + Type: schema.TypeBool, + Optional: true, + }, + RepoConnDrainingWaitTimeKey: { + Description: "Seconds to wait to let connections drain before starting to kill all the connections, " + + "if auto is set to true.", + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + RepoNodesKey: { + Description: "List of nodes for this repository.", + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + RepoNameKey: { + Description: "Name of the repo node.", + Type: schema.TypeString, + Optional: true, + }, + RepoHostKey: { + Description: "Repo node host (ex: `somerepo.cyral.com`). Can be empty if node is dynamic.", + Type: schema.TypeString, + Optional: true, + }, + RepoPortKey: { + Description: "Repository access port (ex: `3306`). Can be empty if node is dynamic.", + Type: schema.TypeInt, + Optional: true, + }, + RepoNodeDynamicKey: { + Description: "*Only supported for MongoDB in cluster configurations.*\n" + + "Indicates if the node is dynamically discovered, meaning that the sidecar " + + "will query the cluster to get the topology information and discover the " + + "addresses of the dynamic nodes. If set to `true`, `host` and `port` must " + + "be empty. A node with value of this field as false considered `static`.\n" + + "The following conditions apply: \n" + + " - The total number of declared `" + RepoNodesKey + "` blocks must match " + + "the actual number of nodes in the cluster.\n" + + " - If there are static nodes in the configuration, they must be declared " + + "before all dynamic nodes.\n" + + " - See the MongoDB-specific configuration in the [" + RepoMongoDBSettingsKey + + "](#nested-schema-for-" + RepoMongoDBSettingsKey + ").", + Type: schema.TypeBool, + Optional: true, + }, + }, + }, + }, + RepoMongoDBSettingsKey: { + Description: "Parameters related to MongoDB repositories.", + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + RepoMongoDBReplicaSetNameKey: { + Description: "Name of the replica set, if applicable.", + Type: schema.TypeString, + Optional: true, + }, + RepoMongoDBServerTypeKey: { + Description: "Type of the MongoDB server. Allowed values: " + utils.SupportedValuesAsMarkdown(mongoServerTypes()) + + "\n\n The following conditions apply:\n" + + " - If `" + Sharded + "` and `" + RepoMongoDBSRVRecordName + "` *not* provided, then all `" + + RepoNodesKey + "` blocks must be static (see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")).\n" + + " - If `" + Sharded + "` and `" + RepoMongoDBSRVRecordName + "` provided, then all `" + + RepoNodesKey + "` blocks must be dynamic (see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")).\n" + + " - If `" + Standalone + "`, then only one `" + RepoNodesKey + + "` block can be declared and it must be static (see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")). The `" + + RepoMongoDBSRVRecordName + "` is not supported in this configuration.\n" + + " - If `" + ReplicaSet + "` and `" + RepoMongoDBSRVRecordName + "` *not* provided, then `" + + RepoNodesKey + "` blocks may mix dynamic and static nodes (see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")).\n" + + " - If `" + ReplicaSet + "` and `" + RepoMongoDBSRVRecordName + "` provided, then `" + + RepoNodesKey + "` blocks must be dynamic (see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")).\n", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(mongoServerTypes(), false), + }, + RepoMongoDBSRVRecordName: { + Description: "Name of a DNS SRV record which contains cluster topology details. " + + "If specified, then all `" + RepoNodesKey + "` blocks must be declared dynamic " + + "(see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")). " + + "Only supported for `" + RepoMongoDBServerTypeKey + "=\"" + Sharded + "\"` or `" + + RepoMongoDBServerTypeKey + "=\"" + ReplicaSet + "\".", + Type: schema.TypeString, + Optional: true, + }, + RepoMongoDBFlavorKey: { + Description: "The flavor of the MongoDB deployment. Allowed values: " + utils.SupportedValuesAsMarkdown(mongoFlavors()) + + "\n\n The following conditions apply:\n" + + " - The `" + MongoDBFlavorDocumentDB + "` flavor cannot be combined with the MongoDB Server type `" + Sharded + "`.\n", + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(mongoFlavors(), false), + }, + }, + }, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} diff --git a/cyral/internal/repository/resource_cyral_repository.go b/cyral/internal/repository/resource_cyral_repository.go deleted file mode 100644 index 1d1631fb..00000000 --- a/cyral/internal/repository/resource_cyral_repository.go +++ /dev/null @@ -1,502 +0,0 @@ -package repository - -import ( - "fmt" - "net/http" - - "github.com/cyralinc/terraform-provider-cyral/cyral/client" - "github.com/cyralinc/terraform-provider-cyral/cyral/core" - "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" - "github.com/cyralinc/terraform-provider-cyral/cyral/utils" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -const ( - // Schema keys. - RepoIDKey = "id" - RepoTypeKey = "type" - RepoNameKey = "name" - RepoLabelsKey = "labels" - // Connection draining keys. - RepoConnDrainingKey = "connection_draining" - RepoConnDrainingAutoKey = "auto" - RepoConnDrainingWaitTimeKey = "wait_time" - // Repo node keys. - RepoNodesKey = "repo_node" - RepoHostKey = "host" - RepoPortKey = "port" - RepoNodeDynamicKey = "dynamic" - // MongoDB settings keys. - RepoMongoDBSettingsKey = "mongodb_settings" - RepoMongoDBReplicaSetNameKey = "replica_set_name" - RepoMongoDBServerTypeKey = "server_type" - RepoMongoDBSRVRecordName = "srv_record_name" - RepoMongoDBFlavorKey = "flavor" -) - -const ( - Denodo = "denodo" - Dremio = "dremio" - DynamoDB = "dynamodb" - DynamoDBStreams = "dynamodbstreams" - Galera = "galera" - MariaDB = "mariadb" - MongoDB = "mongodb" - MySQL = "mysql" - Oracle = "oracle" - PostgreSQL = "postgresql" - Redshift = "redshift" - S3 = "s3" - Snowflake = "snowflake" - SQLServer = "sqlserver" -) - -func RepositoryTypes() []string { - return []string{ - Denodo, - Dremio, - DynamoDB, - DynamoDBStreams, - Galera, - MariaDB, - MongoDB, - MySQL, - Oracle, - PostgreSQL, - Redshift, - S3, - Snowflake, - SQLServer, - } -} - -const ( - ReplicaSet = "replicaset" - Standalone = "standalone" - Sharded = "sharded" -) - -const ( - MongoDBFlavorMongoDB = "mongodb" - MongoDBFlavorDocumentDB = "documentdb" -) - -func mongoServerTypes() []string { - return []string{ - ReplicaSet, - Standalone, - Sharded, - } -} - -func mongoFlavors() []string { - return []string{ - MongoDBFlavorMongoDB, - MongoDBFlavorDocumentDB, - } -} - -type RepoInfo struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Host string `json:"repoHost"` - Port uint32 `json:"repoPort"` - ConnParams *ConnParams `json:"connParams"` - Labels []string `json:"labels"` - RepoNodes []*RepoNode `json:"repoNodes,omitempty"` - MongoDBSettings *MongoDBSettings `json:"mongoDbSettings,omitempty"` -} - -type ConnParams struct { - ConnDraining *ConnDraining `json:"connDraining"` -} - -type ConnDraining struct { - Auto bool `json:"auto"` - WaitTime uint32 `json:"waitTime"` -} - -type MongoDBSettings struct { - ReplicaSetName string `json:"replicaSetName,omitempty"` - ServerType string `json:"serverType,omitempty"` - SRVRecordName string `json:"srvRecordName,omitempty"` - Flavor string `json:"flavor,omitempty"` -} - -type RepoNode struct { - Name string `json:"name"` - Host string `json:"host"` - Port uint32 `json:"port"` - Dynamic bool `json:"dynamic"` -} - -type GetRepoByIDResponse struct { - Repo RepoInfo `json:"repo"` -} - -func (res *GetRepoByIDResponse) WriteToSchema(d *schema.ResourceData) error { - return res.Repo.WriteToSchema(d) -} - -func (res *RepoInfo) WriteToSchema(d *schema.ResourceData) error { - d.Set(RepoTypeKey, res.Type) - d.Set(RepoNameKey, res.Name) - d.Set(RepoLabelsKey, res.LabelsAsInterface()) - d.Set(RepoConnDrainingKey, res.ConnDrainingAsInterface()) - d.Set(RepoNodesKey, res.RepoNodesAsInterface()) - d.Set(RepoMongoDBSettingsKey, res.MongoDBSettingsAsInterface()) - return nil -} - -func (r *RepoInfo) ReadFromSchema(d *schema.ResourceData) error { - r.ID = d.Id() - r.Name = d.Get(RepoNameKey).(string) - r.Type = d.Get(RepoTypeKey).(string) - r.LabelsFromInterface(d.Get(RepoLabelsKey).([]interface{})) - r.RepoNodesFromInterface(d.Get(RepoNodesKey).([]interface{})) - r.ConnDrainingFromInterface(d.Get(RepoConnDrainingKey).(*schema.Set).List()) - var mongoDBSettings = d.Get(RepoMongoDBSettingsKey).(*schema.Set).List() - if r.Type == MongoDB && (mongoDBSettings == nil || len(mongoDBSettings) == 0) { - return fmt.Errorf("'%s' block must be provided when '%s=%s'", RepoMongoDBSettingsKey, utils.TypeKey, MongoDB) - } else if r.Type != MongoDB && len(mongoDBSettings) > 0 { - return fmt.Errorf("'%s' block is only allowed when '%s=%s'", RepoMongoDBSettingsKey, utils.TypeKey, MongoDB) - } - return r.MongoDBSettingsFromInterface(mongoDBSettings) -} - -func (r *RepoInfo) LabelsAsInterface() []interface{} { - if r.Labels == nil { - return nil - } - labels := make([]interface{}, len(r.Labels)) - for i, label := range r.Labels { - labels[i] = label - } - return labels -} - -func (r *RepoInfo) LabelsFromInterface(i []interface{}) { - labels := make([]string, len(i)) - for index, v := range i { - labels[index] = v.(string) - } - r.Labels = labels -} - -func (r *RepoInfo) ConnDrainingAsInterface() []interface{} { - if r.ConnParams == nil || r.ConnParams.ConnDraining == nil { - return nil - } - - return []interface{}{map[string]interface{}{ - RepoConnDrainingAutoKey: r.ConnParams.ConnDraining.Auto, - RepoConnDrainingWaitTimeKey: r.ConnParams.ConnDraining.WaitTime, - }} -} - -func (r *RepoInfo) ConnDrainingFromInterface(i []interface{}) { - if len(i) == 0 { - return - } - r.ConnParams = &ConnParams{ - ConnDraining: &ConnDraining{ - Auto: i[0].(map[string]interface{})[RepoConnDrainingAutoKey].(bool), - WaitTime: uint32(i[0].(map[string]interface{})[RepoConnDrainingWaitTimeKey].(int)), - }, - } -} - -func (r *RepoInfo) RepoNodesAsInterface() []interface{} { - if r.RepoNodes == nil { - return nil - } - repoNodes := make([]interface{}, len(r.RepoNodes)) - for i, node := range r.RepoNodes { - repoNodes[i] = map[string]interface{}{ - RepoNameKey: node.Name, - RepoHostKey: node.Host, - RepoPortKey: node.Port, - RepoNodeDynamicKey: node.Dynamic, - } - } - return repoNodes -} - -func (r *RepoInfo) RepoNodesFromInterface(i []interface{}) { - if len(i) == 0 { - return - } - repoNodes := make([]*RepoNode, len(i)) - for index, nodeInterface := range i { - nodeMap := nodeInterface.(map[string]interface{}) - node := &RepoNode{ - Name: nodeMap[RepoNameKey].(string), - Host: nodeMap[RepoHostKey].(string), - Port: uint32(nodeMap[RepoPortKey].(int)), - Dynamic: nodeMap[RepoNodeDynamicKey].(bool), - } - repoNodes[index] = node - } - r.RepoNodes = repoNodes -} - -func (r *RepoInfo) MongoDBSettingsAsInterface() []interface{} { - if r.MongoDBSettings == nil { - return nil - } - - return []interface{}{map[string]interface{}{ - RepoMongoDBReplicaSetNameKey: r.MongoDBSettings.ReplicaSetName, - RepoMongoDBServerTypeKey: r.MongoDBSettings.ServerType, - RepoMongoDBSRVRecordName: r.MongoDBSettings.SRVRecordName, - RepoMongoDBFlavorKey: r.MongoDBSettings.Flavor, - }} -} - -func (r *RepoInfo) MongoDBSettingsFromInterface(i []interface{}) error { - if len(i) == 0 { - return nil - } - var replicaSetName = i[0].(map[string]interface{})[RepoMongoDBReplicaSetNameKey].(string) - var serverType = i[0].(map[string]interface{})[RepoMongoDBServerTypeKey].(string) - var srvRecordName = i[0].(map[string]interface{})[RepoMongoDBSRVRecordName].(string) - if serverType == ReplicaSet && replicaSetName == "" { - return fmt.Errorf("'%s' must be provided when '%s=\"%s\"'", RepoMongoDBReplicaSetNameKey, - RepoMongoDBServerTypeKey, ReplicaSet) - } - if serverType != ReplicaSet && replicaSetName != "" { - return fmt.Errorf("'%s' cannot be provided when '%s=\"%s\"'", RepoMongoDBReplicaSetNameKey, - RepoMongoDBServerTypeKey, serverType) - } - if serverType == Standalone && srvRecordName != "" { - return fmt.Errorf( - "'%s' cannot be provided when '%s=\"%s\"'", - RepoMongoDBSRVRecordName, - RepoMongoDBServerTypeKey, - Standalone, - ) - } - r.MongoDBSettings = &MongoDBSettings{ - ReplicaSetName: i[0].(map[string]interface{})[RepoMongoDBReplicaSetNameKey].(string), - ServerType: i[0].(map[string]interface{})[RepoMongoDBServerTypeKey].(string), - SRVRecordName: i[0].(map[string]interface{})[RepoMongoDBSRVRecordName].(string), - Flavor: i[0].(map[string]interface{})[RepoMongoDBFlavorKey].(string), - } - return nil -} - -var ReadRepositoryConfig = core.ResourceOperationConfig{ - ResourceName: "RepositoryRead", - Type: operationtype.Read, - HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf( - "https://%s/v1/repos/%s", - c.ControlPlane, - d.Id(), - ) - }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { - return &GetRepoByIDResponse{} - }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Repository"}, -} - -func ResourceRepository() *schema.Resource { - return &schema.Resource{ - Description: "Manages [repositories](https://cyral.com/docs/manage-repositories/repo-track)." + - "\n\nSee also [Cyral Repository Configuration Module](https://github.com/cyralinc/terraform-cyral-repository-config)." + - "\nThis module provides the repository configuration options as shown in Cyral UI.", - CreateContext: core.CreateResource( - core.ResourceOperationConfig{ - ResourceName: "RepositoryCreate", - Type: operationtype.Create, - HttpMethod: http.MethodPost, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf( - "https://%s/v1/repos", - c.ControlPlane, - ) - }, - SchemaReaderFactory: func() core.SchemaReader { return &RepoInfo{} }, - }, - ReadRepositoryConfig, - ), - ReadContext: core.ReadResource(ReadRepositoryConfig), - UpdateContext: core.UpdateResource( - core.ResourceOperationConfig{ - ResourceName: "RepositoryUpdate", - Type: operationtype.Update, - HttpMethod: http.MethodPut, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf( - "https://%s/v1/repos/%s", - c.ControlPlane, - d.Id(), - ) - }, - SchemaReaderFactory: func() core.SchemaReader { return &RepoInfo{} }, - }, - ReadRepositoryConfig, - ), - DeleteContext: core.DeleteResource( - core.ResourceOperationConfig{ - ResourceName: "RepositoryDelete", - Type: operationtype.Delete, - HttpMethod: http.MethodDelete, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf( - "https://%s/v1/repos/%s", - c.ControlPlane, - d.Id(), - ) - }, - }, - ), - - Schema: map[string]*schema.Schema{ - RepoIDKey: { - Description: "ID of this resource in Cyral environment.", - Type: schema.TypeString, - Computed: true, - }, - RepoTypeKey: { - Description: "Repository type. List of supported types:" + utils.SupportedValuesAsMarkdown(RepositoryTypes()), - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice(RepositoryTypes(), false), - }, - RepoNameKey: { - Description: "Repository name that will be used internally in the control plane (ex: `your_repo_name`).", - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - RepoLabelsKey: { - Description: "Labels enable you to categorize your repository.", - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - RepoConnDrainingKey: { - Description: "Parameters related to connection draining.", - Type: schema.TypeSet, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - RepoConnDrainingAutoKey: { - Description: "Whether connections should be drained automatically after a listener dies.", - Type: schema.TypeBool, - Optional: true, - }, - RepoConnDrainingWaitTimeKey: { - Description: "Seconds to wait to let connections drain before starting to kill all the connections, " + - "if auto is set to true.", - Type: schema.TypeInt, - Optional: true, - }, - }, - }, - }, - RepoNodesKey: { - Description: "List of nodes for this repository.", - Type: schema.TypeList, - Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - RepoNameKey: { - Description: "Name of the repo node.", - Type: schema.TypeString, - Optional: true, - }, - RepoHostKey: { - Description: "Repo node host (ex: `somerepo.cyral.com`). Can be empty if node is dynamic.", - Type: schema.TypeString, - Optional: true, - }, - RepoPortKey: { - Description: "Repository access port (ex: `3306`). Can be empty if node is dynamic.", - Type: schema.TypeInt, - Optional: true, - }, - RepoNodeDynamicKey: { - Description: "*Only supported for MongoDB in cluster configurations.*\n" + - "Indicates if the node is dynamically discovered, meaning that the sidecar " + - "will query the cluster to get the topology information and discover the " + - "addresses of the dynamic nodes. If set to `true`, `host` and `port` must " + - "be empty. A node with value of this field as false considered `static`.\n" + - "The following conditions apply: \n" + - " - The total number of declared `" + RepoNodesKey + "` blocks must match " + - "the actual number of nodes in the cluster.\n" + - " - If there are static nodes in the configuration, they must be declared " + - "before all dynamic nodes.\n" + - " - See the MongoDB-specific configuration in the [" + RepoMongoDBSettingsKey + - "](#nested-schema-for-" + RepoMongoDBSettingsKey + ").", - Type: schema.TypeBool, - Optional: true, - }, - }, - }, - }, - RepoMongoDBSettingsKey: { - Description: "Parameters related to MongoDB repositories.", - Type: schema.TypeSet, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - RepoMongoDBReplicaSetNameKey: { - Description: "Name of the replica set, if applicable.", - Type: schema.TypeString, - Optional: true, - }, - RepoMongoDBServerTypeKey: { - Description: "Type of the MongoDB server. Allowed values: " + utils.SupportedValuesAsMarkdown(mongoServerTypes()) + - "\n\n The following conditions apply:\n" + - " - If `" + Sharded + "` and `" + RepoMongoDBSRVRecordName + "` *not* provided, then all `" + - RepoNodesKey + "` blocks must be static (see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")).\n" + - " - If `" + Sharded + "` and `" + RepoMongoDBSRVRecordName + "` provided, then all `" + - RepoNodesKey + "` blocks must be dynamic (see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")).\n" + - " - If `" + Standalone + "`, then only one `" + RepoNodesKey + - "` block can be declared and it must be static (see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")). The `" + - RepoMongoDBSRVRecordName + "` is not supported in this configuration.\n" + - " - If `" + ReplicaSet + "` and `" + RepoMongoDBSRVRecordName + "` *not* provided, then `" + - RepoNodesKey + "` blocks may mix dynamic and static nodes (see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")).\n" + - " - If `" + ReplicaSet + "` and `" + RepoMongoDBSRVRecordName + "` provided, then `" + - RepoNodesKey + "` blocks must be dynamic (see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")).\n", - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice(mongoServerTypes(), false), - }, - RepoMongoDBSRVRecordName: { - Description: "Name of a DNS SRV record which contains cluster topology details. " + - "If specified, then all `" + RepoNodesKey + "` blocks must be declared dynamic " + - "(see [`" + RepoNodeDynamicKey + "`](#" + RepoNodeDynamicKey + ")). " + - "Only supported for `" + RepoMongoDBServerTypeKey + "=\"" + Sharded + "\"` or `" + - RepoMongoDBServerTypeKey + "=\"" + ReplicaSet + "\".", - Type: schema.TypeString, - Optional: true, - }, - RepoMongoDBFlavorKey: { - Description: "The flavor of the MongoDB deployment. Allowed values: " + utils.SupportedValuesAsMarkdown(mongoFlavors()) + - "\n\n The following conditions apply:\n" + - " - The `" + MongoDBFlavorDocumentDB + "` flavor cannot be combined with the MongoDB Server type `" + Sharded + "`.\n", - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice(mongoFlavors(), false), - }, - }, - }, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - } -} diff --git a/cyral/internal/repository/resource_cyral_repository_test.go b/cyral/internal/repository/resource_test.go similarity index 96% rename from cyral/internal/repository/resource_cyral_repository_test.go rename to cyral/internal/repository/resource_test.go index 1d91d02f..db71fb0a 100644 --- a/cyral/internal/repository/resource_cyral_repository_test.go +++ b/cyral/internal/repository/resource_test.go @@ -15,8 +15,8 @@ var ( initialRepoConfig = repository.RepoInfo{ Name: utils.AccTestName(utils.RepositoryResourceName, "repo"), Type: repository.MongoDB, - Labels: []string{"rds", "us-east-2"}, - RepoNodes: []*repository.RepoNode{ + Labels: repository.Labels{"rds", "us-east-2"}, + RepoNodes: repository.RepoNodes{ { Host: "mongo.local", Port: 3333, @@ -30,8 +30,8 @@ var ( updatedRepoConfig = repository.RepoInfo{ Name: utils.AccTestName(utils.RepositoryResourceName, "repo-updated"), Type: repository.MongoDB, - Labels: []string{"rds", "us-east-1"}, - RepoNodes: []*repository.RepoNode{ + Labels: repository.Labels{"rds", "us-east-1"}, + RepoNodes: repository.RepoNodes{ { Host: "mongo.local", Port: 3334, @@ -48,7 +48,7 @@ var ( ConnParams: &repository.ConnParams{ ConnDraining: &repository.ConnDraining{}, }, - RepoNodes: []*repository.RepoNode{ + RepoNodes: repository.RepoNodes{ { Host: "mongo-cluster.local", Port: 27017, @@ -68,7 +68,7 @@ var ( WaitTime: 20, }, }, - RepoNodes: []*repository.RepoNode{ + RepoNodes: repository.RepoNodes{ { Host: "mongo-cluster.local", Port: 27017, @@ -88,7 +88,7 @@ var ( WaitTime: 20, }, }, - RepoNodes: []*repository.RepoNode{ + RepoNodes: repository.RepoNodes{ { Name: "node1", Host: "mongo-cluster.local.node1", @@ -122,7 +122,7 @@ var ( allRepoNodesAreDynamic = repository.RepoInfo{ Name: utils.AccTestName(utils.RepositoryResourceName, "repo-all-repo-nodes-are-dynamic"), Type: "mongodb", - RepoNodes: []*repository.RepoNode{ + RepoNodes: repository.RepoNodes{ { Dynamic: true, }, diff --git a/cyral/internal/repository/schema_loader.go b/cyral/internal/repository/schema_loader.go new file mode 100644 index 00000000..afee33f9 --- /dev/null +++ b/cyral/internal/repository/schema_loader.go @@ -0,0 +1,31 @@ +package repository + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/core" +) + +type packageSchema struct { +} + +func (p *packageSchema) Name() string { + return "repository" +} + +func (p *packageSchema) Schemas() []*core.SchemaDescriptor { + return []*core.SchemaDescriptor{ + { + Name: dataSourceName, + Type: core.DataSourceSchemaType, + Schema: dataSourceSchema, + }, + { + Name: resourceName, + Type: core.ResourceSchemaType, + Schema: resourceSchema, + }, + } +} + +func PackageSchema() core.PackageSchema { + return &packageSchema{} +} diff --git a/cyral/internal/repository/useraccount/resource.go b/cyral/internal/repository/useraccount/resource.go index a0c02135..bc684edd 100644 --- a/cyral/internal/repository/useraccount/resource.go +++ b/cyral/internal/repository/useraccount/resource.go @@ -46,7 +46,7 @@ var readRepositoryUserAccountConfig = core.ResourceOperationConfig{ SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &UserAccountResource{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "User account"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: "User account"}, } func resourceSchema() *schema.Resource { diff --git a/cyral/internal/repository/useraccount/schema_loader.go b/cyral/internal/repository/useraccount/schema_loader.go index a57e0e8d..86b59280 100644 --- a/cyral/internal/repository/useraccount/schema_loader.go +++ b/cyral/internal/repository/useraccount/schema_loader.go @@ -6,7 +6,7 @@ type packageSchema struct { } func (p *packageSchema) Name() string { - return "User Account" + return "useraccount" } func (p *packageSchema) Schemas() []*core.SchemaDescriptor { diff --git a/cyral/internal/role/resource_cyral_role_sso_groups.go b/cyral/internal/role/resource_cyral_role_sso_groups.go index 7ffac954..84a888f1 100644 --- a/cyral/internal/role/resource_cyral_role_sso_groups.go +++ b/cyral/internal/role/resource_cyral_role_sso_groups.go @@ -143,7 +143,7 @@ var readRoleSSOGroupsConfig = core.ResourceOperationConfig{ d.Get("role_id").(string)) }, SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &RoleSSOGroupsReadResponse{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Role SSO groups"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: "Role SSO groups"}, } var deleteRoleSSOGroupsConfig = core.ResourceOperationConfig{ diff --git a/cyral/internal/samlcertificate/schema_loader.go b/cyral/internal/samlcertificate/schema_loader.go index 10234985..5535706d 100644 --- a/cyral/internal/samlcertificate/schema_loader.go +++ b/cyral/internal/samlcertificate/schema_loader.go @@ -6,7 +6,7 @@ type packageSchema struct { } func (p *packageSchema) Name() string { - return "SAML Certificate" + return "samlcertificate" } func (p *packageSchema) Schemas() []*core.SchemaDescriptor { diff --git a/cyral/internal/serviceaccount/resource_cyral_service_account.go b/cyral/internal/serviceaccount/resource_cyral_service_account.go index 86be3fe2..8d0c7da4 100644 --- a/cyral/internal/serviceaccount/resource_cyral_service_account.go +++ b/cyral/internal/serviceaccount/resource_cyral_service_account.go @@ -33,7 +33,7 @@ var ( SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &ServiceAccount{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Service account"}, + RequestErrorHandler: &core.IgnoreHttpNotFound{ResName: "Service account"}, } ) diff --git a/cyral/internal/sidecar/constants.go b/cyral/internal/sidecar/constants.go new file mode 100644 index 00000000..5443c83f --- /dev/null +++ b/cyral/internal/sidecar/constants.go @@ -0,0 +1,7 @@ +package sidecar + +const ( + resourceName = "cyral_sidecar" + dataSourceIdName = "cyral_sidecar_id" + dataSourceBoundPortsName = "cyral_sidecar_bound_ports" +) diff --git a/cyral/internal/sidecar/credentials/constants.go b/cyral/internal/sidecar/credentials/constants.go new file mode 100644 index 00000000..c30abaaf --- /dev/null +++ b/cyral/internal/sidecar/credentials/constants.go @@ -0,0 +1,5 @@ +package credentials + +const ( + resourceName = "cyral_sidecar_credentials" +) diff --git a/cyral/internal/sidecar/credentials/model.go b/cyral/internal/sidecar/credentials/model.go new file mode 100644 index 00000000..ac589424 --- /dev/null +++ b/cyral/internal/sidecar/credentials/model.go @@ -0,0 +1,38 @@ +package credentials + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type CreateSidecarCredentialsRequest struct { + SidecarID string `json:"sidecarId"` +} + +func (r *CreateSidecarCredentialsRequest) ReadFromSchema(d *schema.ResourceData) error { + r.SidecarID = d.Get("sidecar_id").(string) + return nil +} + +type SidecarCredentialsData struct { + SidecarID string `json:"sidecarId"` + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` +} + +func (r *SidecarCredentialsData) WriteToSchema(d *schema.ResourceData) error { + if err := d.Set("client_id", r.ClientID); err != nil { + return fmt.Errorf("error setting 'client_id' field: %w", err) + } + if r.ClientSecret != "" { + if err := d.Set("client_secret", r.ClientSecret); err != nil { + return fmt.Errorf("error setting 'client_secret' field: %w", err) + } + } + if err := d.Set("sidecar_id", r.SidecarID); err != nil { + return fmt.Errorf("error setting 'sidecar_id' field: %w", err) + } + d.SetId(r.ClientID) + return nil +} diff --git a/cyral/internal/sidecar/credentials/resource.go b/cyral/internal/sidecar/credentials/resource.go new file mode 100644 index 00000000..615b2f2f --- /dev/null +++ b/cyral/internal/sidecar/credentials/resource.go @@ -0,0 +1,58 @@ +package credentials + +import ( + "fmt" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var resourceContextHandler = core.DefaultContextHandler{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &CreateSidecarCredentialsRequest{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &SidecarCredentialsData{} }, + SchemaWriterFactoryPostMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &SidecarCredentialsData{} }, + BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/users/sidecarAccounts", c.ControlPlane) + }, +} + +func resourceSchema() *schema.Resource { + return &schema.Resource{ + Description: "Create new [credentials for Cyral sidecar](https://cyral.com/docs/sidecars/sidecar-manage/#rotate-the-client-secret-for-a-sidecar).", + CreateContext: resourceContextHandler.CreateContext(), + ReadContext: resourceContextHandler.ReadContext(), + DeleteContext: resourceContextHandler.DeleteContext(), + + Schema: map[string]*schema.Schema{ + "id": { + Description: "Same as `client_id`.", + Type: schema.TypeString, + Computed: true, + }, + "sidecar_id": { + Description: "ID of the sidecar to create new credentials.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "client_id": { + Description: "Sidecar client ID.", + Type: schema.TypeString, + Computed: true, + }, + "client_secret": { + Description: "Sidecar client secret.", + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} diff --git a/cyral/internal/sidecar/credentials/resource_cyral_sidecar_credentials.go b/cyral/internal/sidecar/credentials/resource_cyral_sidecar_credentials.go deleted file mode 100644 index 2fe820cd..00000000 --- a/cyral/internal/sidecar/credentials/resource_cyral_sidecar_credentials.go +++ /dev/null @@ -1,128 +0,0 @@ -package credentials - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/cyralinc/terraform-provider-cyral/cyral/client" - "github.com/cyralinc/terraform-provider-cyral/cyral/utils" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -type CreateSidecarCredentialsRequest struct { - SidecarID string `json:"sidecarId"` -} - -type SidecarCredentialsData struct { - SidecarID string `json:"sidecarId"` - ClientID string `json:"clientId"` - ClientSecret string `json:"clientSecret"` -} - -func ResourceSidecarCredentials() *schema.Resource { - return &schema.Resource{ - Description: "Create new [credentials for Cyral sidecar](https://cyral.com/docs/sidecars/sidecar-manage/#rotate-the-client-secret-for-a-sidecar).", - CreateContext: resourceSidecarCredentialsCreate, - ReadContext: resourceSidecarCredentialsRead, - DeleteContext: resourceSidecarCredentialsDelete, - - Schema: map[string]*schema.Schema{ - "id": { - Description: "Same as `client_id`.", - Type: schema.TypeString, - Computed: true, - }, - "sidecar_id": { - Description: "ID of the sidecar to create new credentials.", - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "client_id": { - Description: "Sidecar client ID.", - Type: schema.TypeString, - Computed: true, - }, - "client_secret": { - Description: "Sidecar client secret.", - Type: schema.TypeString, - Computed: true, - Sensitive: true, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - } -} - -func resourceSidecarCredentialsCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceSidecarCredentialsCreate") - c := m.(*client.Client) - - payload := CreateSidecarCredentialsRequest{d.Get("sidecar_id").(string)} - - url := fmt.Sprintf("https://%s/v1/users/sidecarAccounts", c.ControlPlane) - - body, err := c.DoRequest(ctx, url, http.MethodPost, payload) - if err != nil { - return utils.CreateError("Unable to create sidecar credentials", fmt.Sprintf("%v", err)) - } - - response := SidecarCredentialsData{} - if err := json.Unmarshal(body, &response); err != nil { - return utils.CreateError("Unable to unmarshall JSON", fmt.Sprintf("%v", err)) - } - tflog.Debug(ctx, fmt.Sprintf("Response body (unmarshalled): %#v", response)) - - d.SetId(response.ClientID) - d.Set("client_id", response.ClientID) - d.Set("client_secret", response.ClientSecret) - - return resourceSidecarCredentialsRead(ctx, d, m) -} - -func resourceSidecarCredentialsRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceSidecarCredentialsRead") - c := m.(*client.Client) - - url := fmt.Sprintf("https://%s/v1/users/sidecarAccounts/%s", c.ControlPlane, d.Id()) - - body, err := c.DoRequest(ctx, url, http.MethodGet, nil) - if err != nil { - return utils.CreateError(fmt.Sprintf("Unable to read sidecar credentials. ClientID: %s", - d.Id()), fmt.Sprintf("%v", err)) - } - - response := SidecarCredentialsData{} - if err := json.Unmarshal(body, &response); err != nil { - return utils.CreateError("Unable to unmarshall JSON", fmt.Sprintf("%v", err)) - } - tflog.Debug(ctx, fmt.Sprintf("Response body (unmarshalled): %#v", response)) - - d.Set("sidecar_id", response.SidecarID) - d.Set("client_id", response.ClientID) - - tflog.Debug(ctx, "End resourceSidecarCredentialsRead") - - return diag.Diagnostics{} -} - -func resourceSidecarCredentialsDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceSidecarCredentialsDelete") - c := m.(*client.Client) - - url := fmt.Sprintf("https://%s/v1/users/sidecarAccounts/%s", c.ControlPlane, d.Id()) - - if _, err := c.DoRequest(ctx, url, http.MethodDelete, nil); err != nil { - return utils.CreateError("Unable to delete sidecar credentials", fmt.Sprintf("%v", err)) - } - - tflog.Debug(ctx, "End resourceSidecarCredentialsDelete") - - return diag.Diagnostics{} -} diff --git a/cyral/internal/sidecar/credentials/resource_cyral_sidecar_credentials_test.go b/cyral/internal/sidecar/credentials/resource_test.go similarity index 100% rename from cyral/internal/sidecar/credentials/resource_cyral_sidecar_credentials_test.go rename to cyral/internal/sidecar/credentials/resource_test.go diff --git a/cyral/internal/sidecar/credentials/schema_loader.go b/cyral/internal/sidecar/credentials/schema_loader.go new file mode 100644 index 00000000..17a4fe9e --- /dev/null +++ b/cyral/internal/sidecar/credentials/schema_loader.go @@ -0,0 +1,26 @@ +package credentials + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/core" +) + +type packageSchema struct { +} + +func (p *packageSchema) Name() string { + return "credentials" +} + +func (p *packageSchema) Schemas() []*core.SchemaDescriptor { + return []*core.SchemaDescriptor{ + { + Name: resourceName, + Type: core.ResourceSchemaType, + Schema: resourceSchema, + }, + } +} + +func PackageSchema() core.PackageSchema { + return &packageSchema{} +} diff --git a/cyral/internal/sidecar/listener/constants.go b/cyral/internal/sidecar/listener/constants.go new file mode 100644 index 00000000..4376659d --- /dev/null +++ b/cyral/internal/sidecar/listener/constants.go @@ -0,0 +1,24 @@ +package listener + +// Resource +const ( + resourceName = "cyral_sidecar_listener" + dataSourceName = "cyral_sidecar_listener" + + RepoTypesKey = "repo_types" + NetworkAddressKey = "network_address" + MySQLSettingsKey = "mysql_settings" + DbVersionKey = "db_version" + CharacterSetKey = "character_set" + S3SettingsKey = "s3_settings" + ProxyModeKey = "proxy_mode" + DynamoDbSettingsKey = "dynamodb_settings" + SQLServerSettingsKey = "sqlserver_settings" + SQLServerVersionKey = "version" +) + +// Data source +const ( + SidecarListenerListKey = "listener_list" + DSRepoTypeKey = "repo_type" +) diff --git a/cyral/internal/sidecar/listener/data_source_cyral_sidecar_listener.go b/cyral/internal/sidecar/listener/data_source_cyral_sidecar_listener.go deleted file mode 100644 index 7d8d86dc..00000000 --- a/cyral/internal/sidecar/listener/data_source_cyral_sidecar_listener.go +++ /dev/null @@ -1,115 +0,0 @@ -package listener - -import ( - "context" - "fmt" - "net/http" - - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "golang.org/x/exp/slices" - - "github.com/cyralinc/terraform-provider-cyral/cyral/client" - "github.com/cyralinc/terraform-provider-cyral/cyral/core" - "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository" - "github.com/cyralinc/terraform-provider-cyral/cyral/utils" -) - -const ( - SidecarListenerListKey = "listener_list" - DSRepoTypeKey = "repo_type" -) - -type ReadDataSourceSidecarListenerAPIResponse struct { - ListenerConfigs []SidecarListener `json:"listenerConfigs"` -} - -func (data ReadDataSourceSidecarListenerAPIResponse) WriteToSchema(d *schema.ResourceData) error { - ctx := context.Background() - tflog.Debug(ctx, "Init ReadDataSourceSidecarListenerAPIResponse.WriteToSchema") - var listenersList []any - tflog.Debug(ctx, fmt.Sprintf("data.ListenerConfig: %+v", data.ListenerConfigs)) - tflog.Debug(ctx, "Init for _, l := range data.ListenerConfig") - repoTypeFilter := d.Get(DSRepoTypeKey).(string) - portFilter := d.Get(utils.PortKey).(int) - for _, listenerConfig := range data.ListenerConfigs { - // Check if either the repo filter or the port filter is provided and matches the listener - if (repoTypeFilter == "" || slices.Contains(listenerConfig.RepoTypes, repoTypeFilter)) && - (portFilter == 0 || listenerConfig.NetworkAddress.Port == portFilter) { - listener := map[string]any{ - utils.ListenerIDKey: listenerConfig.ListenerId, - utils.SidecarIDKey: d.Get(utils.SidecarIDKey).(string), - RepoTypesKey: listenerConfig.RepoTypes, - NetworkAddressKey: listenerConfig.NetworkAddressAsInterface(), - MySQLSettingsKey: listenerConfig.MySQLSettingsAsInterface(), - S3SettingsKey: listenerConfig.S3SettingsAsInterface(), - DynamoDbSettingsKey: listenerConfig.DynamoDbSettingsAsInterface(), - SQLServerSettingsKey: listenerConfig.SQLServerSettingsAsInterface(), - } - tflog.Debug(ctx, fmt.Sprintf("listener: %q", listener)) - listenersList = append(listenersList, listener) - } - } - - tflog.Debug(ctx, fmt.Sprintf("listenersList: %q", listenersList)) - tflog.Debug(ctx, "End for _, l := range data.ListenerConfig") - - if err := d.Set(SidecarListenerListKey, listenersList); err != nil { - return err - } - - d.SetId(uuid.New().String()) - - tflog.Debug(ctx, "End ReadDataSourceSidecarListenerAPIResponse.WriteToSchema") - - return nil -} - -func dataSourceSidecarListenerReadConfig() core.ResourceOperationConfig { - return core.ResourceOperationConfig{ - ResourceName: "SidecarListenerDataSourceRead", - Type: operationtype.Read, - HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - sidecarID := d.Get(utils.SidecarIDKey).(string) - - return fmt.Sprintf("https://%s/v1/sidecars/%s/listeners", c.ControlPlane, sidecarID) - }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &ReadDataSourceSidecarListenerAPIResponse{} }, - } -} - -func DataSourceSidecarListener() *schema.Resource { - listenerSchema := utils.ConvertSchemaFieldsToComputed(getSidecarListenerSchema()) - return &schema.Resource{ - Description: "Retrieve and filter sidecar listeners.", - ReadContext: core.ReadResource(dataSourceSidecarListenerReadConfig()), - Schema: map[string]*schema.Schema{ - utils.SidecarIDKey: { - Description: "Filter the results by sidecar ID.", - Type: schema.TypeString, - Required: true, - }, - DSRepoTypeKey: { - Description: "Filter the results per repository type. Supported repo types:" + utils.SupportedValuesAsMarkdown(repository.RepositoryTypes()), - Type: schema.TypeString, - Optional: true, - }, - utils.PortKey: { - Description: "Filter the results per port.", - Type: schema.TypeInt, - Optional: true, - }, - SidecarListenerListKey: { - Description: "List of existing listeners satisfying the filter criteria.", - Computed: true, - Type: schema.TypeList, - Elem: &schema.Resource{ - Schema: listenerSchema, - }, - }, - }, - } -} diff --git a/cyral/internal/sidecar/listener/datasource.go b/cyral/internal/sidecar/listener/datasource.go new file mode 100644 index 00000000..dfdd4128 --- /dev/null +++ b/cyral/internal/sidecar/listener/datasource.go @@ -0,0 +1,55 @@ +package listener + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository" + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" +) + +var dsContextHandler = core.DefaultContextHandler{ + ResourceName: dataSourceName, + ResourceType: resourcetype.DataSource, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &ReadDataSourceSidecarListenerAPIResponse{} }, + GetPutDeleteURLFactory: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/sidecars/%s/listeners", c.ControlPlane, d.Get(utils.SidecarIDKey).(string)) + }, +} + +func dataSourceSchema() *schema.Resource { + listenerSchema := utils.ConvertSchemaFieldsToComputed(getSidecarListenerSchema()) + return &schema.Resource{ + Description: "Retrieve and filter sidecar listeners.", + ReadContext: dsContextHandler.ReadContext(), + Schema: map[string]*schema.Schema{ + utils.SidecarIDKey: { + Description: "Filter the results by sidecar ID.", + Type: schema.TypeString, + Required: true, + }, + DSRepoTypeKey: { + Description: "Filter the results per repository type. Supported repo types:" + utils.SupportedValuesAsMarkdown(repository.RepositoryTypes()), + Type: schema.TypeString, + Optional: true, + }, + utils.PortKey: { + Description: "Filter the results per port.", + Type: schema.TypeInt, + Optional: true, + }, + SidecarListenerListKey: { + Description: "List of existing listeners satisfying the filter criteria.", + Computed: true, + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: listenerSchema, + }, + }, + }, + } +} diff --git a/cyral/internal/sidecar/listener/data_source_cyral_sidecar_listener_test.go b/cyral/internal/sidecar/listener/datasource_test.go similarity index 100% rename from cyral/internal/sidecar/listener/data_source_cyral_sidecar_listener_test.go rename to cyral/internal/sidecar/listener/datasource_test.go diff --git a/cyral/internal/sidecar/listener/model.go b/cyral/internal/sidecar/listener/model.go new file mode 100644 index 00000000..e9e3b444 --- /dev/null +++ b/cyral/internal/sidecar/listener/model.go @@ -0,0 +1,245 @@ +package listener + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "golang.org/x/exp/slices" + + "github.com/cyralinc/terraform-provider-cyral/cyral/utils" +) + +// SidecarListener struct for sidecar listener. +type SidecarListener struct { + SidecarId string `json:"-"` + ListenerId string `json:"id"` + RepoTypes []string `json:"repoTypes"` + NetworkAddress *NetworkAddress `json:"address,omitempty"` + MySQLSettings *MySQLSettings `json:"mysqlSettings,omitempty"` + S3Settings *S3Settings `json:"s3Settings,omitempty"` + DynamoDbSettings *DynamoDbSettings `json:"dynamoDbSettings,omitempty"` + SQLServerSettings *SQLServerSettings `json:"sqlServerSettings,omitempty"` +} + +type NetworkAddress struct { + Host string `json:"host,omitempty"` + Port int `json:"port"` +} + +type MySQLSettings struct { + DbVersion string `json:"dbVersion,omitempty"` + CharacterSet string `json:"characterSet,omitempty"` +} + +type S3Settings struct { + ProxyMode bool `json:"proxyMode,omitempty"` +} + +type DynamoDbSettings struct { + ProxyMode bool `json:"proxyMode,omitempty"` +} + +type SQLServerSettings struct { + Version string `json:"version,omitempty"` +} + +type ReadSidecarListenerAPIResponse struct { + ListenerConfig *SidecarListener `json:"listenerConfig"` +} +type CreateListenerAPIResponse struct { + ListenerId string `json:"listenerId"` +} + +func (c CreateListenerAPIResponse) WriteToSchema(d *schema.ResourceData) error { + d.SetId(utils.MarshalComposedID([]string{d.Get(utils.SidecarIDKey).(string), c.ListenerId}, "/")) + return d.Set(utils.ListenerIDKey, c.ListenerId) +} + +func (data ReadSidecarListenerAPIResponse) WriteToSchema(d *schema.ResourceData) error { + ctx := context.Background() + tflog.Debug(ctx, "Init ReadSidecarListenerAPIResponse.WriteToSchema") + if data.ListenerConfig != nil { + _ = d.Set(utils.ListenerIDKey, data.ListenerConfig.ListenerId) + _ = d.Set(RepoTypesKey, data.ListenerConfig.RepoTypesAsInterface()) + _ = d.Set(NetworkAddressKey, data.ListenerConfig.NetworkAddressAsInterface()) + _ = d.Set(S3SettingsKey, data.ListenerConfig.S3SettingsAsInterface()) + _ = d.Set(MySQLSettingsKey, data.ListenerConfig.MySQLSettingsAsInterface()) + _ = d.Set(DynamoDbSettingsKey, data.ListenerConfig.DynamoDbSettingsAsInterface()) + _ = d.Set(SQLServerSettingsKey, data.ListenerConfig.SQLServerSettingsAsInterface()) + } + tflog.Debug(ctx, "End ReadSidecarListenerAPIResponse.WriteToSchema") + return nil +} + +func (l *SidecarListener) RepoTypesAsInterface() []interface{} { + if l.RepoTypes == nil { + return nil + } + result := make([]interface{}, len(l.RepoTypes)) + for i, v := range l.RepoTypes { + result[i] = v + } + return result +} +func (l *SidecarListener) RepoTypesFromInterface(anInterface []interface{}) { + repoTypes := make([]string, len(anInterface)) + for i, v := range anInterface { + repoTypes[i] = v.(string) + } + l.RepoTypes = repoTypes +} +func (l *SidecarListener) NetworkAddressAsInterface() []interface{} { + if l.NetworkAddress == nil { + return nil + } + return []interface{}{ + map[string]interface{}{ + utils.HostKey: l.NetworkAddress.Host, + utils.PortKey: l.NetworkAddress.Port, + }, + } +} +func (l *SidecarListener) NetworkAddressFromInterface(anInterface []interface{}) { + if len(anInterface) == 0 { + return + } + l.NetworkAddress = &NetworkAddress{ + Host: anInterface[0].(map[string]interface{})[utils.HostKey].(string), + Port: anInterface[0].(map[string]interface{})[utils.PortKey].(int), + } +} +func (l *SidecarListener) MySQLSettingsAsInterface() []interface{} { + if l.MySQLSettings == nil { + return nil + } + return []interface{}{map[string]interface{}{ + DbVersionKey: l.MySQLSettings.DbVersion, + CharacterSetKey: l.MySQLSettings.CharacterSet, + }} +} +func (l *SidecarListener) MySQLSettingsFromInterface(anInterface []interface{}) { + if len(anInterface) == 0 { + return + } + l.MySQLSettings = &MySQLSettings{ + DbVersion: anInterface[0].(map[string]interface{})[DbVersionKey].(string), + CharacterSet: anInterface[0].(map[string]interface{})[CharacterSetKey].(string), + } +} +func (l *SidecarListener) S3SettingsAsInterface() []interface{} { + if l.S3Settings == nil { + return nil + } + return []interface{}{map[string]interface{}{ + ProxyModeKey: l.S3Settings.ProxyMode, + }} +} +func (l *SidecarListener) S3SettingsFromInterface(anInterface []interface{}) { + if len(anInterface) == 0 { + return + } + l.S3Settings = &S3Settings{ + ProxyMode: anInterface[0].(map[string]interface{})[ProxyModeKey].(bool), + } +} +func (l *SidecarListener) DynamoDbSettingsAsInterface() []interface{} { + if l.DynamoDbSettings == nil { + return nil + } + return []interface{}{map[string]interface{}{ + ProxyModeKey: l.DynamoDbSettings.ProxyMode, + }} +} +func (l *SidecarListener) DynamoDbSettingsFromInterface(anInterface []interface{}) { + if len(anInterface) == 0 { + return + } + l.DynamoDbSettings = &DynamoDbSettings{ + ProxyMode: anInterface[0].(map[string]interface{})[ProxyModeKey].(bool), + } +} +func (l *SidecarListener) SQLServerSettingsAsInterface() []interface{} { + if l.SQLServerSettings == nil { + return nil + } + return []interface{}{map[string]interface{}{ + SQLServerVersionKey: l.SQLServerSettings.Version, + }} +} +func (l *SidecarListener) SQLServerSettingsFromInterface(anInterface []interface{}) { + if len(anInterface) == 0 { + return + } + l.SQLServerSettings = &SQLServerSettings{ + Version: anInterface[0].(map[string]interface{})[SQLServerVersionKey].(string), + } +} + +// SidecarListenerResource represents the payload of a create or update a listener request +type SidecarListenerResource struct { + ListenerConfig SidecarListener `json:"listenerConfig"` +} + +// ReadFromSchema populates the SidecarListenerResource from the schema +func (s *SidecarListenerResource) ReadFromSchema(d *schema.ResourceData) error { + s.ListenerConfig = SidecarListener{ + SidecarId: d.Get(utils.SidecarIDKey).(string), + ListenerId: d.Get(utils.ListenerIDKey).(string), + } + s.ListenerConfig.RepoTypesFromInterface(d.Get(RepoTypesKey).([]interface{})) + s.ListenerConfig.NetworkAddressFromInterface(d.Get(NetworkAddressKey).(*schema.Set).List()) + s.ListenerConfig.MySQLSettingsFromInterface(d.Get(MySQLSettingsKey).(*schema.Set).List()) + s.ListenerConfig.S3SettingsFromInterface(d.Get(S3SettingsKey).(*schema.Set).List()) + s.ListenerConfig.DynamoDbSettingsFromInterface(d.Get(DynamoDbSettingsKey).(*schema.Set).List()) + s.ListenerConfig.SQLServerSettingsFromInterface(d.Get(SQLServerSettingsKey).(*schema.Set).List()) + + return nil +} + +type ReadDataSourceSidecarListenerAPIResponse struct { + ListenerConfigs []SidecarListener `json:"listenerConfigs"` +} + +func (data ReadDataSourceSidecarListenerAPIResponse) WriteToSchema(d *schema.ResourceData) error { + ctx := context.Background() + tflog.Debug(ctx, "Init ReadDataSourceSidecarListenerAPIResponse.WriteToSchema") + var listenersList []any + tflog.Debug(ctx, fmt.Sprintf("data.ListenerConfig: %+v", data.ListenerConfigs)) + tflog.Debug(ctx, "Init for _, l := range data.ListenerConfig") + repoTypeFilter := d.Get(DSRepoTypeKey).(string) + portFilter := d.Get(utils.PortKey).(int) + for _, listenerConfig := range data.ListenerConfigs { + // Check if either the repo filter or the port filter is provided and matches the listener + if (repoTypeFilter == "" || slices.Contains(listenerConfig.RepoTypes, repoTypeFilter)) && + (portFilter == 0 || listenerConfig.NetworkAddress.Port == portFilter) { + listener := map[string]any{ + utils.ListenerIDKey: listenerConfig.ListenerId, + utils.SidecarIDKey: d.Get(utils.SidecarIDKey).(string), + RepoTypesKey: listenerConfig.RepoTypes, + NetworkAddressKey: listenerConfig.NetworkAddressAsInterface(), + MySQLSettingsKey: listenerConfig.MySQLSettingsAsInterface(), + S3SettingsKey: listenerConfig.S3SettingsAsInterface(), + DynamoDbSettingsKey: listenerConfig.DynamoDbSettingsAsInterface(), + SQLServerSettingsKey: listenerConfig.SQLServerSettingsAsInterface(), + } + tflog.Debug(ctx, fmt.Sprintf("listener: %q", listener)) + listenersList = append(listenersList, listener) + } + } + + tflog.Debug(ctx, fmt.Sprintf("listenersList: %q", listenersList)) + tflog.Debug(ctx, "End for _, l := range data.ListenerConfig") + + if err := d.Set(SidecarListenerListKey, listenersList); err != nil { + return err + } + + d.SetId(uuid.New().String()) + + tflog.Debug(ctx, "End ReadDataSourceSidecarListenerAPIResponse.WriteToSchema") + + return nil +} diff --git a/cyral/internal/sidecar/listener/resource_cyral_sidecar_listener.go b/cyral/internal/sidecar/listener/resource.go similarity index 51% rename from cyral/internal/sidecar/listener/resource_cyral_sidecar_listener.go rename to cyral/internal/sidecar/listener/resource.go index c8cd0abc..cf101d82 100644 --- a/cyral/internal/sidecar/listener/resource_cyral_sidecar_listener.go +++ b/cyral/internal/sidecar/listener/resource.go @@ -3,228 +3,34 @@ package listener import ( "context" "fmt" - "net/http" - "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/cyralinc/terraform-provider-cyral/cyral/client" "github.com/cyralinc/terraform-provider-cyral/cyral/core" - "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository" "github.com/cyralinc/terraform-provider-cyral/cyral/utils" ) -// create a constant block for schema keys - -const ( - RepoTypesKey = "repo_types" - NetworkAddressKey = "network_address" - MySQLSettingsKey = "mysql_settings" - DbVersionKey = "db_version" - CharacterSetKey = "character_set" - S3SettingsKey = "s3_settings" - ProxyModeKey = "proxy_mode" - DynamoDbSettingsKey = "dynamodb_settings" - SQLServerSettingsKey = "sqlserver_settings" - SQLServerVersionKey = "version" -) - -// SidecarListener struct for sidecar listener. -type SidecarListener struct { - SidecarId string `json:"-"` - ListenerId string `json:"id"` - RepoTypes []string `json:"repoTypes"` - NetworkAddress *NetworkAddress `json:"address,omitempty"` - MySQLSettings *MySQLSettings `json:"mysqlSettings,omitempty"` - S3Settings *S3Settings `json:"s3Settings,omitempty"` - DynamoDbSettings *DynamoDbSettings `json:"dynamoDbSettings,omitempty"` - SQLServerSettings *SQLServerSettings `json:"sqlServerSettings,omitempty"` -} -type NetworkAddress struct { - Host string `json:"host,omitempty"` - Port int `json:"port"` -} -type MySQLSettings struct { - DbVersion string `json:"dbVersion,omitempty"` - CharacterSet string `json:"characterSet,omitempty"` -} -type S3Settings struct { - ProxyMode bool `json:"proxyMode,omitempty"` -} -type DynamoDbSettings struct { - ProxyMode bool `json:"proxyMode,omitempty"` -} - -type SQLServerSettings struct { - Version string `json:"version,omitempty"` -} - -var ReadSidecarListenersConfig = core.ResourceOperationConfig{ - ResourceName: "SidecarListenersResourceRead", - Type: operationtype.Read, - HttpMethod: http.MethodGet, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { +var resourceContextHandler = core.DefaultContextHandler{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + SchemaReaderFactory: func() core.SchemaReader { return &SidecarListenerResource{} }, + SchemaWriterFactoryGetMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &ReadSidecarListenerAPIResponse{} }, + SchemaWriterFactoryPostMethod: func(_ *schema.ResourceData) core.SchemaWriter { return &CreateListenerAPIResponse{} }, + BaseURLFactory: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/sidecars/%s/listeners", + c.ControlPlane, + d.Get(utils.SidecarIDKey).(string)) + }, + GetPutDeleteURLFactory: func(d *schema.ResourceData, c *client.Client) string { return fmt.Sprintf("https://%s/v1/sidecars/%s/listeners/%s", c.ControlPlane, d.Get(utils.SidecarIDKey).(string), - d.Get(utils.ListenerIDKey).(string)) + d.Get(utils.ListenerIDKey).(string), + ) }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &ReadSidecarListenerAPIResponse{} }, - RequestErrorHandler: &core.ReadIgnoreHttpNotFound{ResName: "Sidecar listener"}, -} - -type ReadSidecarListenerAPIResponse struct { - ListenerConfig *SidecarListener `json:"listenerConfig"` -} -type CreateListenerAPIResponse struct { - ListenerId string `json:"listenerId"` -} - -func (c CreateListenerAPIResponse) WriteToSchema(d *schema.ResourceData) error { - d.SetId(utils.MarshalComposedID([]string{d.Get(utils.SidecarIDKey).(string), c.ListenerId}, "/")) - return d.Set(utils.ListenerIDKey, c.ListenerId) -} - -func (data ReadSidecarListenerAPIResponse) WriteToSchema(d *schema.ResourceData) error { - ctx := context.Background() - tflog.Debug(ctx, "Init ReadSidecarListenerAPIResponse.WriteToSchema") - if data.ListenerConfig != nil { - _ = d.Set(utils.ListenerIDKey, data.ListenerConfig.ListenerId) - _ = d.Set(RepoTypesKey, data.ListenerConfig.RepoTypesAsInterface()) - _ = d.Set(NetworkAddressKey, data.ListenerConfig.NetworkAddressAsInterface()) - _ = d.Set(S3SettingsKey, data.ListenerConfig.S3SettingsAsInterface()) - _ = d.Set(MySQLSettingsKey, data.ListenerConfig.MySQLSettingsAsInterface()) - _ = d.Set(DynamoDbSettingsKey, data.ListenerConfig.DynamoDbSettingsAsInterface()) - _ = d.Set(SQLServerSettingsKey, data.ListenerConfig.SQLServerSettingsAsInterface()) - } - tflog.Debug(ctx, "End ReadSidecarListenerAPIResponse.WriteToSchema") - return nil -} - -func (l *SidecarListener) RepoTypesAsInterface() []interface{} { - if l.RepoTypes == nil { - return nil - } - result := make([]interface{}, len(l.RepoTypes)) - for i, v := range l.RepoTypes { - result[i] = v - } - return result -} -func (l *SidecarListener) RepoTypesFromInterface(anInterface []interface{}) { - repoTypes := make([]string, len(anInterface)) - for i, v := range anInterface { - repoTypes[i] = v.(string) - } - l.RepoTypes = repoTypes -} -func (l *SidecarListener) NetworkAddressAsInterface() []interface{} { - if l.NetworkAddress == nil { - return nil - } - return []interface{}{ - map[string]interface{}{ - utils.HostKey: l.NetworkAddress.Host, - utils.PortKey: l.NetworkAddress.Port, - }, - } -} -func (l *SidecarListener) NetworkAddressFromInterface(anInterface []interface{}) { - if len(anInterface) == 0 { - return - } - l.NetworkAddress = &NetworkAddress{ - Host: anInterface[0].(map[string]interface{})[utils.HostKey].(string), - Port: anInterface[0].(map[string]interface{})[utils.PortKey].(int), - } -} -func (l *SidecarListener) MySQLSettingsAsInterface() []interface{} { - if l.MySQLSettings == nil { - return nil - } - return []interface{}{map[string]interface{}{ - DbVersionKey: l.MySQLSettings.DbVersion, - CharacterSetKey: l.MySQLSettings.CharacterSet, - }} -} -func (l *SidecarListener) MySQLSettingsFromInterface(anInterface []interface{}) { - if len(anInterface) == 0 { - return - } - l.MySQLSettings = &MySQLSettings{ - DbVersion: anInterface[0].(map[string]interface{})[DbVersionKey].(string), - CharacterSet: anInterface[0].(map[string]interface{})[CharacterSetKey].(string), - } -} -func (l *SidecarListener) S3SettingsAsInterface() []interface{} { - if l.S3Settings == nil { - return nil - } - return []interface{}{map[string]interface{}{ - ProxyModeKey: l.S3Settings.ProxyMode, - }} -} -func (l *SidecarListener) S3SettingsFromInterface(anInterface []interface{}) { - if len(anInterface) == 0 { - return - } - l.S3Settings = &S3Settings{ - ProxyMode: anInterface[0].(map[string]interface{})[ProxyModeKey].(bool), - } -} -func (l *SidecarListener) DynamoDbSettingsAsInterface() []interface{} { - if l.DynamoDbSettings == nil { - return nil - } - return []interface{}{map[string]interface{}{ - ProxyModeKey: l.DynamoDbSettings.ProxyMode, - }} -} -func (l *SidecarListener) DynamoDbSettingsFromInterface(anInterface []interface{}) { - if len(anInterface) == 0 { - return - } - l.DynamoDbSettings = &DynamoDbSettings{ - ProxyMode: anInterface[0].(map[string]interface{})[ProxyModeKey].(bool), - } -} -func (l *SidecarListener) SQLServerSettingsAsInterface() []interface{} { - if l.SQLServerSettings == nil { - return nil - } - return []interface{}{map[string]interface{}{ - SQLServerVersionKey: l.SQLServerSettings.Version, - }} -} -func (l *SidecarListener) SQLServerSettingsFromInterface(anInterface []interface{}) { - if len(anInterface) == 0 { - return - } - l.SQLServerSettings = &SQLServerSettings{ - Version: anInterface[0].(map[string]interface{})[SQLServerVersionKey].(string), - } -} - -// SidecarListenerResource represents the payload of a create or update a listener request -type SidecarListenerResource struct { - ListenerConfig SidecarListener `json:"listenerConfig"` -} - -// ReadFromSchema populates the SidecarListenerResource from the schema -func (s *SidecarListenerResource) ReadFromSchema(d *schema.ResourceData) error { - s.ListenerConfig = SidecarListener{ - SidecarId: d.Get(utils.SidecarIDKey).(string), - ListenerId: d.Get(utils.ListenerIDKey).(string), - } - s.ListenerConfig.RepoTypesFromInterface(d.Get(RepoTypesKey).([]interface{})) - s.ListenerConfig.NetworkAddressFromInterface(d.Get(NetworkAddressKey).(*schema.Set).List()) - s.ListenerConfig.MySQLSettingsFromInterface(d.Get(MySQLSettingsKey).(*schema.Set).List()) - s.ListenerConfig.S3SettingsFromInterface(d.Get(S3SettingsKey).(*schema.Set).List()) - s.ListenerConfig.DynamoDbSettingsFromInterface(d.Get(DynamoDbSettingsKey).(*schema.Set).List()) - s.ListenerConfig.SQLServerSettingsFromInterface(d.Get(SQLServerSettingsKey).(*schema.Set).List()) - - return nil } // resourceSidecarListener returns the schema and methods for provisioning a sidecar listener @@ -233,55 +39,16 @@ func (s *SidecarListenerResource) ReadFromSchema(d *schema.ResourceData) error { // POST {{baseURL}}/sidecars/:sidecarID/listeners/ (Create a listener) // PUT {{baseURL}}/sidecars/:sidecarID/listeners/:listenerID (Update a listener) // DELETE {{baseURL}}/sidecars/:sidecarID/listeners/:listenerID (Delete a listener) -func ResourceSidecarListener() *schema.Resource { +func resourceSchema() *schema.Resource { return &schema.Resource{ Description: "Manages [sidecar listeners](https://cyral.com/docs/sidecars/sidecar-listeners)." + "\n~> **Warning** Multiple listeners can be associated to a single sidecar as long as " + "`host` and `port` are unique. If `host` is omitted, then `port` must be unique.", - CreateContext: core.CreateResource( - core.ResourceOperationConfig{ - ResourceName: "SidecarListenersResourceCreate", - Type: operationtype.Create, - HttpMethod: http.MethodPost, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/sidecars/%s/listeners", - c.ControlPlane, - d.Get(utils.SidecarIDKey).(string)) - - }, - SchemaReaderFactory: func() core.SchemaReader { return &SidecarListenerResource{} }, - SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &CreateListenerAPIResponse{} }, - }, ReadSidecarListenersConfig, - ), - ReadContext: core.ReadResource(ReadSidecarListenersConfig), - UpdateContext: core.UpdateResource( - core.ResourceOperationConfig{ - ResourceName: "SidecarListenersResourceUpdate", - Type: operationtype.Update, - HttpMethod: http.MethodPut, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/sidecars/%s/listeners/%s", - c.ControlPlane, - d.Get(utils.SidecarIDKey).(string), - d.Get(utils.ListenerIDKey).(string)) + CreateContext: resourceContextHandler.CreateContext(), + ReadContext: resourceContextHandler.ReadContext(), + UpdateContext: resourceContextHandler.UpdateContext(), + DeleteContext: resourceContextHandler.DeleteContext(), - }, - SchemaReaderFactory: func() core.SchemaReader { return &SidecarListenerResource{} }, - }, ReadSidecarListenersConfig, - ), - DeleteContext: core.DeleteResource( - core.ResourceOperationConfig{ - ResourceName: "SidecarListenersResourceDelete", - Type: operationtype.Delete, - HttpMethod: http.MethodDelete, - URLFactory: func(d *schema.ResourceData, c *client.Client) string { - return fmt.Sprintf("https://%s/v1/sidecars/%s/listeners/%s", - c.ControlPlane, - d.Get(utils.SidecarIDKey).(string), - d.Get(utils.ListenerIDKey).(string)) - }, - }, - ), Schema: getSidecarListenerSchema(), Importer: &schema.ResourceImporter{ StateContext: func( diff --git a/cyral/internal/sidecar/listener/resource_cyral_sidecar_listener_test.go b/cyral/internal/sidecar/listener/resource_test.go similarity index 100% rename from cyral/internal/sidecar/listener/resource_cyral_sidecar_listener_test.go rename to cyral/internal/sidecar/listener/resource_test.go diff --git a/cyral/internal/sidecar/listener/schema_loader.go b/cyral/internal/sidecar/listener/schema_loader.go new file mode 100644 index 00000000..68594311 --- /dev/null +++ b/cyral/internal/sidecar/listener/schema_loader.go @@ -0,0 +1,31 @@ +package listener + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/core" +) + +type packageSchema struct { +} + +func (p *packageSchema) Name() string { + return "listener" +} + +func (p *packageSchema) Schemas() []*core.SchemaDescriptor { + return []*core.SchemaDescriptor{ + { + Name: dataSourceName, + Type: core.DataSourceSchemaType, + Schema: dataSourceSchema, + }, + { + Name: resourceName, + Type: core.ResourceSchemaType, + Schema: resourceSchema, + }, + } +} + +func PackageSchema() core.PackageSchema { + return &packageSchema{} +} diff --git a/cyral/internal/sidecar/model.go b/cyral/internal/sidecar/model.go new file mode 100644 index 00000000..d8d07e79 --- /dev/null +++ b/cyral/internal/sidecar/model.go @@ -0,0 +1,192 @@ +package sidecar + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type CreateSidecarResponse struct { + ID string `json:"ID"` +} + +type SidecarData struct { + ID string `json:"id"` + Name string `json:"name"` + Labels []string `json:"labels"` + SidecarProperties *SidecarProperties `json:"properties"` + ServicesConfig SidecarServicesConfig `json:"services"` + UserEndpoint string `json:"userEndpoint"` + CertificateBundleSecrets CertificateBundleSecrets `json:"certificateBundleSecrets,omitempty"` +} + +func (sd *SidecarData) BypassMode() string { + if sd.ServicesConfig != nil { + if dispConfig, ok := sd.ServicesConfig["dispatcher"]; ok { + if bypass_mode, ok := dispConfig["bypass"]; ok { + return bypass_mode + } + } + } + return "" +} + +func (r *SidecarData) WriteToSchema(d *schema.ResourceData) error { + if err := d.Set("name", r.Name); err != nil { + return fmt.Errorf("error setting 'name' field: %w", err) + } + if r.SidecarProperties != nil { + if err := d.Set("deployment_method", r.SidecarProperties.DeploymentMethod); err != nil { + return fmt.Errorf("error setting 'deployment_method' field: %w", err) + } + if err := d.Set("activity_log_integration_id", r.SidecarProperties.LogIntegrationID); err != nil { + return fmt.Errorf("error setting 'activity_log_integration_id' field: %w", err) + } + if err := d.Set("diagnostic_log_integration_id", r.SidecarProperties.DiagnosticLogIntegrationID); err != nil { + return fmt.Errorf("error setting 'diagnostic_log_integration_id' field: %w", err) + } + } + if err := d.Set("labels", r.Labels); err != nil { + return fmt.Errorf("error setting 'labels' field: %w", err) + } + if err := d.Set("user_endpoint", r.UserEndpoint); err != nil { + return fmt.Errorf("error setting 'user_endpoint' field: %w", err) + } + if bypassMode := r.BypassMode(); bypassMode != "" { + if err := d.Set("bypass_mode", bypassMode); err != nil { + return fmt.Errorf("error setting 'bypass_mode' field: %w", err) + } + } + + if err := d.Set("certificate_bundle_secrets", flattenCertificateBundleSecrets(r.CertificateBundleSecrets)); err != nil { + return fmt.Errorf("error setting 'certificate_bundle_secrets' field: %w", err) + } + return nil +} + +func (r *SidecarData) ReadFromSchema(d *schema.ResourceData) error { + activityLogIntegrationID := d.Get("activity_log_integration_id").(string) + if activityLogIntegrationID == "" { + activityLogIntegrationID = d.Get("log_integration_id").(string) + } + + labels := d.Get("labels").([]interface{}) + sidecarDataLabels := []string{} + for _, labelInterface := range labels { + if label, ok := labelInterface.(string); ok { + sidecarDataLabels = append(sidecarDataLabels, label) + } + } + + r.ID = d.Id() + r.Name = d.Get("name").(string) + r.Labels = sidecarDataLabels + r.SidecarProperties = &SidecarProperties{ + DeploymentMethod: d.Get("deployment_method").(string), + LogIntegrationID: activityLogIntegrationID, + DiagnosticLogIntegrationID: d.Get("diagnostic_log_integration_id").(string), + } + r.ServicesConfig = SidecarServicesConfig{ + "dispatcher": map[string]string{ + "bypass": d.Get("bypass_mode").(string), + }, + } + r.UserEndpoint = d.Get("user_endpoint").(string) + r.CertificateBundleSecrets = getCertificateBundleSecret(d) + + return nil +} + +type SidecarProperties struct { + DeploymentMethod string `json:"deploymentMethod"` + LogIntegrationID string `json:"logIntegrationID,omitempty"` + DiagnosticLogIntegrationID string `json:"diagnosticLogIntegrationID,omitempty"` +} + +type SidecarServicesConfig map[string]map[string]string + +type CertificateBundleSecrets map[string]*CertificateBundleSecret + +type CertificateBundleSecret struct { + Engine string `json:"engine,omitempty"` + SecretId string `json:"secretId,omitempty"` + Type string `json:"type,omitempty"` +} + +func flattenCertificateBundleSecrets(cbs CertificateBundleSecrets) []interface{} { + ctx := context.Background() + tflog.Debug(ctx, "Init flattenCertificateBundleSecrets") + var flatCBS []interface{} + if cbs != nil { + cb := make(map[string]interface{}) + + for key, val := range cbs { + // Ignore self-signed certificates + if key != "sidecar-generated-selfsigned" { + contentCB := make([]interface{}, 1) + + tflog.Debug(ctx, fmt.Sprintf("key: %v", key)) + tflog.Debug(ctx, fmt.Sprintf("val: %v", val)) + + contentCBMap := make(map[string]interface{}) + contentCBMap["secret_id"] = val.SecretId + contentCBMap["engine"] = val.Engine + contentCBMap["type"] = val.Type + + contentCB[0] = contentCBMap + cb[key] = contentCB + } + } + + if len(cb) > 0 { + flatCBS = make([]interface{}, 1) + flatCBS[0] = cb + } + } + + tflog.Debug(ctx, fmt.Sprintf("end flattenCertificateBundleSecrets %v", flatCBS)) + return flatCBS +} + +func getCertificateBundleSecret(d *schema.ResourceData) CertificateBundleSecrets { + ctx := context.Background() + tflog.Debug(ctx, "Init getCertificateBundleSecret") + rdCBS := d.Get("certificate_bundle_secrets").(*schema.Set).List() + ret := make(CertificateBundleSecrets) + + if len(rdCBS) > 0 { + cbsMap := rdCBS[0].(map[string]interface{}) + for k, v := range cbsMap { + vList := v.(*schema.Set).List() + // 1. k = "sidecar" or other direct internal elements of certificate_bundle_secrets + // 2. Also one element on this list due to MaxItems... + // 3. Ignore self signed certificates + if len(vList) > 0 && k != "sidecar-generated-selfsigned" { + vMap := vList[0].(map[string]interface{}) + engine := "" + if val, ok := vMap["engine"]; val != nil && ok { + engine = val.(string) + } + cbsType := vMap["type"].(string) + secretId := vMap["secret_id"].(string) + cbs := CertificateBundleSecret{ + SecretId: secretId, + Engine: engine, + Type: cbsType, + } + ret[k] = &cbs + } + } + } + + // If the occurrence of `sidecar` does not exist, set it to an empty certificate bundle + // so that the API can remove the `sidecar` key from the persisted certificate bundle map. + if _, ok := ret["sidecar"]; !ok { + ret["sidecar"] = &CertificateBundleSecret{} + } + + tflog.Debug(ctx, "end getCertificateBundleSecret") + return ret +} diff --git a/cyral/internal/sidecar/resource.go b/cyral/internal/sidecar/resource.go new file mode 100644 index 00000000..c90fd96c --- /dev/null +++ b/cyral/internal/sidecar/resource.go @@ -0,0 +1,196 @@ +package sidecar + +import ( + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/cyralinc/terraform-provider-cyral/cyral/client" + "github.com/cyralinc/terraform-provider-cyral/cyral/core" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/operationtype" + "github.com/cyralinc/terraform-provider-cyral/cyral/core/types/resourcetype" +) + +var urlFactory = func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/sidecars/%s", c.ControlPlane, d.Id()) +} + +var readConfig = core.ResourceOperationConfig{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + Type: operationtype.Read, + HttpMethod: http.MethodGet, + URLFactory: urlFactory, + SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &SidecarData{} }, + RequestErrorHandler: &core.IgnoreNotFoundByMessage{ + ResName: resourceName, + MessageMatches: "NotFound", + OperationType: operationtype.Read, + }, +} + +func resourceSchema() *schema.Resource { + return &schema.Resource{ + Description: "Manages [sidecars](https://cyral.com/docs/sidecars/manage).", + CreateContext: core.CreateResource( + core.ResourceOperationConfig{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + Type: operationtype.Create, + HttpMethod: http.MethodPost, + URLFactory: func(d *schema.ResourceData, c *client.Client) string { + return fmt.Sprintf("https://%s/v1/sidecars", c.ControlPlane) + }, + SchemaReaderFactory: func() core.SchemaReader { return &SidecarData{} }, + SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &core.IDBasedResponse{} }, + }, + readConfig, + ), + ReadContext: core.ReadResource(readConfig), + UpdateContext: core.UpdateResource( + core.ResourceOperationConfig{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + Type: operationtype.Update, + HttpMethod: http.MethodPut, + URLFactory: urlFactory, + SchemaReaderFactory: func() core.SchemaReader { return &SidecarData{} }, + SchemaWriterFactory: func(_ *schema.ResourceData) core.SchemaWriter { return &SidecarData{} }, + }, + readConfig, + ), + DeleteContext: core.DeleteResource( + core.ResourceOperationConfig{ + ResourceName: resourceName, + ResourceType: resourcetype.Resource, + Type: operationtype.Delete, + HttpMethod: http.MethodDelete, + URLFactory: urlFactory, + RequestErrorHandler: &core.IgnoreNotFoundByMessage{ + ResName: resourceName, + MessageMatches: "NotFound", + OperationType: operationtype.Delete, + }, + }, + ), + Schema: map[string]*schema.Schema{ + "id": { + Description: "ID of this resource in Cyral environment", + Type: schema.TypeString, + Computed: true, + }, + "name": { + Description: "Sidecar name that will be used internally in Control Plane (ex: `your_sidecar_name`).", + Type: schema.TypeString, + Required: true, + }, + "deployment_method": { + Description: "Deployment method that will be used by this sidecar (valid values: `docker`, `cft-ec2`, `terraform`, `helm3`, `automated`, `custom`, `terraformGKE`, `linux`, and `singleContainer`).", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice( + []string{ + "docker", "cft-ec2", "terraform", "helm3", + "automated", "custom", "terraformGKE", "singleContainer", + "linux", + }, false, + ), + }, + "log_integration_id": { + Description: "ID of the log integration mapped to this sidecar, used for Cyral activity logs.", + Type: schema.TypeString, + Optional: true, + Deprecated: "Since sidecar v4.8. Use `activity_log_integration_id` instead.", + ConflictsWith: []string{"activity_log_integration_id"}, + }, + "activity_log_integration_id": { + Description: "ID of the log integration mapped to this sidecar, used for Cyral activity logs.", + Type: schema.TypeString, + Optional: true, + }, + "diagnostic_log_integration_id": { + Description: "ID of the log integration mapped to this sidecar, used for sidecar diagnostic logs.", + Type: schema.TypeString, + Optional: true, + }, + "labels": { + Description: "Labels that can be attached to the sidecar and shown in the `Tags` field in the UI.", + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "user_endpoint": { + Description: "User-defined endpoint (also referred as `alias`) that can be used to override the sidecar DNS endpoint shown in the UI.", + Type: schema.TypeString, + Optional: true, + }, + "bypass_mode": { + Description: "This argument lets you specify how to handle the connection in the event of an error in the sidecar during a user’s session. Valid modes are: `always`, `failover` or `never`. Defaults to `failover`. If `always` is specified, the sidecar will run in [passthrough mode](https://cyral.com/docs/sidecars/sidecar-manage#passthrough-mode). If `failover` is specified, the sidecar will run in [resiliency mode](https://cyral.com/docs/sidecars/sidecar-manage#resilient-mode-of-sidecar-operation). If `never` is specified and there is an error in the sidecar, connections to bound repositories will fail.", + Type: schema.TypeString, + Optional: true, + Default: "failover", + ValidateFunc: validation.StringInSlice( + []string{ + "always", + "failover", + "never", + }, false, + ), + }, + "certificate_bundle_secrets": { + Deprecated: "Since sidecar v4.7 the certificate is managed at deployment level. Refer" + + " to [our public docs](https://cyral.com/docs/sidecars/deployment/certificates)" + + " for more information.", + Description: "Certificate Bundle Secret is a configuration that holds data about the" + + " location of a particular TLS certificate bundle in a secrets manager.", + Type: schema.TypeSet, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "sidecar": { + Description: "Certificate Bundle Secret for sidecar.", + Type: schema.TypeSet, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "engine": { + Description: "Engine is the name of the engine used with the given secrets" + + " manager type, when applicable.", + Type: schema.TypeString, + Optional: true, + }, + "secret_id": { + Description: "Secret ID is the identifier or location for the secret that" + + " holds the certificate bundle.", + Type: schema.TypeString, + Required: true, + }, + "type": { + Description: "Type identifies the secret manager used to store the secret. Valid values are: `aws` and `k8s`.", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice( + []string{ + "aws", + "k8s", + }, false, + ), + }, + }, + }, + }, + }, + }, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} diff --git a/cyral/internal/sidecar/resource_cyral_sidecar.go b/cyral/internal/sidecar/resource_cyral_sidecar.go deleted file mode 100644 index bff5aa1d..00000000 --- a/cyral/internal/sidecar/resource_cyral_sidecar.go +++ /dev/null @@ -1,433 +0,0 @@ -package sidecar - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "regexp" - - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - "github.com/cyralinc/terraform-provider-cyral/cyral/client" - "github.com/cyralinc/terraform-provider-cyral/cyral/utils" -) - -type CreateSidecarResponse struct { - ID string `json:"ID"` -} - -type SidecarData struct { - ID string `json:"id"` - Name string `json:"name"` - Labels []string `json:"labels"` - SidecarProperties *SidecarProperties `json:"properties"` - ServicesConfig SidecarServicesConfig `json:"services"` - UserEndpoint string `json:"userEndpoint"` - CertificateBundleSecrets CertificateBundleSecrets `json:"certificateBundleSecrets,omitempty"` -} - -func (sd *SidecarData) BypassMode() string { - if sd.ServicesConfig != nil { - if dispConfig, ok := sd.ServicesConfig["dispatcher"]; ok { - if bypass_mode, ok := dispConfig["bypass"]; ok { - return bypass_mode - } - } - } - return "" -} - -type SidecarProperties struct { - DeploymentMethod string `json:"deploymentMethod"` - LogIntegrationID string `json:"logIntegrationID,omitempty"` - DiagnosticLogIntegrationID string `json:"diagnosticLogIntegrationID,omitempty"` -} - -func NewSidecarProperties(deploymentMethod, activityLogIntegrationID, diagnosticLogIntegrationID string) *SidecarProperties { - return &SidecarProperties{ - DeploymentMethod: deploymentMethod, - LogIntegrationID: activityLogIntegrationID, - DiagnosticLogIntegrationID: diagnosticLogIntegrationID, - } -} - -type SidecarServicesConfig map[string]map[string]string - -type CertificateBundleSecrets map[string]*CertificateBundleSecret - -type CertificateBundleSecret struct { - Engine string `json:"engine,omitempty"` - SecretId string `json:"secretId,omitempty"` - Type string `json:"type,omitempty"` -} - -func ResourceSidecar() *schema.Resource { - return &schema.Resource{ - Description: "Manages [sidecars](https://cyral.com/docs/sidecars/sidecar-manage).", - CreateContext: resourceSidecarCreate, - ReadContext: resourceSidecarRead, - UpdateContext: resourceSidecarUpdate, - DeleteContext: resourceSidecarDelete, - - Schema: map[string]*schema.Schema{ - "id": { - Description: "ID of this resource in Cyral environment", - Type: schema.TypeString, - Computed: true, - }, - "name": { - Description: "Sidecar name that will be used internally in Control Plane (ex: `your_sidecar_name`).", - Type: schema.TypeString, - Required: true, - }, - "deployment_method": { - Description: "Deployment method that will be used by this sidecar (valid values: `docker`, `cft-ec2`, `terraform`, `helm3`, `automated`, `custom`, `terraformGKE`, `linux`, and `singleContainer`).", - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice( - []string{ - "docker", "cft-ec2", "terraform", "helm3", - "automated", "custom", "terraformGKE", "singleContainer", - "linux", - }, false, - ), - }, - "log_integration_id": { - Description: "ID of the log integration mapped to this sidecar, used for Cyral activity logs.", - Type: schema.TypeString, - Optional: true, - Deprecated: "Since sidecar v4.8. Use `activity_log_integration_id` instead.", - ConflictsWith: []string{"activity_log_integration_id"}, - }, - "activity_log_integration_id": { - Description: "ID of the log integration mapped to this sidecar, used for Cyral activity logs.", - Type: schema.TypeString, - Optional: true, - }, - "diagnostic_log_integration_id": { - Description: "ID of the log integration mapped to this sidecar, used for sidecar diagnostic logs.", - Type: schema.TypeString, - Optional: true, - }, - "labels": { - Description: "Labels that can be attached to the sidecar and shown in the `Tags` field in the UI.", - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "user_endpoint": { - Description: "User-defined endpoint (also referred as `alias`) that can be used to override the sidecar DNS endpoint shown in the UI.", - Type: schema.TypeString, - Optional: true, - }, - "bypass_mode": { - Description: "This argument lets you specify how to handle the connection in the event of an error in the sidecar during a user’s session. Valid modes are: `always`, `failover` or `never`. Defaults to `failover`. If `always` is specified, the sidecar will run in [passthrough mode](https://cyral.com/docs/sidecars/sidecar-manage#passthrough-mode). If `failover` is specified, the sidecar will run in [resiliency mode](https://cyral.com/docs/sidecars/sidecar-manage#resilient-mode-of-sidecar-operation). If `never` is specified and there is an error in the sidecar, connections to bound repositories will fail.", - Type: schema.TypeString, - Optional: true, - Default: "failover", - ValidateFunc: validation.StringInSlice( - []string{ - "always", - "failover", - "never", - }, false, - ), - }, - "certificate_bundle_secrets": { - Deprecated: "Since sidecar v4.7 the certificate is managed at deployment level. Refer" + - " to [our public docs](https://cyral.com/docs/sidecars/deployment/certificates)" + - " for more information.", - Description: "Certificate Bundle Secret is a configuration that holds data about the" + - " location of a particular TLS certificate bundle in a secrets manager.", - Type: schema.TypeSet, - MaxItems: 1, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "sidecar": { - Description: "Certificate Bundle Secret for sidecar.", - Type: schema.TypeSet, - MaxItems: 1, - Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "engine": { - Description: "Engine is the name of the engine used with the given secrets" + - " manager type, when applicable.", - Type: schema.TypeString, - Optional: true, - }, - "secret_id": { - Description: "Secret ID is the identifier or location for the secret that" + - " holds the certificate bundle.", - Type: schema.TypeString, - Required: true, - }, - "type": { - Description: "Type identifies the secret manager used to store the secret. Valid values are: `aws` and `k8s`.", - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice( - []string{ - "aws", - "k8s", - }, false, - ), - }, - }, - }, - }, - }, - }, - }, - }, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - } -} - -func resourceSidecarCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceSidecarCreate") - c := m.(*client.Client) - - resourceData, err := getSidecarDataFromResource(c, d) - if err != nil { - return utils.CreateError("Unable to create sidecar", fmt.Sprintf("%v", err)) - } - - url := fmt.Sprintf("https://%s/v1/sidecars", c.ControlPlane) - - body, err := c.DoRequest(ctx, url, http.MethodPost, resourceData) - if err != nil { - return utils.CreateError("Unable to create sidecar", fmt.Sprintf("%v", err)) - } - - response := SidecarData{} - if err := json.Unmarshal(body, &response); err != nil { - return utils.CreateError("Unable to unmarshall JSON", fmt.Sprintf("%v", err)) - } - tflog.Debug(ctx, fmt.Sprintf("Response body (unmarshalled): %#v", response)) - - d.SetId(response.ID) - - return resourceSidecarRead(ctx, d, m) -} - -func resourceSidecarRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceSidecarRead") - c := m.(*client.Client) - - url := fmt.Sprintf("https://%s/v1/sidecars/%s", c.ControlPlane, d.Id()) - - body, err := c.DoRequest(ctx, url, http.MethodGet, nil) - if err != nil { - // Currently, the sidecar API always returns a status code of 500 for every error, - // so its not possible to distinguish if the error returned is related to - // a 404 Not Found or not by its status code. This way, a workaround for that is to - // check if the error message matches a 'Failed to extract info for wrapper' message, - // since thats the current message returned when the sidecar is not found. Once this - // issue is fixed in the sidecar API, we should handle the error here by its status - // code, and only remove the resource from the state (d.SetId("")) if it returns a 404 - // Not Found. - matched, regexpError := regexp.MatchString( - "Failed to extract info for wrapper", - err.Error(), - ) - if regexpError == nil && matched { - tflog.Debug(ctx, fmt.Sprintf("Sidecar not found. SidecarID: %s. "+ - "Removing it from state. Error: %v", d.Id(), err)) - d.SetId("") - return nil - } - - return utils.CreateError( - fmt.Sprintf( - "Unable to read sidecar. SidecarID: %s", - d.Id(), - ), fmt.Sprintf("%v", err), - ) - } - - response := SidecarData{} - if err := json.Unmarshal(body, &response); err != nil { - return utils.CreateError("Unable to unmarshall JSON", fmt.Sprintf("%v", err)) - } - tflog.Debug(ctx, fmt.Sprintf("Response body (unmarshalled): %#v", response)) - - d.Set("name", response.Name) - if properties := response.SidecarProperties; properties != nil { - d.Set("deployment_method", properties.DeploymentMethod) - d.Set("activity_log_integration_id", properties.LogIntegrationID) - d.Set("diagnostic_log_integration_id", properties.DiagnosticLogIntegrationID) - } - d.Set("labels", response.Labels) - d.Set("user_endpoint", response.UserEndpoint) - if bypassMode := response.BypassMode(); bypassMode != "" { - d.Set("bypass_mode", bypassMode) - } - d.Set("certificate_bundle_secrets", flattenCertificateBundleSecrets(response.CertificateBundleSecrets)) - - tflog.Debug(ctx, "End resourceSidecarRead") - - return diag.Diagnostics{} -} - -func resourceSidecarUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceSidecarUpdate") - c := m.(*client.Client) - - resourceData, err := getSidecarDataFromResource(c, d) - if err != nil { - return utils.CreateError("Unable to update sidecar", fmt.Sprintf("%v", err)) - } - - url := fmt.Sprintf("https://%s/v1/sidecars/%s", c.ControlPlane, d.Id()) - - if _, err = c.DoRequest(ctx, url, http.MethodPut, resourceData); err != nil { - return utils.CreateError("Unable to update sidecar", fmt.Sprintf("%v", err)) - } - - tflog.Debug(ctx, "End resourceSidecarUpdate") - - return resourceSidecarRead(ctx, d, m) -} - -func resourceSidecarDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "Init resourceSidecarDelete") - c := m.(*client.Client) - - url := fmt.Sprintf("https://%s/v1/sidecars/%s", c.ControlPlane, d.Id()) - - if _, err := c.DoRequest(ctx, url, http.MethodDelete, nil); err != nil { - return utils.CreateError("Unable to delete sidecar", fmt.Sprintf("%v", err)) - } - - tflog.Debug(ctx, "End resourceSidecarDelete") - - return diag.Diagnostics{} -} - -func getSidecarDataFromResource(c *client.Client, d *schema.ResourceData) (*SidecarData, error) { - ctx := context.Background() - tflog.Debug(ctx, "Init getSidecarDataFromResource") - - deploymentMethod := d.Get("deployment_method").(string) - - activityLogIntegrationID := d.Get("activity_log_integration_id").(string) - if activityLogIntegrationID == "" { - activityLogIntegrationID = d.Get("log_integration_id").(string) - } - diagnosticLogIntegrationID := d.Get("diagnostic_log_integration_id").(string) - - properties := NewSidecarProperties(deploymentMethod, activityLogIntegrationID, diagnosticLogIntegrationID) - - svcconf := SidecarServicesConfig{ - "dispatcher": map[string]string{ - "bypass": d.Get("bypass_mode").(string), - }, - } - - labels := d.Get("labels").([]interface{}) - sidecarDataLabels := []string{} - for _, labelInterface := range labels { - if label, ok := labelInterface.(string); ok { - sidecarDataLabels = append(sidecarDataLabels, label) - } - } - - cbs := getCertificateBundleSecret(d) - - tflog.Debug(ctx, "end getSidecarDataFromResource") - return &SidecarData{ - ID: d.Id(), - Name: d.Get("name").(string), - Labels: sidecarDataLabels, - SidecarProperties: properties, - ServicesConfig: svcconf, - UserEndpoint: d.Get("user_endpoint").(string), - CertificateBundleSecrets: cbs, - }, nil -} - -func flattenCertificateBundleSecrets(cbs CertificateBundleSecrets) []interface{} { - ctx := context.Background() - tflog.Debug(ctx, "Init flattenCertificateBundleSecrets") - var flatCBS []interface{} - if cbs != nil { - cb := make(map[string]interface{}) - - for key, val := range cbs { - // Ignore self-signed certificates - if key != "sidecar-generated-selfsigned" { - contentCB := make([]interface{}, 1) - - tflog.Debug(ctx, fmt.Sprintf("key: %v", key)) - tflog.Debug(ctx, fmt.Sprintf("val: %v", val)) - - contentCBMap := make(map[string]interface{}) - contentCBMap["secret_id"] = val.SecretId - contentCBMap["engine"] = val.Engine - contentCBMap["type"] = val.Type - - contentCB[0] = contentCBMap - cb[key] = contentCB - } - } - - if len(cb) > 0 { - flatCBS = make([]interface{}, 1) - flatCBS[0] = cb - } - } - - tflog.Debug(ctx, fmt.Sprintf("end flattenCertificateBundleSecrets %v", flatCBS)) - return flatCBS -} - -func getCertificateBundleSecret(d *schema.ResourceData) CertificateBundleSecrets { - ctx := context.Background() - tflog.Debug(ctx, "Init getCertificateBundleSecret") - rdCBS := d.Get("certificate_bundle_secrets").(*schema.Set).List() - ret := make(CertificateBundleSecrets) - - if len(rdCBS) > 0 { - cbsMap := rdCBS[0].(map[string]interface{}) - for k, v := range cbsMap { - vList := v.(*schema.Set).List() - // 1. k = "sidecar" or other direct internal elements of certificate_bundle_secrets - // 2. Also one element on this list due to MaxItems... - // 3. Ignore self signed certificates - if len(vList) > 0 && k != "sidecar-generated-selfsigned" { - vMap := vList[0].(map[string]interface{}) - engine := "" - if val, ok := vMap["engine"]; val != nil && ok { - engine = val.(string) - } - cbsType := vMap["type"].(string) - secretId := vMap["secret_id"].(string) - cbs := CertificateBundleSecret{ - SecretId: secretId, - Engine: engine, - Type: cbsType, - } - ret[k] = &cbs - } - } - } - - // If the occurrence of `sidecar` does not exist, set it to an empty certificate bundle - // so that the API can remove the `sidecar` key from the persisted certificate bundle map. - if _, ok := ret["sidecar"]; !ok { - ret["sidecar"] = &CertificateBundleSecret{} - } - - tflog.Debug(ctx, "end getCertificateBundleSecret") - return ret -} diff --git a/cyral/internal/sidecar/resource_cyral_sidecar_test.go b/cyral/internal/sidecar/resource_test.go similarity index 74% rename from cyral/internal/sidecar/resource_cyral_sidecar_test.go rename to cyral/internal/sidecar/resource_test.go index 1eca02b2..18c5de15 100644 --- a/cyral/internal/sidecar/resource_cyral_sidecar_test.go +++ b/cyral/internal/sidecar/resource_test.go @@ -21,56 +21,84 @@ func getTestCBS() sidecar.CertificateBundleSecrets { } var cloudFormationSidecarConfig = sidecar.SidecarData{ - Name: utils.AccTestName(utils.SidecarResourceName, "cft"), - Labels: []string{"test1"}, - SidecarProperties: sidecar.NewSidecarProperties("cft-ec2", "foo", ""), + Name: utils.AccTestName(utils.SidecarResourceName, "cft"), + Labels: []string{"test1"}, + SidecarProperties: &sidecar.SidecarProperties{ + DeploymentMethod: "cft-ec2", + LogIntegrationID: "foo", + DiagnosticLogIntegrationID: "", + }, UserEndpoint: "some.cft.user.endpoint", CertificateBundleSecrets: getTestCBS(), } var dockerSidecarConfig = sidecar.SidecarData{ - Name: utils.AccTestName(utils.SidecarResourceName, "docker"), - Labels: []string{"test2"}, - SidecarProperties: sidecar.NewSidecarProperties("docker", "bar", ""), + Name: utils.AccTestName(utils.SidecarResourceName, "docker"), + Labels: []string{"test2"}, + SidecarProperties: &sidecar.SidecarProperties{ + DeploymentMethod: "docker", + LogIntegrationID: "bar", + DiagnosticLogIntegrationID: "", + }, UserEndpoint: "some.docker.user.endpoint", CertificateBundleSecrets: getTestCBS(), } var helmSidecarConfig = sidecar.SidecarData{ - Name: utils.AccTestName(utils.SidecarResourceName, "helm3"), - Labels: []string{"test3"}, - SidecarProperties: sidecar.NewSidecarProperties("helm3", "baz", ""), + Name: utils.AccTestName(utils.SidecarResourceName, "helm3"), + Labels: []string{"test3"}, + SidecarProperties: &sidecar.SidecarProperties{ + DeploymentMethod: "helm3", + LogIntegrationID: "baz", + DiagnosticLogIntegrationID: "", + }, UserEndpoint: "some.helm3.user.endpoint", CertificateBundleSecrets: getTestCBS(), } var tfSidecarConfig = sidecar.SidecarData{ - Name: utils.AccTestName(utils.SidecarResourceName, "tf"), - Labels: []string{"test4"}, - SidecarProperties: sidecar.NewSidecarProperties("terraform", "qux", ""), + Name: utils.AccTestName(utils.SidecarResourceName, "tf"), + Labels: []string{"test4"}, + SidecarProperties: &sidecar.SidecarProperties{ + DeploymentMethod: "terraform", + LogIntegrationID: "qux", + DiagnosticLogIntegrationID: "", + }, UserEndpoint: "some.tf.user.endpoint", CertificateBundleSecrets: getTestCBS(), } var singleContainerSidecarConfig = sidecar.SidecarData{ - Name: utils.AccTestName(utils.SidecarResourceName, "singleContainer"), - Labels: []string{"test5"}, - SidecarProperties: sidecar.NewSidecarProperties("singleContainer", "quxx", ""), + Name: utils.AccTestName(utils.SidecarResourceName, "singleContainer"), + Labels: []string{"test5"}, + SidecarProperties: &sidecar.SidecarProperties{ + DeploymentMethod: "singleContainer", + LogIntegrationID: "quxx", + DiagnosticLogIntegrationID: "", + }, UserEndpoint: "some.singleContainer.user.endpoint", CertificateBundleSecrets: getTestCBS(), } var linuxSidecarConfig = sidecar.SidecarData{ - Name: utils.AccTestName(utils.SidecarResourceName, "linux"), - Labels: []string{"test6"}, - SidecarProperties: sidecar.NewSidecarProperties("linux", "empty", ""), + Name: utils.AccTestName(utils.SidecarResourceName, "linux"), + Labels: []string{"test6"}, + SidecarProperties: &sidecar.SidecarProperties{ + DeploymentMethod: "linux", + LogIntegrationID: "empty", + DiagnosticLogIntegrationID: "", + }, UserEndpoint: "some.linux.user.endpoint", CertificateBundleSecrets: getTestCBS(), } var bypassNeverSidecarConfig = sidecar.SidecarData{ - Name: utils.AccTestName(utils.SidecarResourceName, "bypassNeverSidecar"), - SidecarProperties: sidecar.NewSidecarProperties("terraform", "a", ""), + Name: utils.AccTestName(utils.SidecarResourceName, "bypassNeverSidecar"), + SidecarProperties: &sidecar.SidecarProperties{ + DeploymentMethod: "terraform", + LogIntegrationID: "a", + DiagnosticLogIntegrationID: "", + }, ServicesConfig: sidecar.SidecarServicesConfig{ "dispatcher": map[string]string{ "bypass": "never", @@ -80,8 +108,12 @@ var bypassNeverSidecarConfig = sidecar.SidecarData{ } var bypassAlwaysSidecarConfig = sidecar.SidecarData{ - Name: utils.AccTestName(utils.SidecarResourceName, "bypassAlwaysSidecar"), - SidecarProperties: sidecar.NewSidecarProperties("terraform", "b", ""), + Name: utils.AccTestName(utils.SidecarResourceName, "bypassAlwaysSidecar"), + SidecarProperties: &sidecar.SidecarProperties{ + DeploymentMethod: "terraform", + LogIntegrationID: "b", + DiagnosticLogIntegrationID: "", + }, ServicesConfig: sidecar.SidecarServicesConfig{ "dispatcher": map[string]string{ "bypass": "always", diff --git a/cyral/internal/sidecar/schema_loader.go b/cyral/internal/sidecar/schema_loader.go new file mode 100644 index 00000000..6e5c4809 --- /dev/null +++ b/cyral/internal/sidecar/schema_loader.go @@ -0,0 +1,26 @@ +package sidecar + +import ( + "github.com/cyralinc/terraform-provider-cyral/cyral/core" +) + +type packageSchema struct { +} + +func (p *packageSchema) Name() string { + return "sidecar" +} + +func (p *packageSchema) Schemas() []*core.SchemaDescriptor { + return []*core.SchemaDescriptor{ + { + Name: resourceName, + Type: core.ResourceSchemaType, + Schema: resourceSchema, + }, + } +} + +func PackageSchema() core.PackageSchema { + return &packageSchema{} +} diff --git a/cyral/provider/provider.go b/cyral/provider/provider.go index 0a7dbcd0..609efb86 100644 --- a/cyral/provider/provider.go +++ b/cyral/provider/provider.go @@ -21,21 +21,12 @@ import ( "github.com/cyralinc/terraform-provider-cyral/cyral/internal/policy" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/policy/rule" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/regopolicy" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/accessgateway" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/accessrules" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/binding" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/confanalysis" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/confauth" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/network" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/role" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/samlconfiguration" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/serviceaccount" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/sidecar" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/sidecar/credentials" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/sidecar/health" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/sidecar/instance" - "github.com/cyralinc/terraform-provider-cyral/cyral/internal/sidecar/listener" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/systeminfo" ) @@ -112,7 +103,6 @@ func getDataSourceMap(ps []core.PackageSchema) map[string]*schema.Resource { schemaMap["cyral_integration_idp_saml"] = idpsaml.DataSourceIntegrationIdPSAML() schemaMap["cyral_integration_logging"] = logging.DataSourceIntegrationLogging() schemaMap["cyral_permission"] = permission.DataSourcePermission() - schemaMap["cyral_repository"] = repository.DataSourceRepository() schemaMap["cyral_role"] = role.DataSourceRole() schemaMap["cyral_saml_configuration"] = samlconfiguration.DataSourceSAMLConfiguration() schemaMap["cyral_sidecar_bound_ports"] = sidecar.DataSourceSidecarBoundPorts() @@ -122,7 +112,6 @@ func getDataSourceMap(ps []core.PackageSchema) map[string]*schema.Resource { schemaMap["cyral_sidecar_instance_ids"] = deprecated.DataSourceSidecarInstanceIDs() schemaMap["cyral_sidecar_instance_stats"] = instance.DataSourceSidecarInstanceStats() schemaMap["cyral_sidecar_instance"] = instance.DataSourceSidecarInstance() - schemaMap["cyral_sidecar_listener"] = listener.DataSourceSidecarListener() schemaMap["cyral_system_info"] = systeminfo.DataSourceSystemInfo() tflog.Debug(ctx, "End getDataSourceMap") @@ -168,19 +157,9 @@ func getResourceMap(ps []core.PackageSchema) map[string]*schema.Resource { schemaMap["cyral_policy"] = policy.ResourcePolicy() schemaMap["cyral_policy_rule"] = rule.ResourcePolicyRule() schemaMap["cyral_rego_policy_instance"] = regopolicy.ResourceRegoPolicyInstance() - schemaMap["cyral_repository"] = repository.ResourceRepository() - schemaMap["cyral_repository_access_rules"] = accessrules.ResourceRepositoryAccessRules() - schemaMap["cyral_repository_access_gateway"] = accessgateway.ResourceRepositoryAccessGateway() - schemaMap["cyral_repository_binding"] = binding.ResourceRepositoryBinding() - schemaMap["cyral_repository_conf_auth"] = confauth.ResourceRepositoryConfAuth() - schemaMap["cyral_repository_conf_analysis"] = confanalysis.ResourceRepositoryConfAnalysis() - schemaMap["cyral_repository_network_access_policy"] = network.ResourceRepositoryNetworkAccessPolicy() schemaMap["cyral_role"] = role.ResourceRole() schemaMap["cyral_role_sso_groups"] = role.ResourceRoleSSOGroups() schemaMap["cyral_service_account"] = serviceaccount.ResourceServiceAccount() - schemaMap["cyral_sidecar"] = sidecar.ResourceSidecar() - schemaMap["cyral_sidecar_credentials"] = credentials.ResourceSidecarCredentials() - schemaMap["cyral_sidecar_listener"] = listener.ResourceSidecarListener() tflog.Debug(ctx, "End getResourceMap") diff --git a/cyral/provider/schema_loader.go b/cyral/provider/schema_loader.go index f74ebf4b..4cc5f372 100644 --- a/cyral/provider/schema_loader.go +++ b/cyral/provider/schema_loader.go @@ -6,18 +6,38 @@ import ( "github.com/cyralinc/terraform-provider-cyral/cyral/internal/integration/hcvault" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/integration/slack" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/integration/teams" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/accessgateway" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/accessrules" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/binding" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/confanalysis" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/confauth" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/datamap" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/network" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/repository/useraccount" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/samlcertificate" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/sidecar" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/sidecar/credentials" + "github.com/cyralinc/terraform-provider-cyral/cyral/internal/sidecar/listener" "github.com/cyralinc/terraform-provider-cyral/cyral/internal/tokensettings" ) func packagesSchemas() []core.PackageSchema { v := []core.PackageSchema{ + accessgateway.PackageSchema(), + accessrules.PackageSchema(), + binding.PackageSchema(), + confanalysis.PackageSchema(), + confauth.PackageSchema(), + credentials.PackageSchema(), datalabel.PackageSchema(), datamap.PackageSchema(), hcvault.PackageSchema(), + listener.PackageSchema(), + network.PackageSchema(), + repository.PackageSchema(), samlcertificate.PackageSchema(), + sidecar.PackageSchema(), slack.PackageSchema(), teams.PackageSchema(), tokensettings.PackageSchema(), diff --git a/docs/resources/repository_conf_analysis.md b/docs/resources/repository_conf_analysis.md index 92bd5fda..44086546 100644 --- a/docs/resources/repository_conf_analysis.md +++ b/docs/resources/repository_conf_analysis.md @@ -3,12 +3,12 @@ page_title: "cyral_repository_conf_analysis Resource - terraform-provider-cyral" subcategory: "" description: |- - Manages Repository Analysis Configuration. This resource allows configuring both Log Settings https://cyral.com/docs/manage-repositories/repo-log-volume and Advanced settings https://cyral.com/docs/manage-repositories/repo-advanced-settings (Logs, Alerts, Analysis and Enforcement) configurations for Data Repositories. + Manages Repository Analysis Configuration. This resource allows configuring Data Activity Logs https://cyral.com/docs/data-repos/config/#data-activity-logs, Alerts https://cyral.com/docs/data-repos/config/#alerts and Policy Enforcement https://cyral.com/docs/data-repos/config/#policy-enforcement settings for Data Repositories. --- # cyral_repository_conf_analysis (Resource) -Manages Repository Analysis Configuration. This resource allows configuring both [Log Settings](https://cyral.com/docs/manage-repositories/repo-log-volume) and [Advanced settings](https://cyral.com/docs/manage-repositories/repo-advanced-settings) (Logs, Alerts, Analysis and Enforcement) configurations for Data Repositories. +Manages Repository Analysis Configuration. This resource allows configuring [Data Activity Logs](https://cyral.com/docs/data-repos/config/#data-activity-logs), [Alerts](https://cyral.com/docs/data-repos/config/#alerts) and [Policy Enforcement](https://cyral.com/docs/data-repos/config/#policy-enforcement) settings for Data Repositories. ## Example Usage diff --git a/docs/resources/repository_conf_auth.md b/docs/resources/repository_conf_auth.md index cb6cd978..290cd55f 100644 --- a/docs/resources/repository_conf_auth.md +++ b/docs/resources/repository_conf_auth.md @@ -1,6 +1,6 @@ # cyral_repository_conf_auth (Resource) -Manages the [Repository Authentication settings](https://cyral.com/docs/manage-repositories/repo-advanced-settings/#authentication) that is shown in the Advanced tab. +Manages Repository Analysis Configuration. This resource allows configuring both [Log Settings](https://cyral.com/docs/manage-repositories/repo-log-volume) and [Advanced settings](https://cyral.com/docs/manage-repositories/repo-advanced-settings) (Logs, Alerts, Analysis and Enforcement) configurations for Data Repositories. -> Import ID syntax is `{repository_id}`. diff --git a/docs/resources/sidecar.md b/docs/resources/sidecar.md index 4b7fa1f9..c11aafdd 100644 --- a/docs/resources/sidecar.md +++ b/docs/resources/sidecar.md @@ -3,12 +3,12 @@ page_title: "cyral_sidecar Resource - terraform-provider-cyral" subcategory: "" description: |- - Manages sidecars https://cyral.com/docs/sidecars/sidecar-manage. + Manages sidecars https://cyral.com/docs/sidecars/manage. --- # cyral_sidecar (Resource) -Manages [sidecars](https://cyral.com/docs/sidecars/sidecar-manage). +Manages [sidecars](https://cyral.com/docs/sidecars/manage). ## Example Usage