diff --git a/VERSION b/VERSION
index e7ad390..450f88e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.0.19
\ No newline at end of file
+1.0.20
\ No newline at end of file
diff --git a/api/client.go b/api/client.go
index 69d0b74..08cdd3a 100644
--- a/api/client.go
+++ b/api/client.go
@@ -163,4 +163,14 @@ type Client interface {
GetWorkspaceIAMPolicy(ctx context.Context) (*v1pb.IamPolicy, error)
// SetWorkspaceIAMPolicy sets the workspace IAM policy.
SetWorkspaceIAMPolicy(ctx context.Context, setIamPolicyRequest *v1pb.SetIamPolicyRequest) (*v1pb.IamPolicy, error)
+
+ // Review config
+ // ListReviewConfig will return review configs.
+ ListReviewConfig(ctx context.Context) (*v1pb.ListReviewConfigsResponse, error)
+ // GetReviewConfig gets the review config by full name.
+ GetReviewConfig(ctx context.Context, reviewName string) (*v1pb.ReviewConfig, error)
+ // UpsertReviewConfig updates or creates the review config.
+ UpsertReviewConfig(ctx context.Context, patch *v1pb.ReviewConfig, updateMasks []string) (*v1pb.ReviewConfig, error)
+ // DeleteReviewConfig deletes the review config.
+ DeleteReviewConfig(ctx context.Context, reviewName string) error
}
diff --git a/client/instance.go b/client/instance.go
index 8e453b2..91e4904 100644
--- a/client/instance.go
+++ b/client/instance.go
@@ -10,7 +10,7 @@ import (
"google.golang.org/protobuf/encoding/protojson"
)
-// ListInstance will return instances in environment.
+// ListInstance will return instances.
func (c *client) ListInstance(ctx context.Context, showDeleted bool) (*v1pb.ListInstancesResponse, error) {
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/instances?showDeleted=%v", c.url, c.version, showDeleted), nil)
if err != nil {
diff --git a/client/review_config.go b/client/review_config.go
new file mode 100644
index 0000000..4fcbe85
--- /dev/null
+++ b/client/review_config.go
@@ -0,0 +1,64 @@
+package client
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ v1pb "github.com/bytebase/bytebase/proto/generated-go/v1"
+)
+
+// ListReviewConfig will return review configs.
+func (c *client) ListReviewConfig(ctx context.Context) (*v1pb.ListReviewConfigsResponse, error) {
+ req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/reviewConfigs", c.url, c.version), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ body, err := c.doRequest(req)
+ if err != nil {
+ return nil, err
+ }
+
+ var res v1pb.ListReviewConfigsResponse
+ if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil {
+ return nil, err
+ }
+
+ return &res, nil
+}
+
+// GetReviewConfig gets the review config by full name.
+func (c *client) GetReviewConfig(ctx context.Context, reviewName string) (*v1pb.ReviewConfig, error) {
+ body, err := c.getResource(ctx, reviewName)
+ if err != nil {
+ return nil, err
+ }
+
+ var res v1pb.ReviewConfig
+ if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil {
+ return nil, err
+ }
+
+ return &res, nil
+}
+
+// UpsertReviewConfig updates or creates the review config.
+func (c *client) UpsertReviewConfig(ctx context.Context, patch *v1pb.ReviewConfig, updateMasks []string) (*v1pb.ReviewConfig, error) {
+ body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, true /* allow missing */)
+ if err != nil {
+ return nil, err
+ }
+
+ var res v1pb.ReviewConfig
+ if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil {
+ return nil, err
+ }
+
+ return &res, nil
+}
+
+// DeleteReviewConfig deletes the review config.
+func (c *client) DeleteReviewConfig(ctx context.Context, reviewName string) error {
+ return c.deleteResource(ctx, reviewName)
+}
diff --git a/docs/data-sources/group.md b/docs/data-sources/group.md
index 6779230..571116c 100644
--- a/docs/data-sources/group.md
+++ b/docs/data-sources/group.md
@@ -24,6 +24,7 @@ The group data source.
- `description` (String) The group description.
- `id` (String) The ID of this resource.
- `members` (Set of Object) The members in the group. (see [below for nested schema](#nestedatt--members))
+- `roles` (Set of String) The group's roles in the workspace level
- `source` (String) Source means where the group comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID.
- `title` (String) The group title.
diff --git a/docs/data-sources/group_list.md b/docs/data-sources/group_list.md
index a21bd4f..fb47f7b 100644
--- a/docs/data-sources/group_list.md
+++ b/docs/data-sources/group_list.md
@@ -28,6 +28,7 @@ Read-Only:
- `description` (String)
- `members` (Set of Object) (see [below for nested schema](#nestedobjatt--groups--members))
- `name` (String)
+- `roles` (Set of String)
- `source` (String)
- `title` (String)
diff --git a/docs/data-sources/review_config.md b/docs/data-sources/review_config.md
new file mode 100644
index 0000000..bae2a43
--- /dev/null
+++ b/docs/data-sources/review_config.md
@@ -0,0 +1,38 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "bytebase_review_config Data Source - terraform-provider-bytebase"
+subcategory: ""
+description: |-
+ The review config data source.
+---
+
+# bytebase_review_config (Data Source)
+
+The review config data source.
+
+
+
+
+## Schema
+
+### Read-Only
+
+- `enabled` (Boolean) Enable the SQL review config
+- `id` (String) The ID of this resource.
+- `resource_id` (String) The unique resource id for the review config.
+- `resources` (Set of String) Resources using the config. We support attach the review config for environments or projects with format {resurce}/{resource id}. For example, environments/test, projects/sample.
+- `rules` (Set of Object) The SQL review rules. (see [below for nested schema](#nestedatt--rules))
+- `title` (String) The title for the review config.
+
+
+### Nested Schema for `rules`
+
+Read-Only:
+
+- `comment` (String)
+- `engine` (String)
+- `level` (String)
+- `payload` (String)
+- `type` (String)
+
+
diff --git a/docs/data-sources/review_config_list.md b/docs/data-sources/review_config_list.md
new file mode 100644
index 0000000..c3bcb7a
--- /dev/null
+++ b/docs/data-sources/review_config_list.md
@@ -0,0 +1,45 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "bytebase_review_config_list Data Source - terraform-provider-bytebase"
+subcategory: ""
+description: |-
+ The review config data source list.
+---
+
+# bytebase_review_config_list (Data Source)
+
+The review config data source list.
+
+
+
+
+## Schema
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+- `review_configs` (List of Object) (see [below for nested schema](#nestedatt--review_configs))
+
+
+### Nested Schema for `review_configs`
+
+Read-Only:
+
+- `enabled` (Boolean)
+- `resource_id` (String)
+- `resources` (Set of String)
+- `rules` (Set of Object) (see [below for nested schema](#nestedobjatt--review_configs--rules))
+- `title` (String)
+
+
+### Nested Schema for `review_configs.rules`
+
+Read-Only:
+
+- `comment` (String)
+- `engine` (String)
+- `level` (String)
+- `payload` (String)
+- `type` (String)
+
+
diff --git a/docs/resources/group.md b/docs/resources/group.md
index daa7cd9..84ed19f 100644
--- a/docs/resources/group.md
+++ b/docs/resources/group.md
@@ -24,6 +24,7 @@ The group resource. Workspace domain is required for creating groups.
### Optional
- `description` (String) The group description.
+- `roles` (Set of String) The group's roles in the workspace level
### Read-Only
diff --git a/docs/resources/review_config.md b/docs/resources/review_config.md
new file mode 100644
index 0000000..5bda0cc
--- /dev/null
+++ b/docs/resources/review_config.md
@@ -0,0 +1,47 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "bytebase_review_config Resource - terraform-provider-bytebase"
+subcategory: ""
+description: |-
+ The review config resource.
+---
+
+# bytebase_review_config (Resource)
+
+The review config resource.
+
+
+
+
+## Schema
+
+### Required
+
+- `enabled` (Boolean) Enable the SQL review config
+- `resource_id` (String) The unique resource id for the review config.
+- `rules` (Block Set, Min: 1) The SQL review rules. (see [below for nested schema](#nestedblock--rules))
+- `title` (String) The title for the review config.
+
+### Optional
+
+- `resources` (Set of String) Resources using the config. We support attach the review config for environments or projects with format {resurce}/{resource id}. For example, environments/test, projects/sample.
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `rules`
+
+Required:
+
+- `engine` (String) The rule for the database engine.
+- `level` (String) The rule level.
+- `type` (String) The rule unique type. Check https://www.bytebase.com/docs/sql-review/review-rules for all rules
+
+Optional:
+
+- `comment` (String) The comment for the rule.
+- `payload` (String) The payload for the rule.
+
+
diff --git a/docs/resources/user.md b/docs/resources/user.md
index 8cfe660..71d19a7 100644
--- a/docs/resources/user.md
+++ b/docs/resources/user.md
@@ -25,6 +25,7 @@ The user resource.
- `password` (String, Sensitive) The user login password.
- `phone` (String) The user phone.
- `roles` (Set of String) The user's roles in the workspace level
+- `type` (String) The user type.
### Read-Only
@@ -33,8 +34,8 @@ The user resource.
- `last_login_time` (String) The user last login time.
- `mfa_enabled` (Boolean) The mfa_enabled flag means if the user has enabled MFA.
- `name` (String) The user name in users/{user id or email} format.
+- `service_key` (String) The service key for service account.
- `source` (String) Source means where the user comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID.
- `state` (String) The user is deleted or not.
-- `type` (String) The user type.
diff --git a/examples/environments/main.tf b/examples/environments/main.tf
index 288369d..b7c07ea 100644
--- a/examples/environments/main.tf
+++ b/examples/environments/main.tf
@@ -2,7 +2,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.19"
+ version = "1.0.20"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/groups/main.tf b/examples/groups/main.tf
index 4c8764c..2b88420 100644
--- a/examples/groups/main.tf
+++ b/examples/groups/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.19"
+ version = "1.0.20"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/instances/main.tf b/examples/instances/main.tf
index b3626b6..206ea08 100644
--- a/examples/instances/main.tf
+++ b/examples/instances/main.tf
@@ -2,7 +2,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.19"
+ version = "1.0.20"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/policies/main.tf b/examples/policies/main.tf
index 6f45f4c..bd39820 100644
--- a/examples/policies/main.tf
+++ b/examples/policies/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.19"
+ version = "1.0.20"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/projects/main.tf b/examples/projects/main.tf
index d3d3f61..cec7e85 100644
--- a/examples/projects/main.tf
+++ b/examples/projects/main.tf
@@ -2,7 +2,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.19"
+ version = "1.0.20"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/roles/main.tf b/examples/roles/main.tf
index 92b116c..f9112a2 100644
--- a/examples/roles/main.tf
+++ b/examples/roles/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.19"
+ version = "1.0.20"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/settings/main.tf b/examples/settings/main.tf
index 8f0d146..614bceb 100644
--- a/examples/settings/main.tf
+++ b/examples/settings/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.19"
+ version = "1.0.20"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/setup/main.tf b/examples/setup/main.tf
index 116af61..6401533 100644
--- a/examples/setup/main.tf
+++ b/examples/setup/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.19"
+ version = "1.0.20"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/setup/sql_review.tf b/examples/setup/sql_review.tf
new file mode 100644
index 0000000..786d4c3
--- /dev/null
+++ b/examples/setup/sql_review.tf
@@ -0,0 +1,24 @@
+resource "bytebase_review_config" "sample" {
+ depends_on = [
+ bytebase_environment.test,
+ bytebase_environment.prod
+ ]
+
+ resource_id = "review-config-sample"
+ title = "Sample SQL Review Config"
+ enabled = true
+ resources = toset([
+ bytebase_environment.test.name,
+ bytebase_environment.prod.name
+ ])
+ rules {
+ type = "column.no-null"
+ engine = "MYSQL"
+ level = "WARNING"
+ }
+ rules {
+ type = "table.require-pk"
+ engine = "MYSQL"
+ level = "ERROR"
+ }
+}
diff --git a/examples/setup/users.tf b/examples/setup/users.tf
index d8c178a..c681f47 100644
--- a/examples/setup/users.tf
+++ b/examples/setup/users.tf
@@ -33,16 +33,28 @@ resource "bytebase_user" "project_developer" {
roles = ["roles/projectViewer"]
}
+resource "bytebase_user" "service_account" {
+ depends_on = [
+ bytebase_user.project_developer
+ ]
+ title = "CI Bot"
+ email = "ci-bot@service.bytebase.com"
+ type = "SERVICE_ACCOUNT"
+ roles = ["roles/workspaceDBA"]
+}
+
+
# Create or update the group.
resource "bytebase_group" "developers" {
depends_on = [
bytebase_user.workspace_dba,
bytebase_user.project_developer,
+ bytebase_user.service_account,
# group requires the domain.
bytebase_setting.workspace_profile
]
- email = "developers@bytebase.com"
+ email = "developers+dba@bytebase.com"
title = "Bytebase Developers"
members {
@@ -54,4 +66,6 @@ resource "bytebase_group" "developers" {
member = format("users/%s", bytebase_user.project_developer.email)
role = "MEMBER"
}
+
+ roles = ["roles/projectViewer"]
}
diff --git a/examples/sql_review/main.tf b/examples/sql_review/main.tf
new file mode 100644
index 0000000..aef9f8e
--- /dev/null
+++ b/examples/sql_review/main.tf
@@ -0,0 +1,25 @@
+terraform {
+ required_providers {
+ bytebase = {
+ version = "1.0.20"
+ # 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_review_config_list" "all" {
+}
+
+output "all_review_configs" {
+ value = data.bytebase_review_config_list.all
+}
diff --git a/examples/users/main.tf b/examples/users/main.tf
index 4c2d7ed..093b690 100644
--- a/examples/users/main.tf
+++ b/examples/users/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.19"
+ version = "1.0.20"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/vcs/main.tf b/examples/vcs/main.tf
index d72c644..cf2c1d4 100644
--- a/examples/vcs/main.tf
+++ b/examples/vcs/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.19"
+ version = "1.0.20"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/provider/data_source_group.go b/provider/data_source_group.go
index 26e3fee..e343abc 100644
--- a/provider/data_source_group.go
+++ b/provider/data_source_group.go
@@ -42,6 +42,14 @@ func dataSourceGroup() *schema.Resource {
Computed: true,
Description: "Source means where the group comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID.",
},
+ "roles": {
+ Type: schema.TypeSet,
+ Computed: true,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ Description: "The group's roles in the workspace level",
+ },
"members": {
Type: schema.TypeSet,
Computed: true,
@@ -75,10 +83,15 @@ func dataSourceGroupRead(ctx context.Context, d *schema.ResourceData, m interfac
}
d.SetId(group.Name)
- return setGroup(d, group)
+ return setGroup(ctx, c, d, group)
}
-func setGroup(d *schema.ResourceData, group *v1pb.Group) diag.Diagnostics {
+func setGroup(ctx context.Context, client api.Client, d *schema.ResourceData, group *v1pb.Group) diag.Diagnostics {
+ workspaceIAM, err := client.GetWorkspaceIAMPolicy(ctx)
+ if err != nil {
+ return diag.Errorf("cannot get workspace IAM with error: %s", err.Error())
+ }
+
if err := d.Set("name", group.Name); err != nil {
return diag.Errorf("cannot set name for group: %s", err.Error())
}
@@ -102,6 +115,13 @@ func setGroup(d *schema.ResourceData, group *v1pb.Group) diag.Diagnostics {
if err := d.Set("members", schema.NewSet(memberHash, memberList)); err != nil {
return diag.Errorf("cannot set members for group: %s", err.Error())
}
+ groupEmail, err := internal.GetGroupEmail(group.Name)
+ if err != nil {
+ return diag.Errorf("failed to parse group email: %v", err)
+ }
+ if err := d.Set("roles", getRolesInIAM(workspaceIAM, fmt.Sprintf("group:%s", groupEmail))); err != nil {
+ return diag.Errorf("cannot set roles for user: %s", err.Error())
+ }
return nil
}
diff --git a/provider/data_source_group_list.go b/provider/data_source_group_list.go
index 35abc02..d597a07 100644
--- a/provider/data_source_group_list.go
+++ b/provider/data_source_group_list.go
@@ -2,6 +2,7 @@ package provider
import (
"context"
+ "fmt"
"strconv"
"time"
@@ -9,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/bytebase/terraform-provider-bytebase/api"
+ "github.com/bytebase/terraform-provider-bytebase/provider/internal"
)
func dataSourceGroupList() *schema.Resource {
@@ -41,6 +43,14 @@ func dataSourceGroupList() *schema.Resource {
Computed: true,
Description: "Source means where the group comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID.",
},
+ "roles": {
+ Type: schema.TypeSet,
+ Computed: true,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ Description: "The group's roles in the workspace level",
+ },
"members": {
Type: schema.TypeSet,
Computed: true,
@@ -76,6 +86,11 @@ func dataSourceGroupListRead(ctx context.Context, d *schema.ResourceData, m inte
return diag.FromErr(err)
}
+ workspaceIAM, err := c.GetWorkspaceIAMPolicy(ctx)
+ if err != nil {
+ return diag.Errorf("cannot get workspace IAM with error: %s", err.Error())
+ }
+
groups := make([]map[string]interface{}, 0)
for _, group := range response.Groups {
raw := make(map[string]interface{})
@@ -92,6 +107,12 @@ func dataSourceGroupListRead(ctx context.Context, d *schema.ResourceData, m inte
memberList = append(memberList, rawMember)
}
raw["members"] = schema.NewSet(memberHash, memberList)
+
+ groupEmail, err := internal.GetGroupEmail(group.Name)
+ if err != nil {
+ return diag.Errorf("failed to parse group email: %v", err)
+ }
+ raw["roles"] = getRolesInIAM(workspaceIAM, fmt.Sprintf("group:%s", groupEmail))
groups = append(groups, raw)
}
diff --git a/provider/data_source_review_config.go b/provider/data_source_review_config.go
new file mode 100644
index 0000000..9ad7f6f
--- /dev/null
+++ b/provider/data_source_review_config.go
@@ -0,0 +1,93 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+
+ "github.com/bytebase/terraform-provider-bytebase/api"
+ "github.com/bytebase/terraform-provider-bytebase/provider/internal"
+)
+
+func dataSourceReviewConfig() *schema.Resource {
+ return &schema.Resource{
+ Description: "The review config data source.",
+ ReadContext: dataSourceReviewConfigRead,
+ Schema: map[string]*schema.Schema{
+ "resource_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The unique resource id for the review config.",
+ },
+ "title": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The title for the review config.",
+ },
+ "enabled": {
+ Type: schema.TypeBool,
+ Computed: true,
+ Description: "Enable the SQL review config",
+ },
+ "resources": {
+ Type: schema.TypeSet,
+ Computed: true,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ Description: "Resources using the config. We support attach the review config for environments or projects with format {resurce}/{resource id}. For example, environments/test, projects/sample.",
+ },
+ "rules": {
+ Type: schema.TypeSet,
+ Computed: true,
+ Description: "The SQL review rules.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "type": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The rule unique type. Check https://www.bytebase.com/docs/sql-review/review-rules for all rules",
+ },
+ "engine": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The rule for the database engine.",
+ },
+ "level": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The rule level.",
+ },
+ "payload": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The payload for the rule.",
+ },
+ "comment": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The comment for the rule.",
+ },
+ },
+ },
+ Set: reviewRuleHash,
+ },
+ },
+ }
+}
+
+func dataSourceReviewConfigRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ c := m.(api.Client)
+ reviewName := fmt.Sprintf("%s%s", internal.ReviewConfigNamePrefix, d.Get("resource_id").(string))
+
+ review, err := c.GetReviewConfig(ctx, reviewName)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ d.SetId(review.Name)
+
+ return setReviewConfig(d, review)
+}
diff --git a/provider/data_source_review_config_list.go b/provider/data_source_review_config_list.go
new file mode 100644
index 0000000..a3d6500
--- /dev/null
+++ b/provider/data_source_review_config_list.go
@@ -0,0 +1,122 @@
+package provider
+
+import (
+ "context"
+ "strconv"
+ "time"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+
+ "github.com/bytebase/terraform-provider-bytebase/api"
+ "github.com/bytebase/terraform-provider-bytebase/provider/internal"
+)
+
+func dataSourceReviewConfigList() *schema.Resource {
+ return &schema.Resource{
+ Description: "The review config data source list.",
+ ReadContext: dataSourceReviewConfigListRead,
+ Schema: map[string]*schema.Schema{
+ "review_configs": {
+ Type: schema.TypeList,
+ Computed: true,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "resource_id": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The unique resource id for the review config.",
+ },
+ "title": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The title for the review config.",
+ },
+ "enabled": {
+ Type: schema.TypeBool,
+ Computed: true,
+ Description: "Enable the SQL review config",
+ },
+ "resources": {
+ Type: schema.TypeSet,
+ Computed: true,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ Description: "Resources using the config. We support attach the review config for environments or projects with format {resurce}/{resource id}. For example, environments/test, projects/sample.",
+ },
+ "rules": {
+ Type: schema.TypeSet,
+ Computed: true,
+ Description: "The SQL review rules.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "type": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The rule unique type. Check https://www.bytebase.com/docs/sql-review/review-rules for all rules",
+ },
+ "engine": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The rule for the database engine.",
+ },
+ "level": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The rule level.",
+ },
+ "payload": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The payload for the rule.",
+ },
+ "comment": {
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "The comment for the rule.",
+ },
+ },
+ },
+ Set: reviewRuleHash,
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func dataSourceReviewConfigListRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ c := m.(api.Client)
+
+ response, err := c.ListReviewConfig(ctx)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ reviews := make([]map[string]interface{}, 0)
+ for _, review := range response.ReviewConfigs {
+ raw := make(map[string]interface{})
+ reviewID, err := internal.GetReviewConfigID(review.Name)
+ if err != nil {
+ return diag.Errorf("failed to parse id from review name %s with error: %v", review.Name, err.Error())
+ }
+ raw["resource_id"] = reviewID
+ raw["title"] = review.Title
+ raw["enabled"] = review.Enabled
+ raw["resources"] = review.Resources
+ raw["rules"] = schema.NewSet(reviewRuleHash, flattenReviewRules(review.Rules))
+
+ reviews = append(reviews, raw)
+ }
+
+ if err := d.Set("review_configs", reviews); err != nil {
+ return diag.FromErr(err)
+ }
+
+ // always refresh
+ d.SetId(strconv.FormatInt(time.Now().Unix(), 10))
+
+ return nil
+}
diff --git a/provider/data_source_user.go b/provider/data_source_user.go
index 8553765..644a060 100644
--- a/provider/data_source_user.go
+++ b/provider/data_source_user.go
@@ -99,13 +99,12 @@ func dataSourceUserRead(ctx context.Context, d *schema.ResourceData, m interface
return setUser(ctx, c, d, user)
}
-func getUserRoles(iamPolicy *v1pb.IamPolicy, email string) []string {
- userBinding := fmt.Sprintf("user:%s", email)
+func getRolesInIAM(iamPolicy *v1pb.IamPolicy, memberBinding string) []string {
roles := []string{}
for _, binding := range iamPolicy.Bindings {
for _, member := range binding.Members {
- if member == userBinding {
+ if member == memberBinding {
roles = append(roles, binding.Role)
}
}
@@ -153,7 +152,7 @@ func setUser(ctx context.Context, client api.Client, d *schema.ResourceData, use
return diag.Errorf("cannot set source for user: %s", err.Error())
}
}
- if err := d.Set("roles", getUserRoles(workspaceIAM, user.Email)); err != nil {
+ if err := d.Set("roles", getRolesInIAM(workspaceIAM, fmt.Sprintf("user:%s", user.Email))); err != nil {
return diag.Errorf("cannot set roles for user: %s", err.Error())
}
diff --git a/provider/data_source_user_list.go b/provider/data_source_user_list.go
index 8acb045..be88fcc 100644
--- a/provider/data_source_user_list.go
+++ b/provider/data_source_user_list.go
@@ -2,6 +2,7 @@ package provider
import (
"context"
+ "fmt"
"strconv"
"time"
@@ -120,7 +121,7 @@ func dataSourceUserListRead(ctx context.Context, d *schema.ResourceData, m inter
raw["last_login_time"] = p.LastLoginTime.AsTime().UTC().Format(time.RFC3339)
raw["last_change_password_time"] = p.LastChangePasswordTime.AsTime().UTC().Format(time.RFC3339)
}
- raw["roles"] = getUserRoles(workspaceIAM, user.Email)
+ raw["roles"] = getRolesInIAM(workspaceIAM, fmt.Sprintf("user:%s", user.Email))
users = append(users, raw)
}
if err := d.Set("users", users); err != nil {
diff --git a/provider/internal/mock_client.go b/provider/internal/mock_client.go
index c2f88a4..b16ad51 100644
--- a/provider/internal/mock_client.go
+++ b/provider/internal/mock_client.go
@@ -901,3 +901,23 @@ func (c *mockClient) DeleteRole(_ context.Context, roleName string) error {
delete(c.roleMap, roleName)
return nil
}
+
+// ListReviewConfig will return review configs.
+func (*mockClient) ListReviewConfig(_ context.Context) (*v1pb.ListReviewConfigsResponse, error) {
+ return &v1pb.ListReviewConfigsResponse{}, nil
+}
+
+// GetReviewConfig gets the review config by full name.
+func (*mockClient) GetReviewConfig(_ context.Context, _ string) (*v1pb.ReviewConfig, error) {
+ return &v1pb.ReviewConfig{}, nil
+}
+
+// UpsertReviewConfig updates or creates the review config.
+func (*mockClient) UpsertReviewConfig(_ context.Context, _ *v1pb.ReviewConfig, _ []string) (*v1pb.ReviewConfig, error) {
+ return &v1pb.ReviewConfig{}, nil
+}
+
+// DeleteReviewConfig deletes the review config.
+func (*mockClient) DeleteReviewConfig(_ context.Context, _ string) error {
+ return nil
+}
diff --git a/provider/internal/utils.go b/provider/internal/utils.go
index d40dd3b..44a1600 100644
--- a/provider/internal/utils.go
+++ b/provider/internal/utils.go
@@ -37,6 +37,8 @@ const (
GroupNamePrefix = "groups/"
// RoleNamePrefix is the prefix for role name.
RoleNamePrefix = "roles/"
+ // ReviewConfigNamePrefix is the prefix for the review config name.
+ ReviewConfigNamePrefix = "reviewConfigs/"
// DatabaseCatalogNameSuffix is the suffix for the database catalog name.
DatabaseCatalogNameSuffix = "/catalog"
// ResourceIDPattern is the pattern for resource id.
@@ -80,6 +82,31 @@ func ResourceNameValidation(regexs ...*regexp.Regexp) schema.SchemaValidateDiagF
}
}
+// EngineValidation validate the engine type.
+var EngineValidation = validation.StringInSlice([]string{
+ v1pb.Engine_CLICKHOUSE.String(),
+ v1pb.Engine_MYSQL.String(),
+ v1pb.Engine_POSTGRES.String(),
+ v1pb.Engine_SNOWFLAKE.String(),
+ v1pb.Engine_SQLITE.String(),
+ v1pb.Engine_TIDB.String(),
+ v1pb.Engine_MONGODB.String(),
+ v1pb.Engine_REDIS.String(),
+ v1pb.Engine_ORACLE.String(),
+ v1pb.Engine_SPANNER.String(),
+ v1pb.Engine_MSSQL.String(),
+ v1pb.Engine_REDSHIFT.String(),
+ v1pb.Engine_MARIADB.String(),
+ v1pb.Engine_OCEANBASE.String(),
+ v1pb.Engine_DM.String(),
+ v1pb.Engine_RISINGWAVE.String(),
+ v1pb.Engine_OCEANBASE_ORACLE.String(),
+ v1pb.Engine_STARROCKS.String(),
+ v1pb.Engine_DORIS.String(),
+ v1pb.Engine_HIVE.String(),
+ v1pb.Engine_ELASTICSEARCH.String(),
+}, false)
+
// GetPolicyParentAndType returns the policy parent and type by the name.
func GetPolicyParentAndType(name string) (string, v1pb.PolicyType, error) {
names := strings.Split(name, PolicyNamePrefix)
@@ -153,6 +180,24 @@ func GetRoleID(name string) (string, error) {
return tokens[0], nil
}
+// GetGroupEmail will parse the email from group full name.
+func GetGroupEmail(name string) (string, error) {
+ tokens, err := getNameParentTokens(name, GroupNamePrefix)
+ if err != nil {
+ return "", err
+ }
+ return tokens[0], nil
+}
+
+// GetReviewConfigID will parse the id from review config full name.
+func GetReviewConfigID(name string) (string, error) {
+ tokens, err := getNameParentTokens(name, ReviewConfigNamePrefix)
+ if err != nil {
+ return "", err
+ }
+ return tokens[0], nil
+}
+
// GetInstanceDatabaseID will parse the instance resource id and database name.
func GetInstanceDatabaseID(name string) (string, string, error) {
// the instance request should be instances/{instance-id}/databases/{database-id}
diff --git a/provider/provider.go b/provider/provider.go
index 7702d4d..9136a1b 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -70,6 +70,8 @@ func NewProvider() *schema.Provider {
"bytebase_group_list": dataSourceGroupList(),
"bytebase_database": dataSourceDatabase(),
"bytebase_database_list": dataSourceDatabaseList(),
+ "bytebase_review_config": dataSourceReviewConfig(),
+ "bytebase_review_config_list": dataSourceReviewConfigList(),
},
ResourcesMap: map[string]*schema.Resource{
"bytebase_environment": resourceEnvironment(),
@@ -83,6 +85,7 @@ func NewProvider() *schema.Provider {
"bytebase_role": resourceRole(),
"bytebase_group": resourceGroup(),
"bytebase_database": resourceDatabase(),
+ "bytebase_review_config": resourceReviewConfig(),
},
}
}
diff --git a/provider/resource_group.go b/provider/resource_group.go
index b6c7e4d..cb9bb69 100644
--- a/provider/resource_group.go
+++ b/provider/resource_group.go
@@ -55,6 +55,14 @@ func resourceGroup() *schema.Resource {
Computed: true,
Description: "Source means where the group comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID.",
},
+ "roles": {
+ Type: schema.TypeSet,
+ Optional: true,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ Description: "The group's roles in the workspace level",
+ },
"members": {
Type: schema.TypeSet,
Required: true,
@@ -96,7 +104,7 @@ func resourceGroupRead(ctx context.Context, d *schema.ResourceData, m interface{
return diag.FromErr(err)
}
- return setGroup(d, group)
+ return setGroup(ctx, c, d, group)
}
func resourceGroupDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
@@ -172,6 +180,19 @@ func resourceGroupCreate(ctx context.Context, d *schema.ResourceData, m interfac
}
}
+ roles, diagnostic := getRoles(d)
+ if diagnostic != nil {
+ return diagnostic
+ }
+ if err := patchWorkspaceIAMPolicy(ctx, c, fmt.Sprintf("group:%s", groupEmail), roles); err != nil {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Failed to patch group roles",
+ Detail: fmt.Sprintf("Update roles for group %s failed, error: %v", groupName, err),
+ })
+ return diags
+ }
+
d.SetId(groupName)
diag := resourceGroupRead(ctx, d, m)
@@ -225,6 +246,22 @@ func resourceGroupUpdate(ctx context.Context, d *schema.ResourceData, m interfac
}
}
+ if d.HasChange("roles") {
+ roles, diagnostic := getRoles(d)
+ if diagnostic != nil {
+ return diagnostic
+ }
+ groupEmail := d.Get("email").(string)
+ if err := patchWorkspaceIAMPolicy(ctx, c, fmt.Sprintf("group:%s", groupEmail), roles); err != nil {
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Error,
+ Summary: "Failed to patch group roles",
+ Detail: fmt.Sprintf("Update roles for group %s failed, error: %v", groupName, err),
+ })
+ return diags
+ }
+ }
+
diag := resourceGroupRead(ctx, d, m)
if diag != nil {
diags = append(diags, diag...)
diff --git a/provider/resource_instance.go b/provider/resource_instance.go
index 5902917..739f3d0 100644
--- a/provider/resource_instance.go
+++ b/provider/resource_instance.go
@@ -59,32 +59,10 @@ func resourceInstance() *schema.Resource {
Description: "The instance full name in instances/{resource id} format.",
},
"engine": {
- Type: schema.TypeString,
- Required: true,
- ValidateFunc: validation.StringInSlice([]string{
- v1pb.Engine_CLICKHOUSE.String(),
- v1pb.Engine_MYSQL.String(),
- v1pb.Engine_POSTGRES.String(),
- v1pb.Engine_SNOWFLAKE.String(),
- v1pb.Engine_SQLITE.String(),
- v1pb.Engine_TIDB.String(),
- v1pb.Engine_MONGODB.String(),
- v1pb.Engine_REDIS.String(),
- v1pb.Engine_ORACLE.String(),
- v1pb.Engine_SPANNER.String(),
- v1pb.Engine_MSSQL.String(),
- v1pb.Engine_REDSHIFT.String(),
- v1pb.Engine_MARIADB.String(),
- v1pb.Engine_OCEANBASE.String(),
- v1pb.Engine_DM.String(),
- v1pb.Engine_RISINGWAVE.String(),
- v1pb.Engine_OCEANBASE_ORACLE.String(),
- v1pb.Engine_STARROCKS.String(),
- v1pb.Engine_DORIS.String(),
- v1pb.Engine_HIVE.String(),
- v1pb.Engine_ELASTICSEARCH.String(),
- }, false),
- Description: "The instance engine. Support MYSQL, POSTGRES, TIDB, SNOWFLAKE, CLICKHOUSE, MONGODB, SQLITE, REDIS, ORACLE, SPANNER, MSSQL, REDSHIFT, MARIADB, OCEANBASE.",
+ Type: schema.TypeString,
+ Required: true,
+ ValidateFunc: internal.EngineValidation,
+ Description: "The instance engine. Support MYSQL, POSTGRES, TIDB, SNOWFLAKE, CLICKHOUSE, MONGODB, SQLITE, REDIS, ORACLE, SPANNER, MSSQL, REDSHIFT, MARIADB, OCEANBASE.",
},
"engine_version": {
Type: schema.TypeString,
diff --git a/provider/resource_review_config.go b/provider/resource_review_config.go
new file mode 100644
index 0000000..3561dbd
--- /dev/null
+++ b/provider/resource_review_config.go
@@ -0,0 +1,269 @@
+package provider
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "strings"
+
+ v1pb "github.com/bytebase/bytebase/proto/generated-go/v1"
+ "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"
+ "github.com/bytebase/terraform-provider-bytebase/provider/internal"
+)
+
+func resourceReviewConfig() *schema.Resource {
+ return &schema.Resource{
+ Description: "The review config resource.",
+ ReadContext: resourceReviewConfigRead,
+ DeleteContext: resourceReviewConfigDelete,
+ CreateContext: resourceReviewConfigUpsert,
+ UpdateContext: resourceReviewConfigUpsert,
+ Importer: &schema.ResourceImporter{
+ StateContext: schema.ImportStatePassthroughContext,
+ },
+ Schema: map[string]*schema.Schema{
+ "resource_id": {
+ Type: schema.TypeString,
+ Required: true,
+ ValidateFunc: internal.ResourceIDValidation,
+ Description: "The unique resource id for the review config.",
+ },
+ "title": {
+ Type: schema.TypeString,
+ Required: true,
+ ValidateFunc: validation.StringIsNotEmpty,
+ Description: "The title for the review config.",
+ },
+ "enabled": {
+ Type: schema.TypeBool,
+ Required: true,
+ Description: "Enable the SQL review config",
+ },
+ "resources": {
+ Type: schema.TypeSet,
+ Optional: true,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ Description: "Resources using the config. We support attach the review config for environments or projects with format {resurce}/{resource id}. For example, environments/test, projects/sample.",
+ },
+ "rules": {
+ Type: schema.TypeSet,
+ Required: true,
+ MinItems: 1,
+ Description: "The SQL review rules.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "type": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The rule unique type. Check https://www.bytebase.com/docs/sql-review/review-rules for all rules",
+ },
+ "engine": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The rule for the database engine.",
+ ValidateFunc: internal.EngineValidation,
+ },
+ "level": {
+ Type: schema.TypeString,
+ Required: true,
+ Description: "The rule level.",
+ ValidateFunc: validation.StringInSlice([]string{
+ v1pb.SQLReviewRuleLevel_WARNING.String(),
+ v1pb.SQLReviewRuleLevel_ERROR.String(),
+ v1pb.SQLReviewRuleLevel_DISABLED.String(),
+ }, false),
+ },
+ "payload": {
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ Description: "The payload for the rule.",
+ },
+ "comment": {
+ Type: schema.TypeString,
+ Optional: true,
+ Computed: true,
+ Description: "The comment for the rule.",
+ },
+ },
+ },
+ Set: reviewRuleHash,
+ },
+ },
+ }
+}
+
+func resourceReviewConfigRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ c := m.(api.Client)
+
+ fullName := d.Id()
+ review, err := c.GetReviewConfig(ctx, fullName)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ return setReviewConfig(d, review)
+}
+
+func resourceReviewConfigDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ c := m.(api.Client)
+ fullName := d.Id()
+
+ // Warning or errors can be collected in a slice type
+ var diags diag.Diagnostics
+
+ if err := c.DeleteReviewConfig(ctx, fullName); err != nil {
+ return diag.FromErr(err)
+ }
+
+ d.SetId("")
+
+ return diags
+}
+
+func resourceReviewConfigUpsert(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ c := m.(api.Client)
+
+ reviewID := d.Get("resource_id").(string)
+ reviewName := fmt.Sprintf("%s%s", internal.ReviewConfigNamePrefix, reviewID)
+
+ rules, err := convertToV1RuleList(d)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ reviewConfig := &v1pb.ReviewConfig{
+ Name: reviewName,
+ Title: d.Get("title").(string),
+ Enabled: d.Get("enabled").(bool),
+ Rules: rules,
+ }
+ review, err := c.UpsertReviewConfig(ctx, reviewConfig, []string{
+ "title",
+ "enabled",
+ "rules",
+ })
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
+ patchTagPolicy(ctx, c, d, review.Name)
+ d.SetId(review.Name)
+
+ return resourceReviewConfigRead(ctx, d, m)
+}
+
+func patchTagPolicy(ctx context.Context, client api.Client, d *schema.ResourceData, reviewName string) diag.Diagnostics {
+ rawSet, ok := d.Get("resources").(*schema.Set)
+ if !ok || rawSet.Len() == 0 {
+ return nil
+ }
+ for _, raw := range rawSet.List() {
+ resource := raw.(string)
+ if !strings.HasPrefix(resource, internal.ProjectNamePrefix) && !strings.HasPrefix(resource, internal.EnvironmentNamePrefix) {
+ return diag.Errorf("invalid resource, only support projects/{id} or environments/{id}")
+ }
+ policyName := fmt.Sprintf("%s/%s%s", resource, internal.PolicyNamePrefix, v1pb.PolicyType_TAG.String())
+ if _, err := client.UpsertPolicy(ctx, &v1pb.Policy{
+ Name: policyName,
+ Enforce: true,
+ Type: v1pb.PolicyType_TAG,
+ Policy: &v1pb.Policy_TagPolicy{
+ TagPolicy: &v1pb.TagPolicy{
+ Tags: map[string]string{
+ "bb.tag.review_config": reviewName,
+ },
+ },
+ },
+ }, []string{"payload", "enforce"}); err != nil {
+ return diag.Errorf("failed to attach review config %s to resource %s with error: %v", reviewName, resource, err.Error())
+ }
+ }
+
+ return nil
+}
+
+func setReviewConfig(d *schema.ResourceData, review *v1pb.ReviewConfig) diag.Diagnostics {
+ reviewID, err := internal.GetReviewConfigID(review.Name)
+ if err != nil {
+ return diag.Errorf("failed to parse id from review name %s with error: %v", review.Name, err.Error())
+ }
+
+ if err := d.Set("resource_id", reviewID); err != nil {
+ return diag.Errorf("cannot set resource_id for review: %s", err.Error())
+ }
+ if err := d.Set("title", review.Title); err != nil {
+ return diag.Errorf("cannot set title for review: %s", err.Error())
+ }
+ if err := d.Set("enabled", review.Enabled); err != nil {
+ return diag.Errorf("cannot set enabled for review: %s", err.Error())
+ }
+ if err := d.Set("resources", review.Resources); err != nil {
+ return diag.Errorf("cannot set resources for review: %s", err.Error())
+ }
+ if err := d.Set("rules", schema.NewSet(reviewRuleHash, flattenReviewRules(review.Rules))); err != nil {
+ return diag.Errorf("cannot set rules for review: %s", err.Error())
+ }
+
+ return nil
+}
+
+func flattenReviewRules(rules []*v1pb.SQLReviewRule) []interface{} {
+ ruleList := []interface{}{}
+ for _, rule := range rules {
+ rawRule := map[string]interface{}{}
+ rawRule["type"] = rule.Type
+ rawRule["engine"] = rule.Engine.String()
+ rawRule["level"] = rule.Level.String()
+ rawRule["comment"] = rule.Comment
+ rawRule["payload"] = rule.Payload
+ ruleList = append(ruleList, rawRule)
+ }
+ return ruleList
+}
+
+func reviewRuleHash(rawRule interface{}) int {
+ var buf bytes.Buffer
+ raw := rawRule.(map[string]interface{})
+
+ if v, ok := raw["type"].(string); ok {
+ _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
+ }
+ if v, ok := raw["engine"].(string); ok {
+ _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
+ }
+
+ return internal.ToHashcodeInt(buf.String())
+}
+
+func convertToV1RuleList(d *schema.ResourceData) ([]*v1pb.SQLReviewRule, error) {
+ ruleSet, ok := d.Get("rules").(*schema.Set)
+ if !ok || ruleSet.Len() == 0 {
+ return nil, errors.Errorf("rules is required")
+ }
+
+ ruleList := []*v1pb.SQLReviewRule{}
+
+ for _, r := range ruleSet.List() {
+ rawRule := r.(map[string]interface{})
+ payload := rawRule["payload"].(string)
+ if payload == "" {
+ payload = "{}"
+ }
+ ruleList = append(ruleList, &v1pb.SQLReviewRule{
+ Type: rawRule["type"].(string),
+ Comment: rawRule["comment"].(string),
+ Payload: payload,
+ Engine: v1pb.Engine(v1pb.Engine_value[rawRule["engine"].(string)]),
+ Level: v1pb.SQLReviewRuleLevel(v1pb.SQLReviewRuleLevel_value[rawRule["level"].(string)]),
+ })
+ }
+ return ruleList, nil
+}
diff --git a/provider/resource_user.go b/provider/resource_user.go
index 66b9002..2248dfa 100644
--- a/provider/resource_user.go
+++ b/provider/resource_user.go
@@ -166,11 +166,11 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, m interface
Summary: "User is deleted",
Detail: fmt.Sprintf("User %s already deleted, try to undelete the user", userName),
})
- if _, err := c.UndeleteUser(ctx, userName); err != nil {
+ if _, err := c.UndeleteUser(ctx, existedUser.Name); err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Failed to undelete user",
- Detail: fmt.Sprintf("Undelete user %s failed, error: %v", userName, err),
+ Detail: fmt.Sprintf("Undelete user %s failed, error: %v", existedUser.Name, err),
})
return diags
}
@@ -230,7 +230,11 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, m interface
d.SetId(user.Name)
}
- if err := patchWorkspaceIAMPolicy(ctx, c, email, getRoles(d)); err != nil {
+ roles, diagnostic := getRoles(d)
+ if diagnostic != nil {
+ return diagnostic
+ }
+ if err := patchWorkspaceIAMPolicy(ctx, c, fmt.Sprintf("user:%s", email), roles); err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Failed to patch user roles",
@@ -316,7 +320,11 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, m interface
}
if d.HasChange("roles") {
- if err := patchWorkspaceIAMPolicy(ctx, c, email, getRoles(d)); err != nil {
+ roles, diagnostic := getRoles(d)
+ if diagnostic != nil {
+ return diagnostic
+ }
+ if err := patchWorkspaceIAMPolicy(ctx, c, fmt.Sprintf("user:%s", email), roles); err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "Failed to patch user roles",
@@ -334,36 +342,39 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, m interface
return diags
}
-func getRoles(d *schema.ResourceData) []string {
+func getRoles(d *schema.ResourceData) ([]string, diag.Diagnostics) {
rawRoles := d.Get("roles").(*schema.Set)
roleList := []string{}
for _, rawRole := range rawRoles.List() {
+ role := rawRole.(string)
+ if !strings.HasPrefix(role, internal.RoleNamePrefix) {
+ return nil, diag.Errorf("role must in the roles/{id} format")
+ }
roleList = append(roleList, rawRole.(string))
}
- return roleList
+ return roleList, nil
}
-func patchWorkspaceIAMPolicy(ctx context.Context, client api.Client, email string, roles []string) error {
+func patchWorkspaceIAMPolicy(ctx context.Context, client api.Client, member string, roles []string) error {
workspaceIamPolicy, err := client.GetWorkspaceIAMPolicy(ctx)
if err != nil {
return errors.Errorf("cannot get workspace IAM with error: %s", err.Error())
}
- patchMember := fmt.Sprintf("user:%s", email)
roleMap := map[string]bool{}
for _, role := range roles {
roleMap[role] = true
}
for _, binding := range workspaceIamPolicy.Bindings {
- index := slices.Index(binding.Members, patchMember)
+ index := slices.Index(binding.Members, member)
if !roleMap[binding.Role] {
if index >= 0 {
binding.Members = slices.Delete(binding.Members, index, index+1)
}
} else {
if index < 0 {
- binding.Members = append(binding.Members, patchMember)
+ binding.Members = append(binding.Members, member)
}
}
@@ -374,7 +385,7 @@ func patchWorkspaceIAMPolicy(ctx context.Context, client api.Client, email strin
workspaceIamPolicy.Bindings = append(workspaceIamPolicy.Bindings, &v1pb.Binding{
Role: role,
Members: []string{
- patchMember,
+ member,
},
})
}