diff --git a/README.md b/README.md index a10bea3..a8ec97e 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,10 @@ terraform destory > This will generate the doc template in the `docs` folder > -> Check https://github.com/hashicorp/terraform-plugin-docs for details. +> Check https://github.com/hashicorp/terraform-plugin-docs and https://github.com/hashicorp/terraform-plugin-docs/issues/141 for details. ```bash -go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs --provider-name=terraform-provider-bytebase +GOOS=darwin GOARCH=amd64 go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs --provider-name=terraform-provider-bytebase ``` ## Release diff --git a/VERSION b/VERSION index e4c0d46..a6a3a43 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.3 \ No newline at end of file +1.0.4 \ No newline at end of file diff --git a/api/client.go b/api/client.go index 146cec6..9f742ca 100644 --- a/api/client.go +++ b/api/client.go @@ -45,11 +45,11 @@ type Client interface { // Policy // ListPolicies lists policies in a specific resource. - ListPolicies(ctx context.Context, find *PolicyFindMessage) (*ListPolicyMessage, error) + ListPolicies(ctx context.Context, parent string) (*v1pb.ListPoliciesResponse, error) // GetPolicy gets a policy in a specific resource. - GetPolicy(ctx context.Context, policyName string) (*PolicyMessage, error) + GetPolicy(ctx context.Context, policyName string) (*v1pb.Policy, error) // UpsertPolicy creates or updates the policy. - UpsertPolicy(ctx context.Context, patch *PolicyPatchMessage) (*PolicyMessage, error) + UpsertPolicy(ctx context.Context, patch *v1pb.Policy, updateMasks []string) (*v1pb.Policy, error) // DeletePolicy deletes the policy. DeletePolicy(ctx context.Context, policyName string) error diff --git a/api/deployment.go b/api/deployment.go deleted file mode 100644 index 2385bf3..0000000 --- a/api/deployment.go +++ /dev/null @@ -1,19 +0,0 @@ -package api - -// DeploymentType is the type for deployment. -type DeploymentType string - -const ( - // DeploymentTypeDatabaseCreate is the deployment type for creating databases. - DeploymentTypeDatabaseCreate DeploymentType = "DATABASE_CREATE" - // DeploymentTypeDatabaseDDL is the deployment type for updating database schemas (DDL). - DeploymentTypeDatabaseDDL DeploymentType = "DATABASE_DDL" - // DeploymentTypeDatabaseDDLGhost is the deployment type for updating database schemas using gh-ost. - DeploymentTypeDatabaseDDLGhost DeploymentType = "DATABASE_DDL_GHOST" - // DeploymentTypeDatabaseDML is the deployment type for updating database data (DML). - DeploymentTypeDatabaseDML DeploymentType = "DATABASE_DML" - // DeploymentTypeDatabaseRestorePITR is the deployment type for performing a Point-in-time Recovery. - DeploymentTypeDatabaseRestorePITR DeploymentType = "DATABASE_RESTORE_PITR" - // DeploymentTypeDatabaseDMLRollback is the deployment type for a generated rollback issue. - DeploymentTypeDatabaseDMLRollback DeploymentType = "DATABASE_DML_ROLLBACK" -) diff --git a/api/policy.go b/api/policy.go deleted file mode 100644 index fa90bb5..0000000 --- a/api/policy.go +++ /dev/null @@ -1,130 +0,0 @@ -package api - -// PolicyType is the type for the policy. -type PolicyType string - -// ApprovalStrategy is strategy for deployment approval policy. -type ApprovalStrategy string - -// ApprovalGroup is the group for deployment approval policy. -type ApprovalGroup string - -// BackupPlanSchedule is schedule for backup plan policy. -type BackupPlanSchedule string - -// SensitiveDataMaskType is the mask type for sensitive data. -type SensitiveDataMaskType string - -const ( - // PolicyTypeDeploymentApproval is the policy type for deployment approval policy. - PolicyTypeDeploymentApproval PolicyType = "DEPLOYMENT_APPROVAL" - // PolicyTypeBackupPlan is the policy type for backup plan policy. - PolicyTypeBackupPlan PolicyType = "BACKUP_PLAN" - // PolicyTypeSensitiveData is the policy type for sensitive data policy. - PolicyTypeSensitiveData PolicyType = "SENSITIVE_DATA" - // PolicyTypeAccessControl is the policy type for access control policy. - PolicyTypeAccessControl PolicyType = "ACCESS_CONTROL" - - // ApprovalStrategyAutomatic means the pipeline will automatically be approved without user intervention. - ApprovalStrategyAutomatic ApprovalStrategy = "AUTOMATIC" - // ApprovalStrategyManual means the pipeline should be manually approved by user to proceed. - ApprovalStrategyManual ApprovalStrategy = "MANUAL" - - // ApprovalGroupDBA means the assignee can be selected from the workspace owners and DBAs. - ApprovalGroupDBA ApprovalGroup = "APPROVAL_GROUP_DBA" - // ApprovalGroupOwner means the assignee can be selected from the project owners. - ApprovalGroupOwner ApprovalGroup = "APPROVAL_GROUP_PROJECT_OWNER" - - // BackupPlanScheduleUnset is NEVER backup plan policy value. - BackupPlanScheduleUnset BackupPlanSchedule = "UNSET" - // BackupPlanScheduleDaily is DAILY backup plan policy value. - BackupPlanScheduleDaily BackupPlanSchedule = "DAILY" - // BackupPlanScheduleWeekly is WEEKLY backup plan policy value. - BackupPlanScheduleWeekly BackupPlanSchedule = "WEEKLY" - - // SensitiveDataMaskTypeDefault is the sensitive data type to hide data with a default method. - // The default method is subject to change. - SensitiveDataMaskTypeDefault SensitiveDataMaskType = "DEFAULT" -) - -// DeploymentApprovalPolicy is the policy configuration for deployment approval. -type DeploymentApprovalPolicy struct { - DefaultStrategy ApprovalStrategy `json:"defaultStrategy"` - DeploymentApprovalStrategies []*DeploymentApprovalStrategy `json:"deploymentApprovalStrategies"` -} - -// DeploymentApprovalStrategy is the API message for deployment approval strategy. -type DeploymentApprovalStrategy struct { - ApprovalGroup ApprovalGroup `json:"approvalGroup"` - ApprovalStrategy ApprovalStrategy `json:"approvalStrategy"` - DeploymentType DeploymentType `json:"deploymentType"` -} - -// BackupPlanPolicy is the policy configuration for backup plan. -type BackupPlanPolicy struct { - Schedule BackupPlanSchedule `json:"schedule"` - // RetentionDuration is the minimum allowed period that backup data is kept for databases in an environment. - RetentionDuration string `json:"retentionDuration"` -} - -// SensitiveDataPolicy is the API message for sensitive data policy. -type SensitiveDataPolicy struct { - SensitiveData []*SensitiveData `json:"sensitiveData"` -} - -// SensitiveData is the API message for sensitive data. -type SensitiveData struct { - Schema string `json:"schema"` - Table string `json:"table"` - Column string `json:"column"` - MaskType SensitiveDataMaskType `json:"maskType"` -} - -// AccessControlPolicy is the API message for access control policy. -type AccessControlPolicy struct { - DisallowRules []*AccessControlRule `json:"disallowRules"` -} - -// AccessControlRule is the API message for access control rule. -type AccessControlRule struct { - FullDatabase bool `json:"fullDatabase"` -} - -// PolicyFindMessage is the API message for finding policies. -type PolicyFindMessage struct { - Parent string - Type *PolicyType -} - -// PolicyMessage is the API message for policy. -type PolicyMessage struct { - Name string `json:"name"` - InheritFromParent bool `json:"inheritFromParent"` - Type PolicyType `json:"type"` - Enforce bool `json:"enforce"` - - // The policy payload - DeploymentApprovalPolicy *DeploymentApprovalPolicy `json:"deploymentApprovalPolicy"` - BackupPlanPolicy *BackupPlanPolicy `json:"backupPlanPolicy"` - SensitiveDataPolicy *SensitiveDataPolicy `json:"sensitiveDataPolicy"` - AccessControlPolicy *AccessControlPolicy `json:"accessControlPolicy"` -} - -// PolicyPatchMessage is the API message to patch the policy. -type PolicyPatchMessage struct { - Name string `json:"name"` - InheritFromParent *bool `json:"inheritFromParent"` - Enforce *bool `json:"enforce"` - - // The policy payload - DeploymentApprovalPolicy *DeploymentApprovalPolicy `json:"deploymentApprovalPolicy"` - BackupPlanPolicy *BackupPlanPolicy `json:"backupPlanPolicy"` - SensitiveDataPolicy *SensitiveDataPolicy `json:"sensitiveDataPolicy"` - AccessControlPolicy *AccessControlPolicy `json:"accessControlPolicy"` -} - -// ListPolicyMessage is the API message for list policy response. -type ListPolicyMessage struct { - Policies []*PolicyMessage `json:"policies"` - NextPageToken string `json:"nextPageToken"` -} diff --git a/client/policy.go b/client/policy.go index a84a89e..23e6e65 100644 --- a/client/policy.go +++ b/client/policy.go @@ -2,27 +2,21 @@ package client import ( "context" - "encoding/json" "fmt" "net/http" "strings" - "github.com/pkg/errors" - - "github.com/bytebase/terraform-provider-bytebase/api" + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + "google.golang.org/protobuf/encoding/protojson" ) // ListPolicies lists policies in a specific resource. -func (c *client) ListPolicies(ctx context.Context, find *api.PolicyFindMessage) (*api.ListPolicyMessage, error) { - if find.Type != nil { - return nil, errors.Errorf("invalid request, list policies cannot specific the policy type") - } - +func (c *client) ListPolicies(ctx context.Context, parent string) (*v1pb.ListPoliciesResponse, error) { var url string - if find.Parent == "" { + if parent == "" { url = fmt.Sprintf("%s/%s/policies", c.url, c.version) } else { - url = fmt.Sprintf("%s/%s/%s/policies", c.url, c.version, find.Parent) + url = fmt.Sprintf("%s/%s/%s/policies", c.url, c.version, parent) } req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { @@ -34,8 +28,8 @@ func (c *client) ListPolicies(ctx context.Context, find *api.PolicyFindMessage) return nil, err } - var res api.ListPolicyMessage - if err := json.Unmarshal(body, &res); err != nil { + var res v1pb.ListPoliciesResponse + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { return nil, err } @@ -43,7 +37,7 @@ func (c *client) ListPolicies(ctx context.Context, find *api.PolicyFindMessage) } // GetPolicy gets a policy in a specific resource. -func (c *client) GetPolicy(ctx context.Context, policyName string) (*api.PolicyMessage, error) { +func (c *client) GetPolicy(ctx context.Context, policyName string) (*v1pb.Policy, error) { req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", c.url, c.version, policyName), nil) if err != nil { return nil, err @@ -54,8 +48,8 @@ func (c *client) GetPolicy(ctx context.Context, policyName string) (*api.PolicyM return nil, err } - var res api.PolicyMessage - if err := json.Unmarshal(body, &res); err != nil { + var res v1pb.Policy + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { return nil, err } @@ -63,24 +57,13 @@ func (c *client) GetPolicy(ctx context.Context, policyName string) (*api.PolicyM } // UpsertPolicy creates or updates the policy. -func (c *client) UpsertPolicy(ctx context.Context, patch *api.PolicyPatchMessage) (*api.PolicyMessage, error) { - payload, err := json.Marshal(patch) +func (c *client) UpsertPolicy(ctx context.Context, policy *v1pb.Policy, updateMasks []string) (*v1pb.Policy, error) { + payload, err := protojson.Marshal(policy) if err != nil { return nil, err } - paths := []string{} - if patch.InheritFromParent != nil { - paths = append(paths, "inherit_from_parent") - } - if patch.DeploymentApprovalPolicy != nil || - patch.BackupPlanPolicy != nil || - patch.SensitiveDataPolicy != nil || - patch.AccessControlPolicy != nil { - paths = append(paths, "payload") - } - - req, err := http.NewRequestWithContext(ctx, "PATCH", fmt.Sprintf("%s/%s/%s?allow_missing=true&update_mask=%s", c.url, c.version, patch.Name, strings.Join(paths, ",")), strings.NewReader(string(payload))) + req, err := http.NewRequestWithContext(ctx, "PATCH", fmt.Sprintf("%s/%s/%s?allow_missing=true&update_mask=%s", c.url, c.version, policy.Name, strings.Join(updateMasks, ",")), strings.NewReader(string(payload))) if err != nil { return nil, err } @@ -90,8 +73,8 @@ func (c *client) UpsertPolicy(ctx context.Context, patch *api.PolicyPatchMessage return nil, err } - var res api.PolicyMessage - if err := json.Unmarshal(body, &res); err != nil { + var res v1pb.Policy + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { return nil, err } diff --git a/docs/data-sources/instance.md b/docs/data-sources/instance.md index b6a81f8..e75e629 100644 --- a/docs/data-sources/instance.md +++ b/docs/data-sources/instance.md @@ -37,7 +37,11 @@ Read-Only: - `database` (String) - `host` (String) - `id` (String) +- `password` (String) - `port` (String) +- `ssl_ca` (String) +- `ssl_cert` (String) +- `ssl_key` (String) - `type` (String) - `username` (String) diff --git a/docs/data-sources/instance_list.md b/docs/data-sources/instance_list.md index 9b3dfa0..4608bef 100644 --- a/docs/data-sources/instance_list.md +++ b/docs/data-sources/instance_list.md @@ -45,7 +45,11 @@ Read-Only: - `database` (String) - `host` (String) - `id` (String) +- `password` (String) - `port` (String) +- `ssl_ca` (String) +- `ssl_cert` (String) +- `ssl_key` (String) - `type` (String) - `username` (String) diff --git a/docs/data-sources/policy.md b/docs/data-sources/policy.md index 97b1dbd..c519319 100644 --- a/docs/data-sources/policy.md +++ b/docs/data-sources/policy.md @@ -21,11 +21,9 @@ The policy data source. ### Optional -- `access_control_policy` (Block List) (see [below for nested schema](#nestedblock--access_control_policy)) -- `backup_plan_policy` (Block List) (see [below for nested schema](#nestedblock--backup_plan_policy)) -- `deployment_approval_policy` (Block List) (see [below for nested schema](#nestedblock--deployment_approval_policy)) +- `masking_exception_policy` (Block List) (see [below for nested schema](#nestedblock--masking_exception_policy)) +- `masking_policy` (Block List) (see [below for nested schema](#nestedblock--masking_policy)) - `parent` (String) The policy parent name for the policy, support projects/{resource id}, environments/{resource id}, instances/{resource id}, or instances/{resource id}/databases/{database name} -- `sensitive_data_policy` (Block List) (see [below for nested schema](#nestedblock--sensitive_data_policy)) ### Read-Only @@ -34,64 +32,45 @@ The policy data source. - `inherit_from_parent` (Boolean) Decide if the policy should inherit from the parent. - `name` (String) The policy full name - -### Nested Schema for `access_control_policy` + +### Nested Schema for `masking_exception_policy` Optional: -- `disallow_rules` (Block List) (see [below for nested schema](#nestedblock--access_control_policy--disallow_rules)) +- `exceptions` (Block List) (see [below for nested schema](#nestedblock--masking_exception_policy--exceptions)) - -### Nested Schema for `access_control_policy.disallow_rules` + +### Nested Schema for `masking_exception_policy.exceptions` Optional: -- `all_databases` (Boolean) - - - - -### Nested Schema for `backup_plan_policy` - -Optional: - -- `retention_duration` (Number) The minimum allowed seconds that backup data is kept for databases in an environment. -- `schedule` (String) - - - -### Nested Schema for `deployment_approval_policy` - -Optional: - -- `default_strategy` (String) -- `deployment_approval_strategies` (Block List) (see [below for nested schema](#nestedblock--deployment_approval_policy--deployment_approval_strategies)) - - -### Nested Schema for `deployment_approval_policy.deployment_approval_strategies` - -Optional: - -- `approval_group` (String) -- `approval_strategy` (String) -- `deployment_type` (String) +- `action` (String) +- `column` (String) +- `database` (String) The database full name in instances/{instance resource id}/databases/{database name} format +- `expire_timestamp` (String) The expiration timestamp in YYYY-MM-DDThh:mm:ss.000Z format +- `masking_level` (String) +- `member` (String) The member in user:{email} format. +- `schema` (String) +- `table` (String) - -### Nested Schema for `sensitive_data_policy` + +### Nested Schema for `masking_policy` Optional: -- `sensitive_data` (Block List) (see [below for nested schema](#nestedblock--sensitive_data_policy--sensitive_data)) +- `mask_data` (Block List) (see [below for nested schema](#nestedblock--masking_policy--mask_data)) - -### Nested Schema for `sensitive_data_policy.sensitive_data` + +### Nested Schema for `masking_policy.mask_data` Optional: - `column` (String) -- `mask_type` (String) +- `full_masking_algorithm_id` (String) +- `masking_level` (String) +- `partial_masking_algorithm_id` (String) - `schema` (String) - `table` (String) diff --git a/docs/data-sources/policy_list.md b/docs/data-sources/policy_list.md index 2835182..90390fd 100644 --- a/docs/data-sources/policy_list.md +++ b/docs/data-sources/policy_list.md @@ -29,73 +29,52 @@ The policy data source list. Read-Only: -- `access_control_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--access_control_policy)) -- `backup_plan_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--backup_plan_policy)) -- `deployment_approval_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--deployment_approval_policy)) - `enforce` (Boolean) - `inherit_from_parent` (Boolean) +- `masking_exception_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--masking_exception_policy)) +- `masking_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--masking_policy)) - `name` (String) -- `sensitive_data_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--sensitive_data_policy)) - `type` (String) - -### Nested Schema for `policies.access_control_policy` + +### Nested Schema for `policies.masking_exception_policy` Read-Only: -- `disallow_rules` (List of Object) (see [below for nested schema](#nestedobjatt--policies--access_control_policy--disallow_rules)) +- `exceptions` (List of Object) (see [below for nested schema](#nestedobjatt--policies--masking_exception_policy--exceptions)) - -### Nested Schema for `policies.access_control_policy.disallow_rules` + +### Nested Schema for `policies.masking_exception_policy.exceptions` Read-Only: -- `all_databases` (Boolean) - - - - -### Nested Schema for `policies.backup_plan_policy` - -Read-Only: - -- `retention_duration` (Number) -- `schedule` (String) - - - -### Nested Schema for `policies.deployment_approval_policy` - -Read-Only: - -- `default_strategy` (String) -- `deployment_approval_strategies` (List of Object) (see [below for nested schema](#nestedobjatt--policies--deployment_approval_policy--deployment_approval_strategies)) - - -### Nested Schema for `policies.deployment_approval_policy.deployment_approval_strategies` - -Read-Only: - -- `approval_group` (String) -- `approval_strategy` (String) -- `deployment_type` (String) +- `action` (String) +- `column` (String) +- `database` (String) +- `expire_timestamp` (String) +- `masking_level` (String) +- `member` (String) +- `schema` (String) +- `table` (String) - -### Nested Schema for `policies.sensitive_data_policy` + +### Nested Schema for `policies.masking_policy` Read-Only: -- `sensitive_data` (List of Object) (see [below for nested schema](#nestedobjatt--policies--sensitive_data_policy--sensitive_data)) +- `mask_data` (List of Object) (see [below for nested schema](#nestedobjatt--policies--masking_policy--mask_data)) - -### Nested Schema for `policies.sensitive_data_policy.sensitive_data` + +### Nested Schema for `policies.masking_policy.mask_data` Read-Only: - `column` (String) -- `mask_type` (String) +- `full_masking_algorithm_id` (String) +- `masking_level` (String) +- `partial_masking_algorithm_id` (String) - `schema` (String) - `table` (String) diff --git a/docs/data-sources/setting.md b/docs/data-sources/setting.md new file mode 100644 index 0000000..0c9bf2e --- /dev/null +++ b/docs/data-sources/setting.md @@ -0,0 +1,90 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_setting Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The setting data source. +--- + +# bytebase_setting (Data Source) + +The setting data source. + + + + +## Schema + +### Required + +- `name` (String) + +### Read-Only + +- `approval_flow` (Block List) (see [below for nested schema](#nestedblock--approval_flow)) +- `external_approval_nodes` (Block List) (see [below for nested schema](#nestedblock--external_approval_nodes)) +- `id` (String) The ID of this resource. + + +### Nested Schema for `approval_flow` + +Read-Only: + +- `rules` (List of Object) (see [below for nested schema](#nestedatt--approval_flow--rules)) + + +### Nested Schema for `approval_flow.rules` + +Read-Only: + +- `conditions` (List of Object) (see [below for nested schema](#nestedobjatt--approval_flow--rules--conditions)) +- `flow` (List of Object) (see [below for nested schema](#nestedobjatt--approval_flow--rules--flow)) + + +### Nested Schema for `approval_flow.rules.conditions` + +Read-Only: + +- `level` (String) +- `source` (String) + + + +### Nested Schema for `approval_flow.rules.flow` + +Read-Only: + +- `creator` (String) +- `description` (String) +- `steps` (List of Object) (see [below for nested schema](#nestedobjatt--approval_flow--rules--flow--steps)) +- `title` (String) + + +### Nested Schema for `approval_flow.rules.flow.title` + +Read-Only: + +- `node` (String) +- `type` (String) + + + + + + +### Nested Schema for `external_approval_nodes` + +Read-Only: + +- `nodes` (List of Object) (see [below for nested schema](#nestedatt--external_approval_nodes--nodes)) + + +### Nested Schema for `external_approval_nodes.nodes` + +Read-Only: + +- `endpoint` (String) +- `id` (String) +- `title` (String) + + diff --git a/docs/resources/instance.md b/docs/resources/instance.md index d81c1df..e0a052e 100644 --- a/docs/resources/instance.md +++ b/docs/resources/instance.md @@ -45,10 +45,10 @@ Required: Optional: - `database` (String) The database for the instance, you can set this if the engine type is POSTGRES. -- `password` (String) The connection user password used by Bytebase to perform DDL and DML operations. -- `ssl_ca` (String) The CA certificate. Optional, you can set this if the engine type is MYSQL, POSTGRES, TIDB or CLICKHOUSE. -- `ssl_cert` (String) The client certificate. Optional, you can set this if the engine type is MYSQL, POSTGRES, TIDB or CLICKHOUSE. -- `ssl_key` (String) The client key. Optional, you can set this if the engine type is MYSQL, POSTGRES, TIDB or CLICKHOUSE. +- `password` (String, Sensitive) The connection user password used by Bytebase to perform DDL and DML operations. +- `ssl_ca` (String, Sensitive) The CA certificate. Optional, you can set this if the engine type is MYSQL, POSTGRES, TIDB or CLICKHOUSE. +- `ssl_cert` (String, Sensitive) The client certificate. Optional, you can set this if the engine type is MYSQL, POSTGRES, TIDB or CLICKHOUSE. +- `ssl_key` (String, Sensitive) The client key. Optional, you can set this if the engine type is MYSQL, POSTGRES, TIDB or CLICKHOUSE. - `username` (String) The connection user name used by Bytebase to perform DDL and DML operations. diff --git a/docs/resources/policy.md b/docs/resources/policy.md index 7ea158b..7df802b 100644 --- a/docs/resources/policy.md +++ b/docs/resources/policy.md @@ -22,76 +22,55 @@ The policy resource. ### Optional -- `access_control_policy` (Block List) (see [below for nested schema](#nestedblock--access_control_policy)) -- `backup_plan_policy` (Block List) (see [below for nested schema](#nestedblock--backup_plan_policy)) -- `deployment_approval_policy` (Block List) (see [below for nested schema](#nestedblock--deployment_approval_policy)) - `enforce` (Boolean) Decide if the policy is enforced. - `inherit_from_parent` (Boolean) Decide if the policy should inherit from the parent. -- `sensitive_data_policy` (Block List) (see [below for nested schema](#nestedblock--sensitive_data_policy)) +- `masking_exception_policy` (Block List) (see [below for nested schema](#nestedblock--masking_exception_policy)) +- `masking_policy` (Block List) (see [below for nested schema](#nestedblock--masking_policy)) ### Read-Only - `id` (String) The ID of this resource. - `name` (String) The policy full name - -### Nested Schema for `access_control_policy` + +### Nested Schema for `masking_exception_policy` Optional: -- `disallow_rules` (Block List) (see [below for nested schema](#nestedblock--access_control_policy--disallow_rules)) +- `exceptions` (Block List) (see [below for nested schema](#nestedblock--masking_exception_policy--exceptions)) - -### Nested Schema for `access_control_policy.disallow_rules` + +### Nested Schema for `masking_exception_policy.exceptions` Optional: -- `all_databases` (Boolean) - - - - -### Nested Schema for `backup_plan_policy` - -Optional: - -- `retention_duration` (Number) The minimum allowed seconds that backup data is kept for databases in an environment. -- `schedule` (String) - - - -### Nested Schema for `deployment_approval_policy` - -Optional: - -- `default_strategy` (String) -- `deployment_approval_strategies` (Block List) (see [below for nested schema](#nestedblock--deployment_approval_policy--deployment_approval_strategies)) - - -### Nested Schema for `deployment_approval_policy.deployment_approval_strategies` - -Optional: - -- `approval_group` (String) -- `approval_strategy` (String) -- `deployment_type` (String) +- `action` (String) +- `column` (String) +- `database` (String) The database full name in instances/{instance resource id}/databases/{database name} format +- `expire_timestamp` (String) The expiration timestamp in YYYY-MM-DDThh:mm:ss.000Z format +- `masking_level` (String) +- `member` (String) The member in user:{email} format. +- `schema` (String) +- `table` (String) - -### Nested Schema for `sensitive_data_policy` + +### Nested Schema for `masking_policy` Optional: -- `sensitive_data` (Block List) (see [below for nested schema](#nestedblock--sensitive_data_policy--sensitive_data)) +- `mask_data` (Block List) (see [below for nested schema](#nestedblock--masking_policy--mask_data)) - -### Nested Schema for `sensitive_data_policy.sensitive_data` + +### Nested Schema for `masking_policy.mask_data` Optional: - `column` (String) -- `mask_type` (String) +- `full_masking_algorithm_id` (String) +- `masking_level` (String) +- `partial_masking_algorithm_id` (String) - `schema` (String) - `table` (String) diff --git a/docs/resources/setting.md b/docs/resources/setting.md new file mode 100644 index 0000000..93938a6 --- /dev/null +++ b/docs/resources/setting.md @@ -0,0 +1,102 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_setting Resource - terraform-provider-bytebase" +subcategory: "" +description: |- + The setting resource. +--- + +# bytebase_setting (Resource) + +The setting resource. + + + + +## Schema + +### Required + +- `name` (String) + +### Optional + +- `approval_flow` (Block List) (see [below for nested schema](#nestedblock--approval_flow)) +- `external_approval_nodes` (Block List) (see [below for nested schema](#nestedblock--external_approval_nodes)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `approval_flow` + +Required: + +- `rules` (Block List, Min: 1) (see [below for nested schema](#nestedblock--approval_flow--rules)) + + +### Nested Schema for `approval_flow.rules` + +Required: + +- `flow` (Block List, Min: 1) (see [below for nested schema](#nestedblock--approval_flow--rules--flow)) + +Optional: + +- `conditions` (Block List) Match any condition will trigger this approval flow. (see [below for nested schema](#nestedblock--approval_flow--rules--conditions)) + + +### Nested Schema for `approval_flow.rules.flow` + +Required: + +- `creator` (String) The creator name in users/{email} format +- `steps` (Block List, Min: 1) Approval flow following the step order. (see [below for nested schema](#nestedblock--approval_flow--rules--flow--steps)) +- `title` (String) + +Optional: + +- `description` (String) + + +### Nested Schema for `approval_flow.rules.flow.steps` + +Required: + +- `node` (String) + +Optional: + +- `type` (String) + + + + +### Nested Schema for `approval_flow.rules.conditions` + +Optional: + +- `level` (String) +- `source` (String) + + + + + +### Nested Schema for `external_approval_nodes` + +Required: + +- `nodes` (Block List, Min: 1) (see [below for nested schema](#nestedblock--external_approval_nodes--nodes)) + + +### Nested Schema for `external_approval_nodes.nodes` + +Required: + +- `endpoint` (String) The endpoint URL to receive the approval message. Learn more: https://www.bytebase.com/docs/api/external-approval +- `id` (String) The unique external node id. +- `title` (String) The external node title. + + diff --git a/examples/policies/main.tf b/examples/policies/main.tf new file mode 100644 index 0000000..b0da04d --- /dev/null +++ b/examples/policies/main.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + bytebase = { + version = "1.0.4" + # For local development, please use "terraform.local/bytebase/bytebase" instead + source = "registry.terraform.io/bytebase/bytebase" + } + } +} + +provider "bytebase" { + # You need to replace the account and key with your Bytebase service account. + service_account = "terraform@service.bytebase.com" + service_key = "bbs_BxVIp7uQsARl8nR92ZZV" + # The Bytebase service URL. You can use the external URL in production. + # Check the docs about external URL: https://www.bytebase.com/docs/get-started/install/external-url + url = "https://bytebase.example.com" +} + +data "bytebase_policy" "masking_policy" { + parent = "instances/test-sample-instance/databases/employee" + type = "MASKING" +} + +data "bytebase_policy" "masking_exception_policy" { + parent = "projects/project-sample" + type = "MASKING_EXCEPTION" +} diff --git a/examples/settings/main.tf b/examples/settings/main.tf index b43021e..377e866 100644 --- a/examples/settings/main.tf +++ b/examples/settings/main.tf @@ -1,22 +1,26 @@ terraform { required_providers { bytebase = { - version = "1.0.3" + version = "1.0.4" # For local development, please use "terraform.local/bytebase/bytebase" instead - source = "terraform.local/bytebase/bytebase" + source = "registry.terraform.io/bytebase/bytebase" } } } provider "bytebase" { # You need to replace the account and key with your Bytebase service account. - service_account = "ed@bytebase.com" - service_key = "12345678A!" + service_account = "terraform@service.bytebase.com" + service_key = "bbs_BxVIp7uQsARl8nR92ZZV" # The Bytebase service URL. You can use the external URL in production. # Check the docs about external URL: https://www.bytebase.com/docs/get-started/install/external-url - url = "http://localhost:8080" + url = "https://bytebase.example.com" } data "bytebase_setting" "approval_flow" { name = "bb.workspace.approval" } + +data "bytebase_setting" "external_approval" { + name = "bb.workspace.approval.external" +} diff --git a/examples/setup/main.tf b/examples/setup/main.tf index 8266761..93b1dd7 100644 --- a/examples/setup/main.tf +++ b/examples/setup/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.3" + version = "1.0.4" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } @@ -45,6 +45,9 @@ resource "bytebase_environment" "prod" { # Create a new instance named "test instance" # You can replace the parameters with your real instance resource "bytebase_instance" "test" { + depends_on = [ + bytebase_environment.test + ] resource_id = local.instance_id_test environment = bytebase_environment.test.name title = "test instance" @@ -73,6 +76,10 @@ resource "bytebase_instance" "test" { # Create a new instance named "prod instance" resource "bytebase_instance" "prod" { + depends_on = [ + bytebase_environment.prod + ] + resource_id = local.instance_id_prod environment = bytebase_environment.prod.name title = "prod instance" @@ -96,6 +103,24 @@ resource "bytebase_project" "sample_project" { key = "SAMM" } +resource "bytebase_setting" "external_approval" { + name = "bb.workspace.approval.external" + + external_approval_nodes { + nodes { + id = "9e150339-f014-4835-83d7-123aeb1895ba" + title = "Example node" + endpoint = "https://example.com" + } + + nodes { + id = "49a976be-50de-4541-b2d3-f2e32f8e41ef" + title = "Example node 2" + endpoint = "https://example.com" + } + } +} + resource "bytebase_setting" "approval_flow" { name = "bb.workspace.approval" approval_flow { @@ -129,3 +154,57 @@ resource "bytebase_setting" "approval_flow" { } } } + +resource "bytebase_policy" "masking_policy" { + depends_on = [ + bytebase_instance.test + ] + + parent = "instances/test-sample-instance/databases/employee" + type = "MASKING" + enforce = true + inherit_from_parent = false + + masking_policy { + mask_data { + table = "salary" + column = "amount" + masking_level = "FULL" + } + mask_data { + table = "salary" + column = "emp_no" + masking_level = "NONE" + } + } +} + +resource "bytebase_policy" "masking_exception_policy" { + depends_on = [ + bytebase_project.sample_project + ] + + parent = bytebase_project.sample_project.name + type = "MASKING_EXCEPTION" + enforce = true + inherit_from_parent = false + + masking_exception_policy { + exceptions { + database = "instances/test-sample-instance/databases/employee" + table = "salary" + column = "amount" + masking_level = "NONE" + member = "user:ed@bytebase.com" + action = "EXPORT" + } + exceptions { + database = "instances/test-sample-instance/databases/employee" + table = "salary" + column = "amount" + masking_level = "NONE" + member = "user:ed@bytebase.com" + action = "QUERY" + } + } +} diff --git a/provider/data_source_policy.go b/provider/data_source_policy.go index bd148b2..1be5abd 100644 --- a/provider/data_source_policy.go +++ b/provider/data_source_policy.go @@ -4,12 +4,14 @@ import ( "context" "fmt" "regexp" - "strconv" "strings" "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/pkg/errors" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -34,7 +36,7 @@ func dataSourcePolicy() *schema.Resource { // project policy regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern)), // database policy - regexp.MustCompile(fmt.Sprintf("^%s%s%s%s$", internal.InstanceNamePrefix, internal.ResourceIDPattern, internal.DatabaseIDPrefix, internal.ResourceIDPattern)), + regexp.MustCompile(fmt.Sprintf("^%s%s/%s%s$", internal.InstanceNamePrefix, internal.ResourceIDPattern, internal.DatabaseIDPrefix, internal.ResourceIDPattern)), ), Description: "The policy parent name for the policy, support projects/{resource id}, environments/{resource id}, instances/{resource id}, or instances/{resource id}/databases/{database name}", }, @@ -42,10 +44,8 @@ func dataSourcePolicy() *schema.Resource { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice([]string{ - string(api.PolicyTypeDeploymentApproval), - string(api.PolicyTypeBackupPlan), - string(api.PolicyTypeSensitiveData), - string(api.PolicyTypeAccessControl), + v1pb.PolicyType_MASKING.String(), + v1pb.PolicyType_MASKING_EXCEPTION.String(), }, false), Description: "The policy type.", }, @@ -64,140 +64,13 @@ func dataSourcePolicy() *schema.Resource { Computed: true, Description: "Decide if the policy is enforced.", }, - "deployment_approval_policy": getDeploymentApprovalPolicySchema(true), - "backup_plan_policy": getBackupPlanPolicySchema(true), - "sensitive_data_policy": getSensitiveDataPolicy(true), - "access_control_policy": getAccessControlPolicy(true), + "masking_policy": getMaskingPolicySchema(true), + "masking_exception_policy": getMaskingExceptionPolicySchema(true), }, } } -func dataSourcePolicyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(api.Client) - - policyName := fmt.Sprintf("%s/%s%s", d.Get("parent").(string), internal.PolicyNamePrefix, d.Get("type").(string)) - policy, err := c.GetPolicy(ctx, policyName) - if err != nil { - return diag.FromErr(err) - } - - d.SetId(policy.Name) - return setPolicyMessage(d, policy) -} - -func setPolicyMessage(d *schema.ResourceData, policy *api.PolicyMessage) diag.Diagnostics { - parent, _, err := internal.GetPolicyParentAndType(policy.Name) - if err != nil { - return diag.Errorf("cannot parse name for policy: %s", err.Error()) - } - if err := d.Set("name", policy.Name); err != nil { - return diag.Errorf("cannot set name for policy: %s", err.Error()) - } - if err := d.Set("parent", parent); err != nil { - return diag.Errorf("cannot set parent for policy: %s", err.Error()) - } - if err := d.Set("inherit_from_parent", policy.InheritFromParent); err != nil { - return diag.Errorf("cannot set inherit_from_parent for policy: %s", err.Error()) - } - if err := d.Set("enforce", policy.Enforce); err != nil { - return diag.Errorf("cannot set enforce for policy: %s", err.Error()) - } - - if p := policy.DeploymentApprovalPolicy; p != nil { - if err := d.Set("deployment_approval_policy", flattenDeploymentApprovalPolicy(p)); err != nil { - return diag.Errorf("cannot set deployment_approval_policy: %s", err.Error()) - } - } - - if p := policy.BackupPlanPolicy; p != nil { - backupPlan, err := flattenBackupPlanPolicy(p) - if err != nil { - return diag.FromErr(err) - } - if err := d.Set("backup_plan_policy", backupPlan); err != nil { - return diag.Errorf("cannot set backup_plan_policy: %s", err.Error()) - } - } - - if p := policy.SensitiveDataPolicy; p != nil { - if err := d.Set("sensitive_data_policy", flattenSensitiveDataPolicy(p)); err != nil { - return diag.Errorf("cannot set sensitive_data_policy: %s", err.Error()) - } - } - - if p := policy.AccessControlPolicy; p != nil { - if err := d.Set("access_control_policy", flattenAccessControlPolicy(p)); err != nil { - return diag.Errorf("cannot set access_control_policy: %s", err.Error()) - } - } - - return nil -} - -func flattenDeploymentApprovalPolicy(p *api.DeploymentApprovalPolicy) []interface{} { - strategies := []interface{}{} - for _, strategy := range p.DeploymentApprovalStrategies { - raw := map[string]interface{}{} - raw["approval_group"] = strategy.ApprovalGroup - raw["approval_strategy"] = strategy.ApprovalStrategy - raw["deployment_type"] = strategy.DeploymentType - strategies = append(strategies, raw) - } - policy := map[string]interface{}{ - "default_strategy": p.DefaultStrategy, - "deployment_approval_strategies": strategies, - } - - return []interface{}{policy} -} - -func flattenBackupPlanPolicy(p *api.BackupPlanPolicy) ([]interface{}, error) { - duration := p.RetentionDuration - if strings.HasSuffix(duration, "s") { - duration = duration[:(len(duration) - 1)] - } - d, err := strconv.Atoi(duration) - if err != nil { - return nil, err - } - - policy := map[string]interface{}{ - "schedule": p.Schedule, - "retention_duration": d, - } - return []interface{}{policy}, nil -} - -func flattenSensitiveDataPolicy(p *api.SensitiveDataPolicy) []interface{} { - sensitiveDataList := []interface{}{} - for _, data := range p.SensitiveData { - raw := map[string]interface{}{} - raw["schema"] = data.Schema - raw["table"] = data.Table - raw["column"] = data.Column - raw["mask_type"] = data.MaskType - sensitiveDataList = append(sensitiveDataList, raw) - } - policy := map[string]interface{}{ - "sensitive_data": sensitiveDataList, - } - return []interface{}{policy} -} - -func flattenAccessControlPolicy(p *api.AccessControlPolicy) []interface{} { - rules := []interface{}{} - for _, rule := range p.DisallowRules { - raw := map[string]interface{}{} - raw["all_databases"] = rule.FullDatabase - rules = append(rules, raw) - } - policy := map[string]interface{}{ - "disallow_rules": rules, - } - return []interface{}{policy} -} - -func getDeploymentApprovalPolicySchema(computed bool) *schema.Schema { +func getMaskingExceptionPolicySchema(computed bool) *schema.Schema { return &schema.Schema{ Computed: computed, Optional: true, @@ -205,53 +78,68 @@ func getDeploymentApprovalPolicySchema(computed bool) *schema.Schema { Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "default_strategy": { - Type: schema.TypeString, + "exceptions": { Computed: computed, Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - string(api.ApprovalStrategyManual), - string(api.ApprovalStrategyAutomatic), - }, false), - }, - "deployment_approval_strategies": { + Default: nil, Type: schema.TypeList, - Computed: computed, - Optional: true, - MinItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "approval_group": { + "database": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The database full name in instances/{instance resource id}/databases/{database name} format", + }, + "schema": { Type: schema.TypeString, Computed: computed, Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - string(api.ApprovalGroupDBA), - string(api.ApprovalGroupOwner), - }, false), }, - "approval_strategy": { + "table": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "column": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "member": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The member in user:{email} format.", + }, + "masking_level": { Type: schema.TypeString, Computed: computed, Optional: true, ValidateFunc: validation.StringInSlice([]string{ - string(api.ApprovalStrategyManual), - string(api.ApprovalStrategyAutomatic), + v1pb.MaskingLevel_NONE.String(), + v1pb.MaskingLevel_PARTIAL.String(), }, false), }, - "deployment_type": { + "action": { Type: schema.TypeString, Computed: computed, Optional: true, ValidateFunc: validation.StringInSlice([]string{ - string(api.DeploymentTypeDatabaseCreate), - string(api.DeploymentTypeDatabaseDDL), - string(api.DeploymentTypeDatabaseDDLGhost), - string(api.DeploymentTypeDatabaseDML), - string(api.DeploymentTypeDatabaseRestorePITR), - string(api.DeploymentTypeDatabaseDMLRollback), + v1pb.MaskingExceptionPolicy_MaskingException_QUERY.String(), + v1pb.MaskingExceptionPolicy_MaskingException_EXPORT.String(), }, false), }, + "expire_timestamp": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: "The expiration timestamp in YYYY-MM-DDThh:mm:ss.000Z format", + }, }, }, }, @@ -260,36 +148,7 @@ func getDeploymentApprovalPolicySchema(computed bool) *schema.Schema { } } -func getBackupPlanPolicySchema(computed bool) *schema.Schema { - return &schema.Schema{ - Computed: computed, - Optional: true, - Default: nil, - Type: schema.TypeList, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "schedule": { - Type: schema.TypeString, - Computed: computed, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - string(api.BackupPlanScheduleUnset), - string(api.BackupPlanScheduleDaily), - string(api.BackupPlanScheduleWeekly), - }, false), - }, - "retention_duration": { - Type: schema.TypeInt, - Computed: computed, - Optional: true, - Description: "The minimum allowed seconds that backup data is kept for databases in an environment.", - }, - }, - }, - } -} - -func getSensitiveDataPolicy(computed bool) *schema.Schema { +func getMaskingPolicySchema(computed bool) *schema.Schema { return &schema.Schema{ Computed: computed, Optional: true, @@ -297,9 +156,10 @@ func getSensitiveDataPolicy(computed bool) *schema.Schema { Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "sensitive_data": { + "mask_data": { Computed: computed, Optional: true, + Default: nil, Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -309,21 +169,35 @@ func getSensitiveDataPolicy(computed bool) *schema.Schema { Optional: true, }, "table": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "column": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "full_masking_algorithm_id": { Type: schema.TypeString, Computed: computed, Optional: true, }, - "column": { + "partial_masking_algorithm_id": { Type: schema.TypeString, Computed: computed, Optional: true, }, - "mask_type": { + "masking_level": { Type: schema.TypeString, Computed: computed, Optional: true, ValidateFunc: validation.StringInSlice([]string{ - string(api.SensitiveDataMaskTypeDefault), + v1pb.MaskingLevel_NONE.String(), + v1pb.MaskingLevel_PARTIAL.String(), + v1pb.MaskingLevel_FULL.String(), }, false), }, }, @@ -334,29 +208,130 @@ func getSensitiveDataPolicy(computed bool) *schema.Schema { } } -func getAccessControlPolicy(computed bool) *schema.Schema { - return &schema.Schema{ - Computed: computed, - Optional: true, - Default: nil, - Type: schema.TypeList, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "disallow_rules": { - Computed: computed, - Optional: true, - Type: schema.TypeList, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "all_databases": { - Type: schema.TypeBool, - Computed: computed, - Optional: true, - }, - }, - }, - }, - }, - }, +func dataSourcePolicyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + policyName := fmt.Sprintf("%s/%s%s", d.Get("parent").(string), internal.PolicyNamePrefix, d.Get("type").(string)) + policy, err := c.GetPolicy(ctx, policyName) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(policy.Name) + return setPolicyMessage(d, policy) +} + +func setPolicyMessage(d *schema.ResourceData, policy *v1pb.Policy) diag.Diagnostics { + parent, _, err := internal.GetPolicyParentAndType(policy.Name) + if err != nil { + return diag.Errorf("cannot parse name for policy: %s", err.Error()) + } + if err := d.Set("name", policy.Name); err != nil { + return diag.Errorf("cannot set name for policy: %s", err.Error()) + } + if err := d.Set("parent", parent); err != nil { + return diag.Errorf("cannot set parent for policy: %s", err.Error()) + } + if err := d.Set("inherit_from_parent", policy.InheritFromParent); err != nil { + return diag.Errorf("cannot set inherit_from_parent for policy: %s", err.Error()) + } + if err := d.Set("enforce", policy.Enforce); err != nil { + return diag.Errorf("cannot set enforce for policy: %s", err.Error()) + } + + if p := policy.GetMaskingPolicy(); p != nil { + if err := d.Set("masking_policy", flattenMaskingPolicy(p)); err != nil { + return diag.Errorf("cannot set masking_policy: %s", err.Error()) + } } + if p := policy.GetMaskingExceptionPolicy(); p != nil { + exceptionPolicy, err := flattenMaskingExceptionPolicy(p) + if err != nil { + return diag.FromErr(err) + } + if err := d.Set("masking_exception_policy", exceptionPolicy); err != nil { + return diag.Errorf("cannot set masking_policy: %s", err.Error()) + } + } + + return nil +} + +func flattenMaskingPolicy(p *v1pb.MaskingPolicy) []interface{} { + maskDataList := []interface{}{} + for _, maskData := range p.MaskData { + raw := map[string]interface{}{} + raw["schema"] = maskData.Schema + raw["table"] = maskData.Table + raw["column"] = maskData.Column + raw["full_masking_algorithm_id"] = maskData.FullMaskingAlgorithmId + raw["partial_masking_algorithm_id"] = maskData.PartialMaskingAlgorithmId + raw["masking_level"] = maskData.MaskingLevel.String() + maskDataList = append(maskDataList, raw) + } + policy := map[string]interface{}{ + "mask_data": maskDataList, + } + return []interface{}{policy} +} + +func flattenMaskingExceptionPolicy(p *v1pb.MaskingExceptionPolicy) ([]interface{}, error) { + exceptionList := []interface{}{} + for _, exception := range p.MaskingExceptions { + raw := map[string]interface{}{} + raw["member"] = exception.Member + raw["action"] = exception.Action.String() + raw["masking_level"] = exception.MaskingLevel.String() + + expressions := strings.Split(exception.Condition.Expression, " && ") + instanceID := "" + databaseName := "" + for _, expression := range expressions { + if strings.HasPrefix(expression, "resource.instance_id == ") { + instanceID = strings.TrimSuffix( + strings.TrimPrefix(expression, `resource.instance_id == "`), + `"`, + ) + } + if strings.HasPrefix(expression, "resource.database_name == ") { + databaseName = strings.TrimSuffix( + strings.TrimPrefix(expression, `resource.database_name == "`), + `"`, + ) + } + if strings.HasPrefix(expression, "resource.table_name == ") { + raw["table"] = strings.TrimSuffix( + strings.TrimPrefix(expression, `resource.table_name == "`), + `"`, + ) + } + if strings.HasPrefix(expression, "resource.schema_name == ") { + raw["schema"] = strings.TrimSuffix( + strings.TrimPrefix(expression, `resource.schema_name == "`), + `"`, + ) + } + if strings.HasPrefix(expression, "resource.column_name == ") { + raw["column"] = strings.TrimSuffix( + strings.TrimPrefix(expression, `resource.column_name == "`), + `"`, + ) + } + if strings.HasPrefix(expression, "request.time < ") { + raw["expire_timestamp"] = strings.TrimSuffix( + strings.TrimPrefix(expression, `request.time < timestamp("`), + `")`, + ) + } + } + if instanceID == "" || databaseName == "" { + return nil, errors.Errorf("invalid exception policy condition: %v", exception.Condition.Expression) + } + raw["database"] = fmt.Sprintf("%s%s/%s%s", internal.InstanceNamePrefix, instanceID, internal.DatabaseIDPrefix, databaseName) + exceptionList = append(exceptionList, raw) + } + policy := map[string]interface{}{ + "exceptions": exceptionList, + } + return []interface{}{policy}, nil } diff --git a/provider/data_source_policy_list.go b/provider/data_source_policy_list.go index 48462f3..10bcf64 100644 --- a/provider/data_source_policy_list.go +++ b/provider/data_source_policy_list.go @@ -62,10 +62,8 @@ func dataSourcePolicyList() *schema.Resource { Computed: true, Description: "Decide if the policy is enforced.", }, - "deployment_approval_policy": getDeploymentApprovalPolicySchema(true), - "backup_plan_policy": getBackupPlanPolicySchema(true), - "sensitive_data_policy": getSensitiveDataPolicy(true), - "access_control_policy": getAccessControlPolicy(true), + "masking_policy": getMaskingPolicySchema(true), + "masking_exception_policy": getMaskingExceptionPolicySchema(true), }, }, }, @@ -76,11 +74,7 @@ func dataSourcePolicyList() *schema.Resource { func dataSourcePolicyListRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { c := m.(api.Client) - find := &api.PolicyFindMessage{ - Parent: d.Get("parent").(string), - } - - response, err := c.ListPolicies(ctx, find) + response, err := c.ListPolicies(ctx, d.Get("parent").(string)) if err != nil { return diag.FromErr(err) } @@ -89,25 +83,21 @@ func dataSourcePolicyListRead(ctx context.Context, d *schema.ResourceData, m int for _, policy := range response.Policies { raw := make(map[string]interface{}) raw["name"] = policy.Name - raw["type"] = policy.Type + raw["type"] = policy.Type.String() raw["inherit_from_parent"] = policy.InheritFromParent raw["enforce"] = policy.Enforce - if p := policy.DeploymentApprovalPolicy; p != nil { - raw["deployment_approval_policy"] = flattenDeploymentApprovalPolicy(p) + + if p := policy.GetMaskingPolicy(); p != nil { + raw["masking_policy"] = flattenMaskingPolicy(p) } - if p := policy.BackupPlanPolicy; p != nil { - backupPlan, err := flattenBackupPlanPolicy(p) + if p := policy.GetMaskingExceptionPolicy(); p != nil { + exceptionPolicy, err := flattenMaskingExceptionPolicy(p) if err != nil { return diag.FromErr(err) } - raw["backup_plan_policy"] = backupPlan - } - if p := policy.SensitiveDataPolicy; p != nil { - raw["sensitive_data_policy"] = flattenSensitiveDataPolicy(p) - } - if p := policy.AccessControlPolicy; p != nil { - raw["access_control_policy"] = flattenAccessControlPolicy(p) + raw["masking_exception_policy"] = exceptionPolicy } + policies = append(policies, raw) } diff --git a/provider/data_source_policy_list_test.go b/provider/data_source_policy_list_test.go index 0a089d9..7dfb26b 100644 --- a/provider/data_source_policy_list_test.go +++ b/provider/data_source_policy_list_test.go @@ -5,7 +5,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/bytebase/terraform-provider-bytebase/api" + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" ) @@ -27,12 +28,12 @@ func TestAccPolicyListDataSource(t *testing.T) { ), internal.GetTestStepForDataSourceList( testAccCheckPolicyResource( - "backup_plan", - "environments/test", - getBackupPlanPolicy(string(api.BackupPlanScheduleDaily), 999), - api.PolicyTypeBackupPlan, + "masking_policy", + "instances/test-sample-instance/databases/employee", + getMaskingPolicy("salary", "amount", v1pb.MaskingLevel_FULL), + v1pb.PolicyType_MASKING, ), - "bytebase_policy.backup_plan", + "bytebase_policy.masking_policy", "bytebase_policy_list", "after", "policies", diff --git a/provider/data_source_policy_test.go b/provider/data_source_policy_test.go index afa3461..06dedbb 100644 --- a/provider/data_source_policy_test.go +++ b/provider/data_source_policy_test.go @@ -7,7 +7,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/bytebase/terraform-provider-bytebase/api" + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" ) @@ -22,22 +23,24 @@ func TestAccPolicyDataSource(t *testing.T) { { Config: testAccCheckPolicyDataSource( testAccCheckPolicyResource( - "backup_plan", - "environments/test", - getBackupPlanPolicy(string(api.BackupPlanScheduleDaily), 999), - api.PolicyTypeBackupPlan, + "masking_policy", + "instances/test-sample-instance/databases/employee", + getMaskingPolicy("salary", "amount", v1pb.MaskingLevel_FULL), + v1pb.PolicyType_MASKING, ), - "backup_plan", - "environments/test", - "bytebase_policy.backup_plan", - api.PolicyTypeBackupPlan, + "masking_policy", + "instances/test-sample-instance/databases/employee", + "bytebase_policy.masking_policy", + v1pb.PolicyType_MASKING, ), Check: resource.ComposeTestCheckFunc( - internal.TestCheckResourceExists("data.bytebase_policy.backup_plan"), - resource.TestCheckResourceAttr("data.bytebase_policy.backup_plan", "type", string(api.PolicyTypeBackupPlan)), - resource.TestCheckResourceAttr("data.bytebase_policy.backup_plan", "backup_plan_policy.#", "1"), - resource.TestCheckResourceAttr("data.bytebase_policy.backup_plan", "backup_plan_policy.0.schedule", string(api.BackupPlanScheduleDaily)), - resource.TestCheckResourceAttr("data.bytebase_policy.backup_plan", "backup_plan_policy.0.retention_duration", "999"), + internal.TestCheckResourceExists("data.bytebase_policy.masking_policy"), + resource.TestCheckResourceAttr("data.bytebase_policy.masking_policy", "type", v1pb.PolicyType_MASKING.String()), + resource.TestCheckResourceAttr("data.bytebase_policy.masking_policy", "masking_policy.#", "1"), + resource.TestCheckResourceAttr("data.bytebase_policy.masking_policy", "masking_policy.0.mask_data.#", "1"), + resource.TestCheckResourceAttr("data.bytebase_policy.masking_policy", "masking_policy.0.mask_data.0.table", "salary"), + resource.TestCheckResourceAttr("data.bytebase_policy.masking_policy", "masking_policy.0.mask_data.0.column", "amount"), + resource.TestCheckResourceAttr("data.bytebase_policy.masking_policy", "masking_policy.0.mask_data.0.masking_level", v1pb.MaskingLevel_FULL.String()), ), }, }, @@ -56,11 +59,11 @@ func TestAccPolicyDataSource_NotFound(t *testing.T) { Config: testAccCheckPolicyDataSource( "", "policy", - "environments/test", + "instances/test-sample-instance/databases/employee", "", - api.PolicyTypeDeploymentApproval, + v1pb.PolicyType_MASKING, ), - ExpectError: regexp.MustCompile("Cannot found policy environments/test/policies/DEPLOYMENT_APPROVAL"), + ExpectError: regexp.MustCompile("Cannot found policy instances/test-sample-instance/databases/employee/policies/MASKING"), }, }, }) @@ -71,7 +74,7 @@ func testAccCheckPolicyDataSource( identifier, parent, dependsOn string, - pType api.PolicyType) string { + pType v1pb.PolicyType) string { return fmt.Sprintf(` %s @@ -82,5 +85,5 @@ func testAccCheckPolicyDataSource( %s ] } - `, resource, identifier, parent, pType, dependsOn) + `, resource, identifier, parent, pType.String(), dependsOn) } diff --git a/provider/data_source_project.go b/provider/data_source_project.go index 7a992b7..5a4f484 100644 --- a/provider/data_source_project.go +++ b/provider/data_source_project.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "time" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -138,7 +139,7 @@ func setProjectWithDatabases(d *schema.ResourceData, project *v1pb.Project, data db["name"] = database.Name db["environment"] = database.Environment db["sync_state"] = database.SyncState.String() - db["successful_sync_time"] = database.SuccessfulSyncTime + db["successful_sync_time"] = database.SuccessfulSyncTime.AsTime().UTC().Format(time.RFC3339) db["schema_version"] = database.SchemaVersion db["labels"] = database.Labels dbList = append(dbList, db) diff --git a/provider/data_source_project_list.go b/provider/data_source_project_list.go index 025d165..6a7c5de 100644 --- a/provider/data_source_project_list.go +++ b/provider/data_source_project_list.go @@ -138,7 +138,7 @@ func dataSourceProjectListRead(ctx context.Context, d *schema.ResourceData, m in db["name"] = database.Name db["environment"] = database.Environment db["sync_state"] = database.SyncState.String() - db["successful_sync_time"] = database.SuccessfulSyncTime + db["successful_sync_time"] = database.SuccessfulSyncTime.AsTime().UTC().Format(time.RFC3339) db["schema_version"] = database.SchemaVersion db["labels"] = database.Labels dbList = append(dbList, db) diff --git a/provider/data_source_setting.go b/provider/data_source_setting.go index 6f16844..5b6a78a 100644 --- a/provider/data_source_setting.go +++ b/provider/data_source_setting.go @@ -27,9 +27,51 @@ func dataSourceSetting() *schema.Resource { Required: true, ValidateFunc: validation.StringInSlice([]string{ string(api.SettingWorkspaceApproval), + string(api.SettingWorkspaceExternalApproval), }, false), }, - "approval_flow": getWorkspaceApprovalSetting(true), + "approval_flow": getWorkspaceApprovalSetting(true), + "external_approval_nodes": getExternalApprovalSetting(true), + }, + } +} + +func getExternalApprovalSetting(computed bool) *schema.Schema { + return &schema.Schema{ + Computed: computed, + Optional: true, + Default: nil, + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "nodes": { + Type: schema.TypeList, + Computed: computed, + Required: !computed, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: computed, + Required: !computed, + Description: "The unique external node id.", + }, + "title": { + Type: schema.TypeString, + Computed: computed, + Required: !computed, + Description: "The external node title.", + }, + "endpoint": { + Type: schema.TypeString, + Computed: computed, + Required: !computed, + Description: "The endpoint URL to receive the approval message. Learn more: https://www.bytebase.com/docs/api/external-approval", + }, + }, + }, + }, + }, }, } } @@ -45,19 +87,19 @@ func getWorkspaceApprovalSetting(computed bool) *schema.Schema { "rules": { Type: schema.TypeList, Computed: computed, - Optional: true, + Required: !computed, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "flow": { Computed: computed, - Optional: true, + Required: !computed, Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "title": { Type: schema.TypeString, Computed: computed, - Optional: true, + Required: !computed, }, "description": { Type: schema.TypeString, @@ -65,14 +107,16 @@ func getWorkspaceApprovalSetting(computed bool) *schema.Schema { Optional: true, }, "creator": { - Type: schema.TypeString, - Computed: computed, - Optional: true, + Type: schema.TypeString, + Computed: computed, + Required: !computed, + Description: "The creator name in users/{email} format", }, "steps": { - Type: schema.TypeList, - Computed: computed, - Optional: true, + Type: schema.TypeList, + Computed: computed, + Required: !computed, + Description: "Approval flow following the step order.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "type": { @@ -86,11 +130,10 @@ func getWorkspaceApprovalSetting(computed bool) *schema.Schema { }, false), }, "node": { - Optional: true, + Required: !computed, Default: nil, Computed: computed, Type: schema.TypeString, - // TODO(ed): consider add validate }, }, }, @@ -99,9 +142,10 @@ func getWorkspaceApprovalSetting(computed bool) *schema.Schema { }, }, "conditions": { - Computed: computed, - Type: schema.TypeList, - Optional: true, + Computed: computed, + Type: schema.TypeList, + Optional: true, + Description: "Match any condition will trigger this approval flow.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "source": { @@ -162,6 +206,12 @@ func setSettingMessage(ctx context.Context, d *schema.ResourceData, client api.C return diag.Errorf("cannot set workspace_approval_setting: %s", err.Error()) } } + if value := setting.Value.GetExternalApprovalSettingValue(); value != nil { + settingVal := flattenExternalApprovalSetting(value) + if err := d.Set("external_approval_nodes", settingVal); err != nil { + return diag.Errorf("cannot set external_approval_nodes: %s", err.Error()) + } + } return nil } @@ -285,3 +335,19 @@ func flattenWorkspaceApprovalSetting(ctx context.Context, client api.Client, set } return []interface{}{approvalSetting}, nil } + +func flattenExternalApprovalSetting(setting *v1pb.ExternalApprovalSetting) []interface{} { + nodeList := []interface{}{} + for _, node := range setting.Nodes { + rawNode := map[string]interface{}{} + rawNode["id"] = node.Id + rawNode["title"] = node.Title + rawNode["endpoint"] = node.Endpoint + nodeList = append(nodeList, rawNode) + } + + approvalSetting := map[string]interface{}{ + "nodes": nodeList, + } + return []interface{}{approvalSetting} +} diff --git a/provider/internal/mock_client.go b/provider/internal/mock_client.go index e31f289..4cf78d3 100644 --- a/provider/internal/mock_client.go +++ b/provider/internal/mock_client.go @@ -16,7 +16,7 @@ import ( var environmentMap map[string]*v1pb.Environment var instanceMap map[string]*v1pb.Instance -var policyMap map[string]*api.PolicyMessage +var policyMap map[string]*v1pb.Policy var projectMap map[string]*v1pb.Project var databaseMap map[string]*v1pb.Database var settingMap map[string]*v1pb.Setting @@ -24,7 +24,7 @@ var settingMap map[string]*v1pb.Setting func init() { environmentMap = map[string]*v1pb.Environment{} instanceMap = map[string]*v1pb.Instance{} - policyMap = map[string]*api.PolicyMessage{} + policyMap = map[string]*v1pb.Policy{} projectMap = map[string]*v1pb.Project{} databaseMap = map[string]*v1pb.Database{} settingMap = map[string]*v1pb.Setting{} @@ -33,7 +33,7 @@ func init() { type mockClient struct { environmentMap map[string]*v1pb.Environment instanceMap map[string]*v1pb.Instance - policyMap map[string]*api.PolicyMessage + policyMap map[string]*v1pb.Policy projectMap map[string]*v1pb.Project databaseMap map[string]*v1pb.Database settingMap map[string]*v1pb.Setting @@ -254,21 +254,21 @@ func (*mockClient) SyncInstanceSchema(_ context.Context, _ string) error { } // ListPolicies lists policies in a specific resource. -func (c *mockClient) ListPolicies(_ context.Context, find *api.PolicyFindMessage) (*api.ListPolicyMessage, error) { - policies := make([]*api.PolicyMessage, 0) +func (c *mockClient) ListPolicies(_ context.Context, parent string) (*v1pb.ListPoliciesResponse, error) { + policies := make([]*v1pb.Policy, 0) for _, policy := range c.policyMap { - if find.Parent == "" || strings.HasPrefix(policy.Name, find.Parent) { + if parent == "" || strings.HasPrefix(policy.Name, parent) { policies = append(policies, policy) } } - return &api.ListPolicyMessage{ + return &v1pb.ListPoliciesResponse{ Policies: policies, }, nil } // GetPolicy gets a policy in a specific resource. -func (c *mockClient) GetPolicy(_ context.Context, policyName string) (*api.PolicyMessage, error) { +func (c *mockClient) GetPolicy(_ context.Context, policyName string) (*v1pb.Policy, error) { policy, ok := c.policyMap[policyName] if !ok { return nil, errors.Errorf("Cannot found policy %s", policyName) @@ -278,7 +278,7 @@ func (c *mockClient) GetPolicy(_ context.Context, policyName string) (*api.Polic } // UpsertPolicy creates or updates the policy. -func (c *mockClient) UpsertPolicy(_ context.Context, patch *api.PolicyPatchMessage) (*api.PolicyMessage, error) { +func (c *mockClient) UpsertPolicy(_ context.Context, patch *v1pb.Policy, updateMasks []string) (*v1pb.Policy, error) { _, policyType, err := GetPolicyParentAndType(patch.Name) if err != nil { return nil, err @@ -287,7 +287,7 @@ func (c *mockClient) UpsertPolicy(_ context.Context, patch *api.PolicyPatchMessa policy, existed := c.policyMap[patch.Name] if !existed { - policy = &api.PolicyMessage{ + policy = &v1pb.Policy{ Name: patch.Name, Type: policyType, Enforce: true, @@ -295,48 +295,37 @@ func (c *mockClient) UpsertPolicy(_ context.Context, patch *api.PolicyPatchMessa } switch policyType { - case api.PolicyTypeAccessControl: + case v1pb.PolicyType_MASKING: if !existed { - if patch.AccessControlPolicy == nil { + if patch.GetMaskingPolicy() == nil { return nil, errors.Errorf("payload is required to create the policy") } } - if v := patch.AccessControlPolicy; v != nil { - policy.AccessControlPolicy = v - } - case api.PolicyTypeBackupPlan: - if !existed { - if patch.BackupPlanPolicy == nil { - return nil, errors.Errorf("payload is required to create the policy") + if v := patch.GetMaskingPolicy(); v != nil { + policy.Policy = &v1pb.Policy_MaskingPolicy{ + MaskingPolicy: v, } } - if v := patch.BackupPlanPolicy; v != nil { - policy.BackupPlanPolicy = v - } - case api.PolicyTypeDeploymentApproval: + case v1pb.PolicyType_MASKING_EXCEPTION: if !existed { - if patch.DeploymentApprovalPolicy == nil { + if patch.GetMaskingExceptionPolicy() == nil { return nil, errors.Errorf("payload is required to create the policy") } } - if v := patch.DeploymentApprovalPolicy; v != nil { - policy.DeploymentApprovalPolicy = v - } - case api.PolicyTypeSensitiveData: - if !existed { - if patch.SensitiveDataPolicy == nil { - return nil, errors.Errorf("payload is required to create the policy") + if v := patch.GetMaskingExceptionPolicy(); v != nil { + policy.Policy = &v1pb.Policy_MaskingExceptionPolicy{ + MaskingExceptionPolicy: v, } } - if v := patch.SensitiveDataPolicy; v != nil { - policy.SensitiveDataPolicy = v - } default: return nil, errors.Errorf("invalid policy type %v", policyType) } - if v := patch.InheritFromParent; v != nil { - policy.InheritFromParent = *v + if slices.Contains(updateMasks, "inherit_from_parent") { + policy.InheritFromParent = patch.InheritFromParent + } + if slices.Contains(updateMasks, "enforce") { + policy.Enforce = patch.Enforce } c.policyMap[policy.Name] = policy diff --git a/provider/internal/utils.go b/provider/internal/utils.go index a4068cd..8a8a435 100644 --- a/provider/internal/utils.go +++ b/provider/internal/utils.go @@ -5,13 +5,12 @@ import ( "regexp" "strings" + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" "github.com/hashicorp/go-cty/cty" "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/pkg/errors" - - "github.com/bytebase/terraform-provider-bytebase/api" ) const ( @@ -69,14 +68,18 @@ func ResourceNameValidation(regexs ...*regexp.Regexp) schema.SchemaValidateDiagF } // GetPolicyParentAndType returns the policy parent and type by the name. -func GetPolicyParentAndType(name string) (string, api.PolicyType, error) { +func GetPolicyParentAndType(name string) (string, v1pb.PolicyType, error) { names := strings.Split(name, PolicyNamePrefix) if len(names) != 2 { - return "", "", errors.Errorf("invalid policy name %s", name) + return "", v1pb.PolicyType_POLICY_TYPE_UNSPECIFIED, errors.Errorf("invalid policy name %s", name) + } + policyType := strings.ToUpper(names[1]) + pType, ok := v1pb.PolicyType_value[policyType] + if !ok { + return "", v1pb.PolicyType_POLICY_TYPE_UNSPECIFIED, errors.Errorf("invalid policy name %s", name) } - policyType := api.PolicyType(strings.ToUpper(names[1])) - return strings.TrimSuffix(names[0], "/"), policyType, nil + return strings.TrimSuffix(names[0], "/"), v1pb.PolicyType(pType), nil } // GetEnvironmentID will parse the environment resource id. diff --git a/provider/resource_policy.go b/provider/resource_policy.go index 992bc3d..18ccffa 100644 --- a/provider/resource_policy.go +++ b/provider/resource_policy.go @@ -10,6 +10,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/pkg/errors" + "google.golang.org/genproto/googleapis/type/expr" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -39,7 +42,7 @@ func resourcePolicy() *schema.Resource { // project policy regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern)), // database policy - regexp.MustCompile(fmt.Sprintf("^%s%s%s%s$", internal.InstanceNamePrefix, internal.ResourceIDPattern, internal.DatabaseIDPrefix, internal.ResourceIDPattern)), + regexp.MustCompile(fmt.Sprintf("^%s%s/%s%s$", internal.InstanceNamePrefix, internal.ResourceIDPattern, internal.DatabaseIDPrefix, internal.ResourceIDPattern)), ), Description: "The policy parent name for the policy, support projects/{resource id}, environments/{resource id}, instances/{resource id}, or instances/{resource id}/databases/{database name}", }, @@ -47,10 +50,8 @@ func resourcePolicy() *schema.Resource { Type: schema.TypeString, Required: true, ValidateFunc: validation.StringInSlice([]string{ - string(api.PolicyTypeDeploymentApproval), - string(api.PolicyTypeBackupPlan), - string(api.PolicyTypeSensitiveData), - string(api.PolicyTypeAccessControl), + v1pb.PolicyType_MASKING.String(), + v1pb.PolicyType_MASKING_EXCEPTION.String(), }, false), Description: "The policy type.", }, @@ -71,10 +72,8 @@ func resourcePolicy() *schema.Resource { Default: false, Description: "Decide if the policy should inherit from the parent.", }, - "deployment_approval_policy": getDeploymentApprovalPolicySchema(false), - "backup_plan_policy": getBackupPlanPolicySchema(false), - "sensitive_data_policy": getSensitiveDataPolicy(false), - "access_control_policy": getAccessControlPolicy(false), + "masking_policy": getMaskingPolicySchema(false), + "masking_exception_policy": getMaskingExceptionPolicySchema(false), }, } } @@ -113,52 +112,39 @@ func resourcePolicyCreate(ctx context.Context, d *schema.ResourceData, m interfa inheritFromParent := d.Get("inherit_from_parent").(bool) enforce := d.Get("enforce").(bool) - patch := &api.PolicyPatchMessage{ + _, policyType, err := internal.GetPolicyParentAndType(policyName) + if err != nil { + return diag.FromErr(err) + } + + patch := &v1pb.Policy{ Name: policyName, - InheritFromParent: &inheritFromParent, - Enforce: &enforce, + InheritFromParent: inheritFromParent, + Enforce: enforce, + Type: policyType, } - policyType := api.PolicyType(strings.ToUpper(d.Get("type").(string))) switch policyType { - case api.PolicyTypeDeploymentApproval: - if _, ok := d.GetOk("deployment_approval_policy"); ok { - policy, err := convertDeploymentApprovalPolicy(d) - if err != nil { - return diag.FromErr(err) - } - patch.DeploymentApprovalPolicy = policy - } - case api.PolicyTypeBackupPlan: - if _, ok := d.GetOk("backup_plan_policy"); ok { - policy, err := convertBackupPlanPolicy(d) - if err != nil { - return diag.FromErr(err) - } - patch.BackupPlanPolicy = policy + case v1pb.PolicyType_MASKING: + maskingPolicy, err := convertToMaskingPolicy(d) + if err != nil { + return diag.FromErr(err) } - case api.PolicyTypeSensitiveData: - if _, ok := d.GetOk("sensitive_data_policy"); ok { - policy, err := convertSensitiveDataPolicy(d) - if err != nil { - return diag.FromErr(err) - } - patch.SensitiveDataPolicy = policy + patch.Policy = &v1pb.Policy_MaskingPolicy{ + MaskingPolicy: maskingPolicy, } - case api.PolicyTypeAccessControl: - policy, err := convertAccessControlPolicy(d) + case v1pb.PolicyType_MASKING_EXCEPTION: + maskingExceptionPolicy, err := convertToMaskingExceptionPolicy(d) if err != nil { return diag.FromErr(err) } - patch.AccessControlPolicy = policy - } - - if err := validatePolicy(policyType, patch); err != nil { - return diag.FromErr(err) + patch.Policy = &v1pb.Policy_MaskingExceptionPolicy{ + MaskingExceptionPolicy: maskingExceptionPolicy, + } } var diags diag.Diagnostics - p, err := c.UpsertPolicy(ctx, patch) + p, err := c.UpsertPolicy(ctx, patch, []string{"inherit_from_parent", "enforce", "payload"}) if err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, @@ -181,56 +167,49 @@ func resourcePolicyCreate(ctx context.Context, d *schema.ResourceData, m interfa func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { c := m.(api.Client) policyName := d.Id() + _, policyType, err := internal.GetPolicyParentAndType(policyName) + if err != nil { + return diag.FromErr(err) + } - patch := &api.PolicyPatchMessage{ - Name: policyName, + patch := &v1pb.Policy{ + Name: policyName, + InheritFromParent: d.Get("inherit_from_parent").(bool), + Enforce: d.Get("enforce").(bool), + Type: policyType, } + updateMasks := []string{} if d.HasChange("inherit_from_parent") { - v := d.Get("inherit_from_parent").(bool) - patch.InheritFromParent = &v + updateMasks = append(updateMasks, "inherit_from_parent") } if d.HasChange("enforce") { - v := d.Get("enforce").(bool) - patch.Enforce = &v + updateMasks = append(updateMasks, "enforce") } - if d.HasChange("deployment_approval_policy") { - policy, err := convertDeploymentApprovalPolicy(d) + if d.HasChange("masking_policy") { + updateMasks = append(updateMasks, "payload") + maskingPolicy, err := convertToMaskingPolicy(d) if err != nil { return diag.FromErr(err) } - patch.DeploymentApprovalPolicy = policy - } - if d.HasChange("backup_plan_policy") { - policy, err := convertBackupPlanPolicy(d) - if err != nil { - return diag.FromErr(err) + patch.Policy = &v1pb.Policy_MaskingPolicy{ + MaskingPolicy: maskingPolicy, } - patch.BackupPlanPolicy = policy } - if d.HasChange("sensitive_data_policy") { - policy, err := convertSensitiveDataPolicy(d) + if d.HasChange("masking_exception_policy") { + updateMasks = append(updateMasks, "payload") + maskingExceptionPolicy, err := convertToMaskingExceptionPolicy(d) if err != nil { return diag.FromErr(err) } - patch.SensitiveDataPolicy = policy - } - if d.HasChange("access_control_policy") { - policy, err := convertAccessControlPolicy(d) - if err != nil { - return diag.FromErr(err) + patch.Policy = &v1pb.Policy_MaskingExceptionPolicy{ + MaskingExceptionPolicy: maskingExceptionPolicy, } - patch.AccessControlPolicy = policy - } - - policyType := api.PolicyType(strings.ToUpper(d.Get("type").(string))) - if err := validatePolicy(policyType, patch); err != nil { - return diag.FromErr(err) } var diags diag.Diagnostics - if _, err := c.UpsertPolicy(ctx, patch); err != nil { + if _, err := c.UpsertPolicy(ctx, patch, updateMasks); err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Failed to upsert policy", @@ -247,119 +226,86 @@ func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interfa return diags } -func convertDeploymentApprovalPolicy(d *schema.ResourceData) (*api.DeploymentApprovalPolicy, error) { - rawList, ok := d.Get("deployment_approval_policy").([]interface{}) - if !ok || len(rawList) != 1 { - return nil, errors.Errorf("invalid deployment_approval_policy") - } - - raw := rawList[0].(map[string]interface{}) - strategies := raw["deployment_approval_strategies"].([]interface{}) - - policy := &api.DeploymentApprovalPolicy{ - DefaultStrategy: api.ApprovalStrategy(raw["default_strategy"].(string)), - } - - for _, strategy := range strategies { - rawStrategy := strategy.(map[string]interface{}) - policy.DeploymentApprovalStrategies = append(policy.DeploymentApprovalStrategies, &api.DeploymentApprovalStrategy{ - ApprovalGroup: api.ApprovalGroup(rawStrategy["approval_group"].(string)), - ApprovalStrategy: api.ApprovalStrategy(rawStrategy["approval_strategy"].(string)), - DeploymentType: api.DeploymentType(rawStrategy["deployment_type"].(string)), - }) - } - return policy, nil +func convertToMaskingLevel(level string) v1pb.MaskingLevel { + return v1pb.MaskingLevel(v1pb.MaskingLevel_value[level]) } -func convertBackupPlanPolicy(d *schema.ResourceData) (*api.BackupPlanPolicy, error) { - rawList, ok := d.Get("backup_plan_policy").([]interface{}) +func convertToMaskingExceptionPolicy(d *schema.ResourceData) (*v1pb.MaskingExceptionPolicy, error) { + rawList, ok := d.Get("masking_exception_policy").([]interface{}) if !ok || len(rawList) != 1 { - return nil, errors.Errorf("invalid backup_plan_policy") + return nil, errors.Errorf("invalid masking_exception_policy") } raw := rawList[0].(map[string]interface{}) - return &api.BackupPlanPolicy{ - Schedule: api.BackupPlanSchedule(raw["schedule"].(string)), - RetentionDuration: fmt.Sprintf("%ds", raw["retention_duration"].(int)), - }, nil -} + exceptionList := raw["exceptions"].([]interface{}) -func convertSensitiveDataPolicy(d *schema.ResourceData) (*api.SensitiveDataPolicy, error) { - rawList, ok := d.Get("sensitive_data_policy").([]interface{}) - if !ok || len(rawList) != 1 { - return nil, errors.Errorf("invalid sensitive_data_policy") - } + policy := &v1pb.MaskingExceptionPolicy{} - raw := rawList[0].(map[string]interface{}) - dataList := raw["sensitive_data"].([]interface{}) - policy := &api.SensitiveDataPolicy{} - - for _, data := range dataList { - rawData := data.(map[string]interface{}) - policy.SensitiveData = append(policy.SensitiveData, &api.SensitiveData{ - Schema: rawData["schema"].(string), - Table: rawData["table"].(string), - Column: rawData["column"].(string), - MaskType: api.SensitiveDataMaskType(rawData["mask_type"].(string)), + for _, exception := range exceptionList { + rawException := exception.(map[string]interface{}) + + databaseFullName := rawException["database"].(string) + instanceID, databaseName, err := internal.GetInstanceDatabaseID(databaseFullName) + if err != nil { + return nil, errors.Wrapf(err, "invalid database full name: %v", databaseFullName) + } + + expressions := []string{ + fmt.Sprintf(`resource.instance_id == "%s"`, instanceID), + fmt.Sprintf(`resource.database_name == "%s"`, databaseName), + } + if schema, ok := rawException["schema"].(string); ok && schema != "" { + expressions = append(expressions, fmt.Sprintf(`resource.schema_name == "%s"`, schema)) + } + if table, ok := rawException["table"].(string); ok && table != "" { + expressions = append(expressions, fmt.Sprintf(`resource.table_name == "%s"`, table)) + } + if column, ok := rawException["column"].(string); ok && column != "" { + expressions = append(expressions, fmt.Sprintf(`resource.column_name == "%s"`, column)) + } + if expire, ok := rawException["expire_timestamp"].(string); ok && expire != "" { + expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, expire)) + } + member := rawException["member"].(string) + if !strings.HasPrefix(member, "user:") { + return nil, errors.Errorf("member should in user:{email} format") + } + policy.MaskingExceptions = append(policy.MaskingExceptions, &v1pb.MaskingExceptionPolicy_MaskingException{ + Member: rawException["member"].(string), + Action: v1pb.MaskingExceptionPolicy_MaskingException_Action( + v1pb.MaskingExceptionPolicy_MaskingException_Action_value[rawException["action"].(string)], + ), + MaskingLevel: convertToMaskingLevel(rawException["masking_level"].(string)), + Condition: &expr.Expr{ + Expression: strings.Join(expressions, " && "), + }, }) } - return policy, nil } -func convertAccessControlPolicy(d *schema.ResourceData) (*api.AccessControlPolicy, error) { - rawList, ok := d.Get("access_control_policy").([]interface{}) - if !ok || len(rawList) > 1 { - return nil, errors.Errorf("invalid access_control_policy") - } - - if len(rawList) == 0 { - if _, ok := d.GetOk("database"); !ok { - return nil, errors.Errorf("access_control_policy is required") - } - - return &api.AccessControlPolicy{ - DisallowRules: []*api.AccessControlRule{ - { - FullDatabase: false, - }, - }, - }, nil +func convertToMaskingPolicy(d *schema.ResourceData) (*v1pb.MaskingPolicy, error) { + rawList, ok := d.Get("masking_policy").([]interface{}) + if !ok || len(rawList) != 1 { + return nil, errors.Errorf("invalid masking_policy") } raw := rawList[0].(map[string]interface{}) - rules := raw["disallow_rules"].([]interface{}) - policy := &api.AccessControlPolicy{} - - for _, rule := range rules { - rawRule := rule.(map[string]interface{}) - policy.DisallowRules = append(policy.DisallowRules, &api.AccessControlRule{ - FullDatabase: rawRule["all_databases"].(bool), + rawMaskList := raw["mask_data"].([]interface{}) + + policy := &v1pb.MaskingPolicy{} + + for _, maskData := range rawMaskList { + rawMask := maskData.(map[string]interface{}) + policy.MaskData = append(policy.MaskData, &v1pb.MaskData{ + Schema: rawMask["schema"].(string), + Table: rawMask["table"].(string), + Column: rawMask["column"].(string), + FullMaskingAlgorithmId: rawMask["full_masking_algorithm_id"].(string), + PartialMaskingAlgorithmId: rawMask["partial_masking_algorithm_id"].(string), + MaskingLevel: convertToMaskingLevel(rawMask["masking_level"].(string)), }) } return policy, nil } - -func validatePolicy(policyType api.PolicyType, patch *api.PolicyPatchMessage) error { - switch policyType { - case api.PolicyTypeDeploymentApproval: - if patch.DeploymentApprovalPolicy == nil { - return errors.Errorf("must set deployment_approval_policy for %v policy", policyType) - } - case api.PolicyTypeBackupPlan: - if patch.BackupPlanPolicy == nil { - return errors.Errorf("must set backup_plan_policy for %v policy", policyType) - } - case api.PolicyTypeSensitiveData: - if patch.SensitiveDataPolicy == nil { - return errors.Errorf("must set sensitive_data_policy for %v policy", policyType) - } - case api.PolicyTypeAccessControl: - if patch.AccessControlPolicy == nil { - return errors.Errorf("must set access_control_policy for %v policy", policyType) - } - } - - return nil -} diff --git a/provider/resource_policy_test.go b/provider/resource_policy_test.go index b2aa4d2..e78d951 100644 --- a/provider/resource_policy_test.go +++ b/provider/resource_policy_test.go @@ -3,10 +3,9 @@ package provider import ( "context" "fmt" - "regexp" - "strings" "testing" + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/pkg/errors" @@ -25,108 +24,43 @@ func TestAccPolicy(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccCheckPolicyResource( - "backup_plan", - "environments/test", - getBackupPlanPolicy(string(api.BackupPlanScheduleDaily), 999), - api.PolicyTypeBackupPlan, + "masking_policy", + "instances/test-sample-instance/databases/employee", + getMaskingPolicy("salary", "amount", v1pb.MaskingLevel_FULL), + v1pb.PolicyType_MASKING, ), Check: resource.ComposeTestCheckFunc( - internal.TestCheckResourceExists("bytebase_policy.backup_plan"), - resource.TestCheckResourceAttr("bytebase_policy.backup_plan", "type", string(api.PolicyTypeBackupPlan)), - resource.TestCheckResourceAttr("bytebase_policy.backup_plan", "backup_plan_policy.#", "1"), - resource.TestCheckResourceAttr("bytebase_policy.backup_plan", "backup_plan_policy.0.schedule", string(api.BackupPlanScheduleDaily)), - resource.TestCheckResourceAttr("bytebase_policy.backup_plan", "backup_plan_policy.0.retention_duration", "999"), + internal.TestCheckResourceExists("bytebase_policy.masking_policy"), + resource.TestCheckResourceAttr("bytebase_policy.masking_policy", "type", v1pb.PolicyType_MASKING.String()), + resource.TestCheckResourceAttr("bytebase_policy.masking_policy", "masking_policy.#", "1"), + resource.TestCheckResourceAttr("bytebase_policy.masking_policy", "masking_policy.0.mask_data.#", "1"), + resource.TestCheckResourceAttr("bytebase_policy.masking_policy", "masking_policy.0.mask_data.0.table", "salary"), + resource.TestCheckResourceAttr("bytebase_policy.masking_policy", "masking_policy.0.mask_data.0.column", "amount"), + resource.TestCheckResourceAttr("bytebase_policy.masking_policy", "masking_policy.0.mask_data.0.masking_level", v1pb.MaskingLevel_FULL.String()), ), }, { Config: testAccCheckPolicyResource( - "backup_plan", - "environments/test", - getBackupPlanPolicy(string(api.BackupPlanScheduleWeekly), 99), - api.PolicyTypeBackupPlan, + "masking_exception_policy", + "projects/project-sample", + getMaskingExceptionPolicy("instances/test-sample-instance/databases/employee", "salary", "amount", v1pb.MaskingLevel_PARTIAL), + v1pb.PolicyType_MASKING_EXCEPTION, ), Check: resource.ComposeTestCheckFunc( - internal.TestCheckResourceExists("bytebase_policy.backup_plan"), - resource.TestCheckResourceAttr("bytebase_policy.backup_plan", "type", string(api.PolicyTypeBackupPlan)), - resource.TestCheckResourceAttr("bytebase_policy.backup_plan", "backup_plan_policy.#", "1"), - resource.TestCheckResourceAttr("bytebase_policy.backup_plan", "backup_plan_policy.0.schedule", string(api.BackupPlanScheduleWeekly)), - resource.TestCheckResourceAttr("bytebase_policy.backup_plan", "backup_plan_policy.0.retention_duration", "99"), - ), - }, - { - Config: testAccCheckPolicyResource( - "deployment_approval", - "environments/test", - getDeploymentApprovalPolicy(string(api.ApprovalStrategyAutomatic), []*api.DeploymentApprovalStrategy{ - { - ApprovalGroup: api.ApprovalGroupDBA, - ApprovalStrategy: api.ApprovalStrategyAutomatic, - DeploymentType: api.DeploymentTypeDatabaseCreate, - }, - { - ApprovalGroup: api.ApprovalGroupOwner, - ApprovalStrategy: api.ApprovalStrategyAutomatic, - DeploymentType: api.DeploymentTypeDatabaseDDL, - }, - }), - api.PolicyTypeDeploymentApproval, - ), - Check: resource.ComposeTestCheckFunc( - internal.TestCheckResourceExists("bytebase_policy.deployment_approval"), - resource.TestCheckResourceAttr("bytebase_policy.deployment_approval", "type", string(api.PolicyTypeDeploymentApproval)), - resource.TestCheckResourceAttr("bytebase_policy.deployment_approval", "deployment_approval_policy.#", "1"), - resource.TestCheckResourceAttr("bytebase_policy.deployment_approval", "deployment_approval_policy.0.default_strategy", string(api.ApprovalStrategyAutomatic)), - resource.TestCheckResourceAttr("bytebase_policy.deployment_approval", "deployment_approval_policy.0.deployment_approval_strategies.#", "2"), - resource.TestCheckResourceAttr("bytebase_policy.deployment_approval", "deployment_approval_policy.0.deployment_approval_strategies.0.deployment_type", string(api.DeploymentTypeDatabaseCreate)), - resource.TestCheckResourceAttr("bytebase_policy.deployment_approval", "deployment_approval_policy.0.deployment_approval_strategies.1.deployment_type", string(api.DeploymentTypeDatabaseDDL)), + internal.TestCheckResourceExists("bytebase_policy.masking_exception_policy"), + resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "type", v1pb.PolicyType_MASKING_EXCEPTION.String()), + resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "masking_exception_policy.#", "1"), + resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.#", "1"), + resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.table", "salary"), + resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.column", "amount"), + resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.masking_level", v1pb.MaskingLevel_PARTIAL.String()), ), }, }, }) } -func TestAccPolicy_InvalidInput(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { - testAccPreCheck(t) - }, - Providers: testAccProviders, - CheckDestroy: testAccCheckPolicyDestroy, - Steps: []resource.TestStep{ - { - Config: testAccCheckPolicyResource( - "backup_plan", - "environments/test", - getBackupPlanPolicy("daily", 999), - api.PolicyTypeBackupPlan, - ), - ExpectError: regexp.MustCompile("expected backup_plan_policy.0.schedule to be one of"), - }, - { - Config: testAccCheckPolicyResource( - "deployment_approval", - "environments/test", - getDeploymentApprovalPolicy("unknown", []*api.DeploymentApprovalStrategy{ - { - ApprovalGroup: api.ApprovalGroupDBA, - ApprovalStrategy: api.ApprovalStrategyAutomatic, - DeploymentType: api.DeploymentTypeDatabaseCreate, - }, - { - ApprovalGroup: api.ApprovalGroupOwner, - ApprovalStrategy: api.ApprovalStrategyAutomatic, - DeploymentType: api.DeploymentTypeDatabaseDDL, - }, - }), - api.PolicyTypeDeploymentApproval, - ), - ExpectError: regexp.MustCompile("expected deployment_approval_policy.0.default_strategy to be one of"), - }, - }, - }) -} - -func testAccCheckPolicyResource(identifier, parent, payload string, pType api.PolicyType) string { +func testAccCheckPolicyResource(identifier, parent, payload string, pType v1pb.PolicyType) string { return fmt.Sprintf(` resource "bytebase_policy" "%s" { parent = "%s" @@ -134,36 +68,34 @@ func testAccCheckPolicyResource(identifier, parent, payload string, pType api.Po %s } - `, identifier, parent, pType, payload) + `, identifier, parent, pType.String(), payload) } -func getBackupPlanPolicy(schedule string, duration int) string { +func getMaskingPolicy(table, column string, level v1pb.MaskingLevel) string { return fmt.Sprintf(` - backup_plan_policy { - schedule = "%s" - retention_duration = %d - } - `, schedule, duration) -} - -func getDeploymentApprovalPolicy(defaultStrategy string, strategies []*api.DeploymentApprovalStrategy) string { - approvalStrategies := []string{} - for _, strategy := range strategies { - approvalStrategies = append(approvalStrategies, fmt.Sprintf(` - deployment_approval_strategies { - approval_group = "%s" - approval_strategy = "%s" - deployment_type = "%s" + masking_policy { + mask_data { + table = "%s" + column = "%s" + masking_level = "%s" } - `, strategy.ApprovalGroup, strategy.ApprovalStrategy, strategy.DeploymentType)) } + `, table, column, level.String()) +} +func getMaskingExceptionPolicy(database, table, column string, level v1pb.MaskingLevel) string { return fmt.Sprintf(` - deployment_approval_policy { - default_strategy = "%s" - %s + masking_exception_policy { + exceptions { + database = "%s" + table = "%s" + column = "%s" + masking_level = "%s" + member = "user:ed@bytebase.com" + action = "QUERY" + } } - `, defaultStrategy, strings.Join(approvalStrategies, "\n")) + `, database, table, column, level.String()) } func testAccCheckPolicyDestroy(s *terraform.State) error { diff --git a/provider/resource_setting.go b/provider/resource_setting.go index 8bf890e..7fd5ffa 100644 --- a/provider/resource_setting.go +++ b/provider/resource_setting.go @@ -33,9 +33,11 @@ func resourceSetting() *schema.Resource { Required: true, ValidateFunc: validation.StringInSlice([]string{ string(api.SettingWorkspaceApproval), + string(api.SettingWorkspaceExternalApproval), }, false), }, - "approval_flow": getWorkspaceApprovalSetting(false), + "approval_flow": getWorkspaceApprovalSetting(false), + "external_approval_nodes": getExternalApprovalSetting(false), }, } } @@ -62,6 +64,16 @@ func resourceSettingUpsert(ctx context.Context, d *schema.ResourceData, m interf WorkspaceApprovalSettingValue: workspaceApproval, }, } + case api.SettingWorkspaceExternalApproval: + externalApproval, err := convertToV1ExternalNodesSetting(d) + if err != nil { + return diag.FromErr(err) + } + setting.Value = &v1pb.Value{ + Value: &v1pb.Value_ExternalApprovalSettingValue{ + ExternalApprovalSettingValue: externalApproval, + }, + } default: return diag.FromErr(errors.Errorf("Unsupport setting: %v", name)) } @@ -81,6 +93,27 @@ func resourceSettingUpsert(ctx context.Context, d *schema.ResourceData, m interf return diags } +func convertToV1ExternalNodesSetting(d *schema.ResourceData) (*v1pb.ExternalApprovalSetting, error) { + rawList, ok := d.Get("external_approval_nodes").([]interface{}) + if !ok || len(rawList) != 1 { + return nil, errors.Errorf("invalid external_approval_nodes") + } + + raw := rawList[0].(map[string]interface{}) + nodes := raw["nodes"].([]interface{}) + externalApprovalSetting := &v1pb.ExternalApprovalSetting{} + + for _, node := range nodes { + rawNode := node.(map[string]interface{}) + externalApprovalSetting.Nodes = append(externalApprovalSetting.Nodes, &v1pb.ExternalApprovalSetting_Node{ + Id: rawNode["id"].(string), + Title: rawNode["title"].(string), + Endpoint: rawNode["endpoint"].(string), + }) + } + return externalApprovalSetting, nil +} + func convertToV1ApprovalSetting(d *schema.ResourceData) (*v1pb.WorkspaceApprovalSetting, error) { rawList, ok := d.Get("approval_flow").([]interface{}) if !ok || len(rawList) != 1 { @@ -111,6 +144,10 @@ func convertToV1ApprovalSetting(d *schema.ResourceData) (*v1pb.WorkspaceApproval return nil, errors.Errorf("invalid flow") } rawFlow := flowList[0].(map[string]interface{}) + creator := rawFlow["creator"].(string) + if !strings.HasPrefix(creator, "users/") { + return nil, errors.Errorf("creator should in users/{email} format") + } approvalRule := &v1pb.WorkspaceApprovalSetting_Rule{ Template: &v1pb.ApprovalTemplate{ Title: rawFlow["title"].(string), @@ -138,13 +175,23 @@ func convertToV1ApprovalSetting(d *schema.ResourceData) (*v1pb.WorkspaceApproval } switch stepType { case api.ApprovalNodeTypeRole: + if !strings.HasPrefix(node, "roles/") { + return nil, errors.Errorf("invalid role name: %v, role name should in roles/{role} format", node) + } approvalNode.Payload = &v1pb.ApprovalNode_Role{ Role: node, } case api.ApprovalNodeTypeGroup: group, ok := v1pb.ApprovalNode_GroupValue_value[node] if !ok { - return nil, errors.Errorf("invalid group: %v", node) + return nil, errors.Errorf( + "invalid group: %v, group should be one of: %s, %s, %s, %s", + node, + v1pb.ApprovalNode_WORKSPACE_OWNER.String(), + v1pb.ApprovalNode_WORKSPACE_DBA.String(), + v1pb.ApprovalNode_PROJECT_OWNER.String(), + v1pb.ApprovalNode_PROJECT_MEMBER.String(), + ) } approvalNode.Payload = &v1pb.ApprovalNode_GroupValue_{ GroupValue: v1pb.ApprovalNode_GroupValue(group),