diff --git a/api/client.go b/api/client.go index edfe03c..61c2b23 100644 --- a/api/client.go +++ b/api/client.go @@ -191,4 +191,15 @@ type Client interface { UpdateRisk(ctx context.Context, patch *v1pb.Risk, updateMasks []string) (*v1pb.Risk, error) // DeleteRisk deletes the risk by name. DeleteRisk(ctx context.Context, name string) error + + // ListDatabaseGroup list all database groups in a project. + ListDatabaseGroup(ctx context.Context, project string) (*v1pb.ListDatabaseGroupsResponse, error) + // CreateDatabaseGroup creates the database group. + CreateDatabaseGroup(ctx context.Context, project, groupID string, group *v1pb.DatabaseGroup) (*v1pb.DatabaseGroup, error) + // GetDatabaseGroup gets the database group by name. + GetDatabaseGroup(ctx context.Context, name string, view v1pb.DatabaseGroupView) (*v1pb.DatabaseGroup, error) + // UpdateDatabaseGroup updates the database group. + UpdateDatabaseGroup(ctx context.Context, patch *v1pb.DatabaseGroup, updateMasks []string) (*v1pb.DatabaseGroup, error) + // DeleteDatabaseGroup deletes the database group by name. + DeleteDatabaseGroup(ctx context.Context, name string) error } diff --git a/client/common.go b/client/common.go index 32afcdc..b86b43f 100644 --- a/client/common.go +++ b/client/common.go @@ -63,8 +63,8 @@ func (c *client) updateResource(ctx context.Context, name string, patch protoref } // getResource gets the resource by name. -func (c *client) getResource(ctx context.Context, name string) ([]byte, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", c.url, c.version, url.QueryEscape(name)), nil) +func (c *client) getResource(ctx context.Context, name, query string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s?%s", c.url, c.version, url.QueryEscape(name), query), nil) if err != nil { return nil, err } diff --git a/client/database.go b/client/database.go index 57720b0..5b96275 100644 --- a/client/database.go +++ b/client/database.go @@ -17,7 +17,7 @@ import ( // GetDatabase gets the database by the database full name. func (c *client) GetDatabase(ctx context.Context, databaseName string) (*v1pb.Database, error) { - body, err := c.getResource(ctx, databaseName) + body, err := c.getResource(ctx, databaseName, "") if err != nil { return nil, err } @@ -177,7 +177,7 @@ func (c *client) BatchUpdateDatabases(ctx context.Context, request *v1pb.BatchUp // GetDatabaseCatalog gets the database catalog by the database full name. func (c *client) GetDatabaseCatalog(ctx context.Context, databaseName string) (*v1pb.DatabaseCatalog, error) { - body, err := c.getResource(ctx, fmt.Sprintf("%s/catalog", databaseName)) + body, err := c.getResource(ctx, fmt.Sprintf("%s/catalog", databaseName), "") if err != nil { return nil, err } diff --git a/client/database_group.go b/client/database_group.go new file mode 100644 index 0000000..f7b065f --- /dev/null +++ b/client/database_group.go @@ -0,0 +1,93 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strings" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + "google.golang.org/protobuf/encoding/protojson" +) + +// ListDatabaseGroup list all database groups in a project. +func (c *client) ListDatabaseGroup(ctx context.Context, project string) (*v1pb.ListDatabaseGroupsResponse, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s/databaseGroups", c.url, c.version, project), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var res v1pb.ListDatabaseGroupsResponse + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// CreateDatabaseGroup creates the database group. +func (c *client) CreateDatabaseGroup(ctx context.Context, project, groupID string, group *v1pb.DatabaseGroup) (*v1pb.DatabaseGroup, error) { + payload, err := protojson.Marshal(group) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s/databaseGroups?databaseGroupId=%s", c.url, c.version, project, groupID), strings.NewReader(string(payload))) + + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var res v1pb.DatabaseGroup + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// GetDatabaseGroup gets the database group by name. +func (c *client) GetDatabaseGroup(ctx context.Context, name string, view v1pb.DatabaseGroupView) (*v1pb.DatabaseGroup, error) { + // TODO(ed): query + body, err := c.getResource(ctx, name, fmt.Sprintf("view=%s", view.String())) + if err != nil { + return nil, err + } + + var res v1pb.DatabaseGroup + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// UpdateDatabaseGroup updates the database group. +func (c *client) UpdateDatabaseGroup(ctx context.Context, patch *v1pb.DatabaseGroup, updateMasks []string) (*v1pb.DatabaseGroup, error) { + body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) + if err != nil { + return nil, err + } + + var res v1pb.DatabaseGroup + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// DeleteDatabaseGroup deletes the database group by name. +func (c *client) DeleteDatabaseGroup(ctx context.Context, name string) error { + return c.deleteResource(ctx, name) +} diff --git a/client/group.go b/client/group.go index 601ba24..25b3d4c 100644 --- a/client/group.go +++ b/client/group.go @@ -59,7 +59,7 @@ func (c *client) CreateGroup(ctx context.Context, email string, group *v1pb.Grou // GetGroup gets the group by name. func (c *client) GetGroup(ctx context.Context, name string) (*v1pb.Group, error) { - body, err := c.getResource(ctx, name) + body, err := c.getResource(ctx, name, "") if err != nil { return nil, err } diff --git a/client/instance.go b/client/instance.go index 013cf6a..e455340 100644 --- a/client/instance.go +++ b/client/instance.go @@ -116,7 +116,7 @@ func (c *client) listInstancePerPage(ctx context.Context, query, pageToken strin // GetInstance gets the instance by full name. func (c *client) GetInstance(ctx context.Context, instanceName string) (*v1pb.Instance, error) { - body, err := c.getResource(ctx, instanceName) + body, err := c.getResource(ctx, instanceName, "") if err != nil { return nil, err } diff --git a/client/policy.go b/client/policy.go index a2e8fcc..4a8a18f 100644 --- a/client/policy.go +++ b/client/policy.go @@ -36,7 +36,7 @@ func (c *client) ListPolicies(ctx context.Context, parent string) (*v1pb.ListPol // GetPolicy gets a policy in a specific resource. func (c *client) GetPolicy(ctx context.Context, policyName string) (*v1pb.Policy, error) { - body, err := c.getResource(ctx, policyName) + body, err := c.getResource(ctx, policyName, "") if err != nil { return nil, err } diff --git a/client/project.go b/client/project.go index 766e0cf..5c13a96 100644 --- a/client/project.go +++ b/client/project.go @@ -17,7 +17,7 @@ import ( // GetProject gets the project by project full name. func (c *client) GetProject(ctx context.Context, projectName string) (*v1pb.Project, error) { - body, err := c.getResource(ctx, projectName) + body, err := c.getResource(ctx, projectName, "") if err != nil { return nil, err } @@ -32,7 +32,7 @@ func (c *client) GetProject(ctx context.Context, projectName string) (*v1pb.Proj // GetProjectIAMPolicy gets the project IAM policy by project full name. func (c *client) GetProjectIAMPolicy(ctx context.Context, projectName string) (*v1pb.IamPolicy, error) { - body, err := c.getResource(ctx, fmt.Sprintf("%s:getIamPolicy", projectName)) + body, err := c.getResource(ctx, fmt.Sprintf("%s:getIamPolicy", projectName), "") if err != nil { return nil, err } diff --git a/client/review_config.go b/client/review_config.go index 4fcbe85..991f51a 100644 --- a/client/review_config.go +++ b/client/review_config.go @@ -30,7 +30,7 @@ func (c *client) ListReviewConfig(ctx context.Context) (*v1pb.ListReviewConfigsR // 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) + body, err := c.getResource(ctx, reviewName, "") if err != nil { return nil, err } diff --git a/client/risk.go b/client/risk.go index bf04b41..8370479 100644 --- a/client/risk.go +++ b/client/risk.go @@ -32,7 +32,7 @@ func (c *client) ListRisk(ctx context.Context) ([]*v1pb.Risk, error) { // GetRisk gets the risk by full name. func (c *client) GetRisk(ctx context.Context, name string) (*v1pb.Risk, error) { - body, err := c.getResource(ctx, name) + body, err := c.getResource(ctx, name, "") if err != nil { return nil, err } diff --git a/client/role.go b/client/role.go index 502eb8c..7a48c0a 100644 --- a/client/role.go +++ b/client/role.go @@ -12,7 +12,7 @@ import ( // GetRole gets the role by full name. func (c *client) GetRole(ctx context.Context, name string) (*v1pb.Role, error) { - body, err := c.getResource(ctx, name) + body, err := c.getResource(ctx, name, "") if err != nil { return nil, err } diff --git a/client/setting.go b/client/setting.go index a33389d..972344b 100644 --- a/client/setting.go +++ b/client/setting.go @@ -30,7 +30,7 @@ func (c *client) ListSettings(ctx context.Context) (*v1pb.ListSettingsResponse, // GetSetting gets the setting by the name. func (c *client) GetSetting(ctx context.Context, settingName string) (*v1pb.Setting, error) { - body, err := c.getResource(ctx, settingName) + body, err := c.getResource(ctx, settingName, "") if err != nil { return nil, err } diff --git a/client/user.go b/client/user.go index 3dfd61d..c0b7b31 100644 --- a/client/user.go +++ b/client/user.go @@ -136,7 +136,7 @@ func (c *client) CreateUser(ctx context.Context, user *v1pb.User) (*v1pb.User, e // GetUser gets the user by name. func (c *client) GetUser(ctx context.Context, userName string) (*v1pb.User, error) { - body, err := c.getResource(ctx, userName) + body, err := c.getResource(ctx, userName, "") if err != nil { return nil, err } diff --git a/client/workspace.go b/client/workspace.go index 989293e..3e4f8d2 100644 --- a/client/workspace.go +++ b/client/workspace.go @@ -12,7 +12,7 @@ import ( // GetWorkspaceIAMPolicy gets the workspace IAM policy. func (c *client) GetWorkspaceIAMPolicy(ctx context.Context) (*v1pb.IamPolicy, error) { - body, err := c.getResource(ctx, "workspaces/-:getIamPolicy") + body, err := c.getResource(ctx, "workspaces/-:getIamPolicy", "") if err != nil { return nil, err } diff --git a/docs/data-sources/database_group.md b/docs/data-sources/database_group.md new file mode 100644 index 0000000..8e3c0bb --- /dev/null +++ b/docs/data-sources/database_group.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_database_group Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The database group data source. +--- + +# bytebase_database_group (Data Source) + +The database group data source. + + + + +## Schema + +### Required + +- `project` (String) The project fullname in projects/{id} format. +- `resource_id` (String) The database group unique resource id. + +### Read-Only + +- `condition` (String) The database group condition. +- `id` (String) The ID of this resource. +- `matched_databases` (Set of String) The matched databases in the group. +- `title` (String) The database group title. + + diff --git a/docs/data-sources/database_group_list.md b/docs/data-sources/database_group_list.md new file mode 100644 index 0000000..7f7a1a1 --- /dev/null +++ b/docs/data-sources/database_group_list.md @@ -0,0 +1,36 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_database_group_list Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The database group data source list. +--- + +# bytebase_database_group_list (Data Source) + +The database group data source list. + + + + +## Schema + +### Required + +- `project` (String) The project fullname in projects/{id} format. + +### Read-Only + +- `database_groups` (List of Object) (see [below for nested schema](#nestedatt--database_groups)) +- `id` (String) The ID of this resource. + + +### Nested Schema for `database_groups` + +Read-Only: + +- `condition` (String) +- `name` (String) +- `title` (String) + + diff --git a/docs/resources/database_group.md b/docs/resources/database_group.md new file mode 100644 index 0000000..2ff6bb8 --- /dev/null +++ b/docs/resources/database_group.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_database_group Resource - terraform-provider-bytebase" +subcategory: "" +description: |- + The database group resource. +--- + +# bytebase_database_group (Resource) + +The database group resource. + + + + +## Schema + +### Required + +- `condition` (String) The database group condition. Check the proto message https://github.com/bytebase/bytebase/blob/main/proto/v1/v1/database_group_service.proto#L185 for details. +- `project` (String) The project fullname in projects/{id} format. +- `resource_id` (String) The database group unique resource id. +- `title` (String) The database group title. + +### Read-Only + +- `id` (String) The ID of this resource. +- `matched_databases` (Set of String) The matched databases in the group. + + diff --git a/docs/resources/risk.md b/docs/resources/risk.md index 0cffc5f..f18c4a1 100644 --- a/docs/resources/risk.md +++ b/docs/resources/risk.md @@ -17,7 +17,7 @@ The risk resource. Require ENTERPRISE subscription. Check the docs https://www.b ### Required -- `condition` (String) The risk condition. +- `condition` (String) The risk condition. Check the proto message https://github.com/bytebase/bytebase/blob/main/proto/v1/v1/risk_service.proto#L210 for details. - `level` (Number) The risk level. - `source` (String) The risk source. - `title` (String) The risk title. diff --git a/examples/database_group/main.tf b/examples/database_group/main.tf new file mode 100644 index 0000000..823febe --- /dev/null +++ b/examples/database_group/main.tf @@ -0,0 +1,51 @@ +terraform { + required_providers { + bytebase = { + version = "3.6.0" + # 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_project" "sample_project" { + resource_id = "sample-project" +} + +data "bytebase_database_group_list" "all" { + depends_on = [data.bytebase_project.sample_project] + project = data.bytebase_project.sample_project.name +} + +output "database_group_list" { + value = data.bytebase_database_group_list.all +} + +resource "bytebase_database_group" "databases_in_test" { + depends_on = [data.bytebase_project.sample_project] + + resource_id = "databases-in-test" + project = data.bytebase_project.sample_project.name + title = "Databases in test env" + condition = "resource.environment_name == \"test\"" +} + +data "bytebase_database_group" "databases_in_test" { + depends_on = [bytebase_database_group.databases_in_test] + + resource_id = bytebase_database_group.databases_in_test.resource_id + project = data.bytebase_project.sample_project.name +} + +output "database_group" { + value = data.bytebase_database_group.databases_in_test +} diff --git a/examples/setup/database_group.tf b/examples/setup/database_group.tf new file mode 100644 index 0000000..479d08d --- /dev/null +++ b/examples/setup/database_group.tf @@ -0,0 +1,8 @@ +resource "bytebase_database_group" "databases_in_test" { + depends_on = [bytebase_project.sample_project] + + resource_id = "databases-in-test" + project = bytebase_project.sample_project.name + title = "Databases in test env" + condition = "resource.environment_name == \"test\"" +} diff --git a/go.mod b/go.mod index 1f2d91d..9660c6a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/bytebase/terraform-provider-bytebase go 1.24.2 require ( - github.com/bytebase/bytebase v0.0.0-20250513033606-5479107aeeb3 + github.com/bytebase/bytebase v0.0.0-20250519144720-22dc39c1fd08 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-docs v0.13.0 github.com/hashicorp/terraform-plugin-log v0.7.0 diff --git a/go.sum b/go.sum index 8126d3e..a2c5f0d 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bytebase/bytebase v0.0.0-20250513033606-5479107aeeb3 h1:D60aM1+dFti+VTqhmjt6zDXSwStc99OUAUowmfFY5uI= -github.com/bytebase/bytebase v0.0.0-20250513033606-5479107aeeb3/go.mod h1:HHRGRkJYb2FKv2Iyl1feFzaadoXc14t2iMk4z+Xc/Kg= +github.com/bytebase/bytebase v0.0.0-20250519144720-22dc39c1fd08 h1:CEU+1Waq1qiJFC2LJYT/VxN3XhFyXbS9DC2gUnqILd4= +github.com/bytebase/bytebase v0.0.0-20250519144720-22dc39c1fd08/go.mod h1:HHRGRkJYb2FKv2Iyl1feFzaadoXc14t2iMk4z+Xc/Kg= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/provider/data_source_database_group.go b/provider/data_source_database_group.go new file mode 100644 index 0000000..88c351d --- /dev/null +++ b/provider/data_source_database_group.go @@ -0,0 +1,72 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func dataSourceDatabaseGroup() *schema.Resource { + return &schema.Resource{ + Description: "The database group data source.", + ReadContext: dataSourceDatabaseGroupRead, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: internal.ResourceIDValidation, + Description: "The database group unique resource id.", + }, + "project": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: internal.ResourceNameValidation( + regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern)), + ), + Description: "The project fullname in projects/{id} format.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The database group title.", + }, + "condition": { + Type: schema.TypeString, + Computed: true, + Description: "The database group condition.", + }, + "matched_databases": { + Type: schema.TypeSet, + Computed: true, + Description: "The matched databases in the group.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func dataSourceDatabaseGroupRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + groupID := d.Get("resource_id").(string) + projectName := d.Get("project").(string) + groupName := fmt.Sprintf("%s/%s%s", projectName, internal.DatabaseGroupNamePrefix, groupID) + + group, err := c.GetDatabaseGroup(ctx, groupName, v1pb.DatabaseGroupView_DATABASE_GROUP_VIEW_FULL) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(group.Name) + + return setDatabaseGroup(d, group) +} diff --git a/provider/data_source_database_group_list.go b/provider/data_source_database_group_list.go new file mode 100644 index 0000000..789d7e6 --- /dev/null +++ b/provider/data_source_database_group_list.go @@ -0,0 +1,86 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + "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 dataSourceDatabaseGroupList() *schema.Resource { + return &schema.Resource{ + Description: "The database group data source list.", + ReadContext: dataSourceDatabaseGroupListRead, + Schema: map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: internal.ResourceNameValidation( + regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern)), + ), + Description: "The project fullname in projects/{id} format.", + }, + "database_groups": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The database group fullname in projects/{id}/databaseGroups/{id} format.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The database group title.", + }, + "condition": { + Type: schema.TypeString, + Computed: true, + Description: "The database group condition.", + }, + }, + }, + }, + }, + } +} + +func dataSourceDatabaseGroupListRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + projectName := d.Get("project").(string) + + // Warning or errors can be collected in a slice type + var diags diag.Diagnostics + + response, err := c.ListDatabaseGroup(ctx, projectName) + if err != nil { + return diag.FromErr(err) + } + + groups := []map[string]interface{}{} + for _, group := range response.DatabaseGroups { + raw := make(map[string]interface{}) + raw["name"] = group.Name + raw["title"] = group.Title + raw["condition"] = group.DatabaseExpr.Expression + groups = append(groups, raw) + } + + if err := d.Set("database_groups", groups); err != nil { + return diag.FromErr(err) + } + + // always refresh + d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) + + return diags +} diff --git a/provider/internal/mock_client.go b/provider/internal/mock_client.go index 94bf988..7899905 100644 --- a/provider/internal/mock_client.go +++ b/provider/internal/mock_client.go @@ -727,3 +727,28 @@ func (*mockClient) UpdateRisk(_ context.Context, _ *v1pb.Risk, _ []string) (*v1p func (*mockClient) DeleteRisk(_ context.Context, _ string) error { return nil } + +// ListDatabaseGroup list all database groups in a project. +func (*mockClient) ListDatabaseGroup(_ context.Context, _ string) (*v1pb.ListDatabaseGroupsResponse, error) { + return &v1pb.ListDatabaseGroupsResponse{}, nil +} + +// GetDatabaseGroup gets the database group by name. +func (*mockClient) GetDatabaseGroup(_ context.Context, _ string, _ v1pb.DatabaseGroupView) (*v1pb.DatabaseGroup, error) { + return &v1pb.DatabaseGroup{}, nil +} + +// CreateDatabaseGroup creates the database group. +func (*mockClient) CreateDatabaseGroup(_ context.Context, _, _ string, _ *v1pb.DatabaseGroup) (*v1pb.DatabaseGroup, error) { + return &v1pb.DatabaseGroup{}, nil +} + +// UpdateDatabaseGroup updates the database group. +func (*mockClient) UpdateDatabaseGroup(_ context.Context, _ *v1pb.DatabaseGroup, _ []string) (*v1pb.DatabaseGroup, error) { + return &v1pb.DatabaseGroup{}, nil +} + +// DeleteDatabaseGroup deletes the database group by name. +func (*mockClient) DeleteDatabaseGroup(_ context.Context, _ string) error { + return nil +} diff --git a/provider/internal/utils.go b/provider/internal/utils.go index 0df7d06..5c01fc6 100644 --- a/provider/internal/utils.go +++ b/provider/internal/utils.go @@ -27,16 +27,14 @@ const ( PolicyNamePrefix = "policies/" // SettingNamePrefix is the prefix for setting unique name. SettingNamePrefix = "settings/" - // VCSProviderNamePrefix is the prefix for vcs provider unique name. - VCSProviderNamePrefix = "vcsProviders/" - // VCSConnectorNamePrefix is the prefix for vcs connector unique name. - VCSConnectorNamePrefix = "vcsConnectors/" // UserNamePrefix is the prefix for user name. UserNamePrefix = "users/" // GroupNamePrefix is the prefix for group name. GroupNamePrefix = "groups/" // RoleNamePrefix is the prefix for role name. RoleNamePrefix = "roles/" + // DatabaseGroupNamePrefix is the prefix for database group name. + DatabaseGroupNamePrefix = "databaseGroups/" // ReviewConfigNamePrefix is the prefix for the review config name. ReviewConfigNamePrefix = "reviewConfigs/" // DatabaseCatalogNameSuffix is the suffix for the database catalog name. @@ -135,26 +133,6 @@ func GetEnvironmentID(name string) (string, error) { return tokens[0], nil } -// GetVCSProviderID will parse the vcs provider resource id. -func GetVCSProviderID(name string) (string, error) { - // the vcs provider name should be vcsProviders/{resource-id} - tokens, err := getNameParentTokens(name, VCSProviderNamePrefix) - if err != nil { - return "", err - } - return tokens[0], nil -} - -// GetVCSConnectorID will parse the vcs connector resource id. -func GetVCSConnectorID(name string) (string, string, error) { - // the vcs connector name should be projects/{project}/vcsConnectors/{resource-id} - tokens, err := getNameParentTokens(name, ProjectNamePrefix, VCSConnectorNamePrefix) - if err != nil { - return "", "", err - } - return tokens[0], tokens[1], nil -} - // GetInstanceID will parse the environment resource id and instance resource id. func GetInstanceID(name string) (string, error) { // the instance request should be instances/{instance-id} @@ -211,6 +189,16 @@ func GetInstanceDatabaseID(name string) (string, string, error) { return tokens[0], tokens[1], nil } +// GetProjectDatabaseGroupID will parse the project resource id and database group resource id. +func GetProjectDatabaseGroupID(name string) (string, string, error) { + // the instance request should be projects/{id}/databaseGroups/{id} + tokens, err := getNameParentTokens(name, ProjectNamePrefix, DatabaseGroupNamePrefix) + if err != nil { + return "", "", err + } + return tokens[0], tokens[1], nil +} + func getNameParentTokens(name string, tokenPrefixes ...string) ([]string, error) { parts := strings.Split(name, "/") if len(parts) != 2*len(tokenPrefixes) { diff --git a/provider/provider.go b/provider/provider.go index bb556e1..54b7838 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -49,41 +49,44 @@ func NewProvider() *schema.Provider { }, ConfigureContextFunc: providerConfigure, DataSourcesMap: map[string]*schema.Resource{ - "bytebase_instance": dataSourceInstance(), - "bytebase_instance_list": dataSourceInstanceList(), - "bytebase_policy": dataSourcePolicy(), - "bytebase_policy_list": dataSourcePolicyList(), - "bytebase_project": dataSourceProject(), - "bytebase_project_list": dataSourceProjectList(), - "bytebase_setting": dataSourceSetting(), - "bytebase_user": dataSourceUser(), - "bytebase_user_list": dataSourceUserList(), - "bytebase_role": dataSourceRole(), - "bytebase_role_list": dataSourceRoleList(), - "bytebase_group": dataSourceGroup(), - "bytebase_group_list": dataSourceGroupList(), - "bytebase_database": dataSourceDatabase(), - "bytebase_database_list": dataSourceDatabaseList(), - "bytebase_review_config": dataSourceReviewConfig(), - "bytebase_review_config_list": dataSourceReviewConfigList(), - "bytebase_iam_policy": dataSourceIAMPolicy(), - "bytebase_environment": dataSourceEnvironment(), - "bytebase_risk": dataSourceRisk(), - "bytebase_risk_list": dataSourceRiskList(), + "bytebase_instance": dataSourceInstance(), + "bytebase_instance_list": dataSourceInstanceList(), + "bytebase_policy": dataSourcePolicy(), + "bytebase_policy_list": dataSourcePolicyList(), + "bytebase_project": dataSourceProject(), + "bytebase_project_list": dataSourceProjectList(), + "bytebase_setting": dataSourceSetting(), + "bytebase_user": dataSourceUser(), + "bytebase_user_list": dataSourceUserList(), + "bytebase_role": dataSourceRole(), + "bytebase_role_list": dataSourceRoleList(), + "bytebase_group": dataSourceGroup(), + "bytebase_group_list": dataSourceGroupList(), + "bytebase_database": dataSourceDatabase(), + "bytebase_database_list": dataSourceDatabaseList(), + "bytebase_database_group": dataSourceDatabaseGroup(), + "bytebase_database_group_list": dataSourceDatabaseGroupList(), + "bytebase_review_config": dataSourceReviewConfig(), + "bytebase_review_config_list": dataSourceReviewConfigList(), + "bytebase_iam_policy": dataSourceIAMPolicy(), + "bytebase_environment": dataSourceEnvironment(), + "bytebase_risk": dataSourceRisk(), + "bytebase_risk_list": dataSourceRiskList(), }, ResourcesMap: map[string]*schema.Resource{ - "bytebase_instance": resourceInstance(), - "bytebase_policy": resourcePolicy(), - "bytebase_project": resourceProjct(), - "bytebase_setting": resourceSetting(), - "bytebase_user": resourceUser(), - "bytebase_role": resourceRole(), - "bytebase_group": resourceGroup(), - "bytebase_database": resourceDatabase(), - "bytebase_review_config": resourceReviewConfig(), - "bytebase_iam_policy": resourceIAMPolicy(), - "bytebase_environment": resourceEnvironment(), - "bytebase_risk": resourceRisk(), + "bytebase_instance": resourceInstance(), + "bytebase_policy": resourcePolicy(), + "bytebase_project": resourceProjct(), + "bytebase_setting": resourceSetting(), + "bytebase_user": resourceUser(), + "bytebase_role": resourceRole(), + "bytebase_group": resourceGroup(), + "bytebase_database": resourceDatabase(), + "bytebase_database_group": resourceDatabaseGroup(), + "bytebase_review_config": resourceReviewConfig(), + "bytebase_iam_policy": resourceIAMPolicy(), + "bytebase_environment": resourceEnvironment(), + "bytebase_risk": resourceRisk(), }, } } diff --git a/provider/resource_database_group.go b/provider/resource_database_group.go new file mode 100644 index 0000000..b336f96 --- /dev/null +++ b/provider/resource_database_group.go @@ -0,0 +1,232 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "google.golang.org/genproto/googleapis/type/expr" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func resourceDatabaseGroup() *schema.Resource { + return &schema.Resource{ + Description: "The database group resource.", + ReadContext: resourceDatabaseGroupRead, + DeleteContext: resourceDatabaseGroupDelete, + CreateContext: resourceDatabaseGroupCreate, + UpdateContext: resourceDatabaseGroupUpdate, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: internal.ResourceIDValidation, + Description: "The database group unique resource id.", + }, + "project": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: internal.ResourceNameValidation( + regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern)), + ), + Description: "The project fullname in projects/{id} format.", + }, + "title": { + Type: schema.TypeString, + Required: true, + Description: "The database group title.", + ValidateFunc: validation.StringIsNotEmpty, + }, + "condition": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The database group condition. Check the proto message https://github.com/bytebase/bytebase/blob/main/proto/v1/v1/database_group_service.proto#L185 for details.", + }, + "matched_databases": { + Type: schema.TypeSet, + Computed: true, + Description: "The matched databases in the group.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func resourceDatabaseGroupRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + fullName := d.Id() + + group, err := c.GetDatabaseGroup(ctx, fullName, v1pb.DatabaseGroupView_DATABASE_GROUP_VIEW_FULL) + if err != nil { + return diag.FromErr(err) + } + + return setDatabaseGroup(d, group) +} + +func setDatabaseGroup(d *schema.ResourceData, group *v1pb.DatabaseGroup) diag.Diagnostics { + projectID, groupID, err := internal.GetProjectDatabaseGroupID(group.Name) + if err != nil { + return diag.FromErr(err) + } + + if err := d.Set("resource_id", groupID); err != nil { + return diag.Errorf("cannot set resource_id for group: %s", err.Error()) + } + if err := d.Set("project", fmt.Sprintf("%s%s", internal.ProjectNamePrefix, projectID)); err != nil { + return diag.Errorf("cannot set resource_id for group: %s", err.Error()) + } + if err := d.Set("title", group.Title); err != nil { + return diag.Errorf("cannot set title for group: %s", err.Error()) + } + if err := d.Set("condition", group.DatabaseExpr.Expression); err != nil { + return diag.Errorf("cannot set condition for group: %s", err.Error()) + } + + matchedDatabases := []string{} + for _, db := range group.MatchedDatabases { + matchedDatabases = append(matchedDatabases, db.Name) + } + if err := d.Set("matched_databases", matchedDatabases); err != nil { + return diag.Errorf("cannot set matched_databases for group: %s", err.Error()) + } + + return nil +} + +func resourceDatabaseGroupDelete(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.DeleteDatabaseGroup(ctx, fullName); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return diags +} + +func resourceDatabaseGroupCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + groupID := d.Get("resource_id").(string) + projectName := d.Get("project").(string) + groupName := fmt.Sprintf("%s/%s%s", projectName, internal.DatabaseGroupNamePrefix, groupID) + + existedGroup, err := c.GetDatabaseGroup(ctx, groupName, v1pb.DatabaseGroupView_DATABASE_GROUP_VIEW_BASIC) + if err != nil { + tflog.Debug(ctx, fmt.Sprintf("get group %s failed with error: %v", groupName, err)) + } + + databaseGroup := &v1pb.DatabaseGroup{ + Name: groupName, + Title: d.Get("title").(string), + DatabaseExpr: &expr.Expr{ + Expression: d.Get("condition").(string), + }, + } + + var diags diag.Diagnostics + if existedGroup != nil && err == nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Database group already exists", + Detail: fmt.Sprintf("Database group %s already exists, try to exec the update operation", groupName), + }) + + updated, err := c.UpdateDatabaseGroup(ctx, databaseGroup, []string{"title", "database_expr"}) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update database group", + Detail: fmt.Sprintf("Update database group %s failed, error: %v", groupName, err), + }) + return diags + } + existedGroup = updated + } else { + created, err := c.CreateDatabaseGroup(ctx, projectName, groupID, databaseGroup) + if err != nil { + return diag.FromErr(err) + } + existedGroup = created + } + + d.SetId(existedGroup.Name) + + diag := setDatabaseGroup(d, existedGroup) + if diag != nil { + diags = append(diags, diag...) + } + + return diags +} + +func resourceDatabaseGroupUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + if d.HasChange("resource_id") { + return diag.Errorf("cannot change the resource id") + } + if d.HasChange("project") { + return diag.Errorf("cannot change the project") + } + + c := m.(api.Client) + groupName := d.Id() + + databaseGroup := &v1pb.DatabaseGroup{ + Name: groupName, + Title: d.Get("title").(string), + DatabaseExpr: &expr.Expr{ + Expression: d.Get("condition").(string), + }, + } + + updateMasks := []string{} + if d.HasChange("title") { + updateMasks = append(updateMasks, "title") + } + if d.HasChange("condition") { + updateMasks = append(updateMasks, "database_expr") + } + + if len(updateMasks) == 0 { + return nil + } + + var diags diag.Diagnostics + updated, err := c.UpdateDatabaseGroup(ctx, databaseGroup, updateMasks) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update database group", + Detail: fmt.Sprintf("Update database group %s failed, error: %v", groupName, err), + }) + return diags + } + databaseGroup = updated + + diag := setDatabaseGroup(d, databaseGroup) + if diag != nil { + diags = append(diags, diag...) + } + + return diags +} diff --git a/provider/resource_risk.go b/provider/resource_risk.go index fd9230a..7d5788b 100644 --- a/provider/resource_risk.go +++ b/provider/resource_risk.go @@ -66,7 +66,7 @@ func resourceRisk() *schema.Resource { "condition": { Type: schema.TypeString, Required: true, - Description: "The risk condition.", + Description: "The risk condition. Check the proto message https://github.com/bytebase/bytebase/blob/main/proto/v1/v1/risk_service.proto#L210 for details.", }, }, }