From 69254dcdd45711a14ece09e717227d9af0f716b2 Mon Sep 17 00:00:00 2001 From: ecmadao Date: Mon, 25 Aug 2025 11:34:35 +0800 Subject: [PATCH 1/5] chore: dependency update --- README.md | 2 +- api/client.go | 33 ++- client/auth.go | 59 ++-- client/cel.go | 33 +-- client/client.go | 117 ++++---- client/common.go | 77 ------ client/database.go | 156 +++++------ client/database_group.go | 102 ++++--- client/group.go | 146 +++++++--- client/instance.go | 178 ++++++------ client/policy.go | 76 ++--- client/project.go | 308 ++++++++++----------- client/review_config.go | 71 +++-- client/risk.go | 94 ++++--- client/role.go | 96 ++++--- client/setting.go | 59 ++-- client/user.go | 160 ++++++----- client/workspace.go | 44 ++- docs/data-sources/group_list.md | 5 + docs/data-sources/policy.md | 2 +- docs/data-sources/project.md | 2 +- docs/data-sources/project_list.md | 2 +- docs/data-sources/review_config.md | 2 +- docs/data-sources/review_config_list.md | 2 +- docs/data-sources/setting.md | 13 +- docs/resources/group.md | 2 +- docs/resources/instance.md | 12 +- docs/resources/policy.md | 2 +- docs/resources/project.md | 2 +- docs/resources/review_config.md | 2 +- docs/resources/setting.md | 13 +- examples/groups/main.tf | 25 +- examples/setup/environment.tf | 65 +++-- examples/setup/iam.tf | 10 +- examples/setup/instance.tf | 32 ++- examples/setup/users.tf | 29 +- go.mod | 9 +- go.sum | 59 ++-- provider/data_source_database.go | 71 +---- provider/data_source_database_group.go | 2 +- provider/data_source_database_list.go | 6 +- provider/data_source_group.go | 16 +- provider/data_source_group_list.go | 22 +- provider/data_source_iam_policy.go | 74 ++--- provider/data_source_instance_list.go | 6 +- provider/data_source_policy.go | 11 +- provider/data_source_policy_list.go | 2 +- provider/data_source_policy_list_test.go | 2 +- provider/data_source_policy_test.go | 2 +- provider/data_source_project.go | 26 +- provider/data_source_project_list.go | 4 +- provider/data_source_review_config.go | 3 +- provider/data_source_review_config_list.go | 5 +- provider/data_source_setting.go | 62 +++-- provider/data_source_user.go | 2 +- provider/data_source_user_list.go | 2 +- provider/internal/mock_client.go | 83 +----- provider/internal/resource.go | 42 +-- provider/internal/utils.go | 26 +- provider/provider.go | 4 +- provider/resource_database.go | 148 ++++++---- provider/resource_database_group.go | 18 +- provider/resource_environment.go | 2 +- provider/resource_group.go | 44 ++- provider/resource_iam_policy.go | 144 ++++++---- provider/resource_instance.go | 238 ++++++++-------- provider/resource_instance_test.go | 2 +- provider/resource_policy.go | 133 +++++---- provider/resource_policy_test.go | 2 +- provider/resource_project.go | 56 ++-- provider/resource_project_test.go | 2 +- provider/resource_review_config.go | 56 ++-- provider/resource_risk.go | 19 +- provider/resource_role.go | 18 +- provider/resource_setting.go | 227 ++++++++------- provider/resource_user.go | 18 +- 76 files changed, 1946 insertions(+), 1755 deletions(-) delete mode 100644 client/common.go diff --git a/README.md b/README.md index 4a06d92..b0c0281 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ using Terraform Bytebase Provider to prepare those instances ready for applicati - [Go](https://golang.org/doc/install) (1.19 or later) - [Terraform](https://developer.hashicorp.com/terraform/downloads?product_intent=terraform) (1.3.5 or later) -- [Bytebase](https://github.com/bytebase/bytebase) (3.8.0 or later) +- [Bytebase](https://github.com/bytebase/bytebase) (3.9.2 or later) > If you have problems running `terraform` in MacOS with Apple Silicon, you can following https://stackoverflow.com/questions/66281882/how-can-i-get-terraform-init-to-run-on-my-apple-silicon-macbook-pro-for-the-go and use the `tfenv`. diff --git a/api/client.go b/api/client.go index b2224bb..a76c335 100644 --- a/api/client.go +++ b/api/client.go @@ -3,7 +3,7 @@ package api import ( "context" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" v1alpha1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) @@ -51,15 +51,14 @@ type UserFilter struct { State v1pb.State } +// GroupFilter is the filter for list group API. +type GroupFilter struct { + Query string + Project string +} + // Client is the API message for Bytebase OpenAPI client. type Client interface { - // GetCaller returns the API caller. - GetCaller() *v1pb.User - // CheckResourceExist check if the resource exists. - CheckResourceExist(ctx context.Context, name string) error - // DeleteResource force delete the resource by name. - DeleteResource(ctx context.Context, name string) error - // Instance // ListInstance will return instances. ListInstance(ctx context.Context, filter *InstanceFilter) ([]*v1pb.Instance, error) @@ -71,6 +70,8 @@ type Client interface { UpdateInstance(ctx context.Context, patch *v1pb.Instance, updateMasks []string) (*v1pb.Instance, error) // UndeleteInstance undeletes the instance. UndeleteInstance(ctx context.Context, instanceName string) (*v1pb.Instance, error) + // DeleteInstance deletes the instance. + DeleteInstance(ctx context.Context, instanceName string) error // SyncInstanceSchema will trigger the schema sync for an instance. SyncInstanceSchema(ctx context.Context, instanceName string) error @@ -109,6 +110,8 @@ type Client interface { UpdateProject(ctx context.Context, patch *v1pb.Project, updateMask []string) (*v1pb.Project, error) // UndeleteProject undeletes the project. UndeleteProject(ctx context.Context, projectName string) (*v1pb.Project, error) + // DeleteProject deletes the project. + DeleteProject(ctx context.Context, projectName string) error // GetProjectIAMPolicy gets the project IAM policy by project full name. GetProjectIAMPolicy(ctx context.Context, projectName string) (*v1pb.IamPolicy, error) // SetProjectIAMPolicy sets the project IAM policy. @@ -143,6 +146,8 @@ type Client interface { UpdateUser(ctx context.Context, patch *v1pb.User, updateMasks []string) (*v1pb.User, error) // UndeleteUser undeletes the user by name. UndeleteUser(ctx context.Context, userName string) (*v1pb.User, error) + // DeleteUser deletes the user. + DeleteUser(ctx context.Context, userName string) error // Role // ListRole will returns all roles. @@ -153,16 +158,20 @@ type Client interface { GetRole(ctx context.Context, name string) (*v1pb.Role, error) // UpdateRole updates the role. UpdateRole(ctx context.Context, patch *v1pb.Role, updateMasks []string) (*v1pb.Role, error) + // DeleteRole deletes the role. + DeleteRole(ctx context.Context, roleName string) error // Group // ListGroup list all groups. - ListGroup(ctx context.Context) (*v1pb.ListGroupsResponse, error) + ListGroup(ctx context.Context, filter *GroupFilter) ([]*v1pb.Group, error) // CreateGroup creates the group. CreateGroup(ctx context.Context, email string, group *v1pb.Group) (*v1pb.Group, error) // GetGroup gets the group by name. GetGroup(ctx context.Context, name string) (*v1pb.Group, error) // UpdateGroup updates the group. UpdateGroup(ctx context.Context, patch *v1pb.Group, updateMasks []string) (*v1pb.Group, error) + // DeleteGroup deletes the group. + DeleteGroup(ctx context.Context, groupName string) error // Workspace // GetWorkspaceIAMPolicy gets the workspace IAM policy. @@ -177,6 +186,8 @@ type Client interface { 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, reviewConfigName string) error // Risk // ListRisk lists the risk. @@ -187,6 +198,8 @@ type Client interface { CreateRisk(ctx context.Context, risk *v1pb.Risk) (*v1pb.Risk, error) // UpdateRisk updates the risk. UpdateRisk(ctx context.Context, patch *v1pb.Risk, updateMasks []string) (*v1pb.Risk, error) + // DeleteRisk deletes the risk. + DeleteRisk(ctx context.Context, riskName string) error // ListDatabaseGroup list all database groups in a project. ListDatabaseGroup(ctx context.Context, project string) (*v1pb.ListDatabaseGroupsResponse, error) @@ -196,4 +209,6 @@ type Client interface { 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. + DeleteDatabaseGroup(ctx context.Context, databaseGroupName string) error } diff --git a/client/auth.go b/client/auth.go index cbf44ba..1ee1be2 100644 --- a/client/auth.go +++ b/client/auth.go @@ -1,40 +1,41 @@ package client import ( + "context" "fmt" - "net/http" - "strings" - "github.com/pkg/errors" - "google.golang.org/protobuf/encoding/protojson" - - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + "connectrpc.com/connect" ) -// Login will login the user and get the response. -func (c *client) login(request *v1pb.LoginRequest) (*v1pb.LoginResponse, error) { - if request.Email == "" || request.Password == "" { - return nil, errors.Errorf("undefined login service account or key") - } - rb, err := protojson.Marshal(request) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s/auth/login", c.url, c.version), strings.NewReader(string(rb))) - if err != nil { - return nil, err - } +// Note: The login method has been moved to client.go and now uses Connect RPC. +// This file is kept for backward compatibility but the implementation +// has been migrated to use the AuthServiceClient from Connect RPC. +// authInterceptor implements connect.Interceptor to add authentication headers +type authInterceptor struct { + token string +} - body, err := c.doRequest(req) - if err != nil { - return nil, errors.Wrapf(err, "failed to login") - } +func (a *authInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return connect.UnaryFunc(func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + if req.Spec().IsClient && a.token != "" { + req.Header().Set("Authorization", fmt.Sprintf("Bearer %s", a.token)) + } + return next(ctx, req) + }) +} - ar := v1pb.LoginResponse{} - if err := ProtojsonUnmarshaler.Unmarshal(body, &ar); err != nil { - return nil, err - } +func (a *authInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { + return connect.StreamingClientFunc(func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { + conn := next(ctx, spec) + if a.token != "" { + conn.RequestHeader().Set("Authorization", fmt.Sprintf("Bearer %s", a.token)) + } + return conn + }) +} - return &ar, nil +func (*authInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return connect.StreamingHandlerFunc(func(ctx context.Context, conn connect.StreamingHandlerConn) error { + return next(ctx, conn) + }) } diff --git a/client/cel.go b/client/cel.go index 2315e7b..2270e89 100644 --- a/client/cel.go +++ b/client/cel.go @@ -3,42 +3,31 @@ package client import ( "context" "fmt" - "net/http" - "strings" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" "github.com/pkg/errors" v1alpha1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" - "google.golang.org/protobuf/encoding/protojson" ) -// ParseExpression parse the expression string. +// ParseExpression parse the expression string using Connect RPC. func (c *client) ParseExpression(ctx context.Context, expression string) (*v1alpha1.Expr, error) { - payload, err := protojson.Marshal(&v1pb.BatchParseRequest{ - Expressions: []string{expression}, - }) - if err != nil { - return nil, err + if c.celClient == nil { + return nil, fmt.Errorf("cel service client not initialized") } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/cel/batchParse", c.url, c.version), strings.NewReader(string(payload))) - if err != nil { - return nil, err - } + req := connect.NewRequest(&v1pb.BatchParseRequest{ + Expressions: []string{expression}, + }) - body, err := c.doRequest(req) + resp, err := c.celClient.BatchParse(ctx, req) if err != nil { return nil, err } - var res v1pb.BatchParseResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - if len(res.Expressions) != 1 { + if len(resp.Msg.Expressions) != 1 { return nil, errors.Errorf("failed to parse the cel: %v", expression) } - return res.GetExpressions()[0], nil + return resp.Msg.GetExpressions()[0], nil } diff --git a/client/client.go b/client/client.go index c465b10..70ce7f5 100644 --- a/client/client.go +++ b/client/client.go @@ -3,86 +3,91 @@ package client import ( "context" - "fmt" - "io" "net/http" + "strings" "time" "github.com/pkg/errors" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + "buf.build/gen/go/bytebase/bytebase/connectrpc/go/v1/bytebasev1connect" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" "github.com/bytebase/terraform-provider-bytebase/api" ) // client is the API message for Bytebase API client. type client struct { - url string - version string - client *http.Client - token string - caller *v1pb.User + url string + client *http.Client + + // Connect RPC clients + authClient bytebasev1connect.AuthServiceClient + workspaceClient bytebasev1connect.WorkspaceServiceClient + instanceClient bytebasev1connect.InstanceServiceClient + databaseClient bytebasev1connect.DatabaseServiceClient + databaseCatalogClient bytebasev1connect.DatabaseCatalogServiceClient + databaseGroupClient bytebasev1connect.DatabaseGroupServiceClient + projectClient bytebasev1connect.ProjectServiceClient + userClient bytebasev1connect.UserServiceClient + roleClient bytebasev1connect.RoleServiceClient + groupClient bytebasev1connect.GroupServiceClient + settingClient bytebasev1connect.SettingServiceClient + orgPolicyClient bytebasev1connect.OrgPolicyServiceClient + reviewConfigClient bytebasev1connect.ReviewConfigServiceClient + riskClient bytebasev1connect.RiskServiceClient + celClient bytebasev1connect.CelServiceClient } // NewClient returns the new Bytebase API client. -func NewClient(url, version, email, password string) (api.Client, error) { +func NewClient(url, email, password string) (api.Client, error) { c := client{ - client: &http.Client{Timeout: 10 * time.Second}, - url: url, - version: version, + url: strings.TrimSuffix(url, "/"), } - response, err := c.login(&v1pb.LoginRequest{ - Email: email, - Password: password, - }) - if err != nil { - return nil, err + // Use standard HTTP client that supports both HTTP/1.1 and HTTP/2 + c.client = &http.Client{ + Timeout: 30 * time.Second, } - c.token = response.Token - c.caller = response.User + authInt := &authInterceptor{} + interceptors := connect.WithInterceptors(authInt) - return &c, nil -} + // Create auth client without token first + // Try without WithGRPC first to see if it's a standard Connect/gRPC-Web service + c.authClient = bytebasev1connect.NewAuthServiceClient( + c.client, + c.url, + ) -func (c *client) doRequest(req *http.Request) ([]byte, error) { - if c.token != "" { - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) - } - - res, err := c.client.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() + // Login to get token + loginReq := connect.NewRequest(&v1pb.LoginRequest{ + Email: email, + Password: password, + }) - body, err := io.ReadAll(res.Body) + loginResp, err := c.authClient.Login(context.Background(), loginReq) if err != nil { - return nil, err - } - - if res.StatusCode != http.StatusOK { - return nil, errors.Errorf("status: %d, body: %s", res.StatusCode, body) + return nil, errors.Wrapf(err, "failed to login") } - return body, err -} + authInt.token = loginResp.Msg.Token + + // Initialize other clients with auth token + c.workspaceClient = bytebasev1connect.NewWorkspaceServiceClient(c.client, c.url, interceptors) + c.instanceClient = bytebasev1connect.NewInstanceServiceClient(c.client, c.url, interceptors) + c.databaseClient = bytebasev1connect.NewDatabaseServiceClient(c.client, c.url, interceptors) + c.databaseCatalogClient = bytebasev1connect.NewDatabaseCatalogServiceClient(c.client, c.url, interceptors) + c.databaseGroupClient = bytebasev1connect.NewDatabaseGroupServiceClient(c.client, c.url, interceptors) + c.projectClient = bytebasev1connect.NewProjectServiceClient(c.client, c.url, interceptors) + c.userClient = bytebasev1connect.NewUserServiceClient(c.client, c.url, interceptors) + c.roleClient = bytebasev1connect.NewRoleServiceClient(c.client, c.url, interceptors) + c.groupClient = bytebasev1connect.NewGroupServiceClient(c.client, c.url, interceptors) + c.settingClient = bytebasev1connect.NewSettingServiceClient(c.client, c.url, interceptors) + c.orgPolicyClient = bytebasev1connect.NewOrgPolicyServiceClient(c.client, c.url, interceptors) + c.reviewConfigClient = bytebasev1connect.NewReviewConfigServiceClient(c.client, c.url, interceptors) + c.riskClient = bytebasev1connect.NewRiskServiceClient(c.client, c.url, interceptors) + c.celClient = bytebasev1connect.NewCelServiceClient(c.client, c.url, interceptors) -// GetCaller returns the API caller. -func (c *client) GetCaller() *v1pb.User { - return c.caller -} - -// CheckResourceExist check if the resource exists. -func (c *client) CheckResourceExist(ctx context.Context, name string) error { - if _, err := c.getResource(ctx, name, ""); err != nil { - return err - } - return nil -} - -// DeleteResource force delete the resource by name. -func (c *client) DeleteResource(ctx context.Context, name string) error { - return c.execDelete(ctx, name) + return &c, nil } diff --git a/client/common.go b/client/common.go deleted file mode 100644 index 1762d2c..0000000 --- a/client/common.go +++ /dev/null @@ -1,77 +0,0 @@ -package client - -import ( - "context" - "fmt" - "net/http" - "net/url" - "strings" - - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/reflect/protoreflect" -) - -// ProtojsonUnmarshaler is the unmarshal for protocol. -var ProtojsonUnmarshaler = protojson.UnmarshalOptions{DiscardUnknown: true} - -// execDelete deletes the resource by name. -func (c *client) execDelete(ctx context.Context, name string) error { - req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/%s/%s?force=true", c.url, c.version, url.QueryEscape(name)), nil) - if err != nil { - return err - } - - if _, err := c.doRequest(req); err != nil { - return err - } - return nil -} - -// undeleteResource undeletes the resource by name. -func (c *client) undeleteResource(ctx context.Context, name string) ([]byte, error) { - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s:undelete", c.url, c.version, url.QueryEscape(name)), nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) - if err != nil { - return nil, err - } - - return body, nil -} - -// updateResource update the resource. -func (c *client) updateResource(ctx context.Context, name string, patch protoreflect.ProtoMessage, updateMasks []string, allowMissing bool) ([]byte, error) { - payload, err := protojson.Marshal(patch) - if err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, "PATCH", fmt.Sprintf("%s/%s/%s?update_mask=%s&allow_missing=%v", c.url, c.version, url.QueryEscape(name), strings.Join(updateMasks, ","), allowMissing), strings.NewReader(string(payload))) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) - if err != nil { - return nil, err - } - - return body, nil -} - -// getResource gets the resource by name. -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 - } - - body, err := c.doRequest(req) - if err != nil { - return nil, err - } - return body, nil -} diff --git a/client/database.go b/client/database.go index 9fbe918..53ccd91 100644 --- a/client/database.go +++ b/client/database.go @@ -3,34 +3,36 @@ package client import ( "context" "fmt" - "net/http" - "net/url" "strings" "time" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" "github.com/hashicorp/terraform-plugin-log/tflog" - "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/bytebase/terraform-provider-bytebase/api" ) -// GetDatabase gets the database by the database full name. +// GetDatabase gets the database by the database full name using Connect RPC. func (c *client) GetDatabase(ctx context.Context, databaseName string) (*v1pb.Database, error) { - body, err := c.getResource(ctx, databaseName, "") - if err != nil { - return nil, err + if c.databaseClient == nil { + return nil, fmt.Errorf("database service client not initialized") } - var res v1pb.Database - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetDatabaseRequest{ + Name: databaseName, + }) + + resp, err := c.databaseClient.GetDatabase(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -func buildDatabaseQuery(filter *api.DatabaseFilter) string { +func buildDatabaseFilter(filter *api.DatabaseFilter) string { params := []string{} if v := filter.Query; v != "" { @@ -68,29 +70,43 @@ func buildDatabaseQuery(filter *api.DatabaseFilter) string { } } - return fmt.Sprintf("filter=%s", url.QueryEscape(strings.Join(params, " && "))) + return strings.Join(params, " && ") } -// ListDatabase list all databases. +// ListDatabase list all databases using Connect RPC. func (c *client) ListDatabase(ctx context.Context, parent string, filter *api.DatabaseFilter, listAll bool) ([]*v1pb.Database, error) { + if c.databaseClient == nil { + return nil, fmt.Errorf("database service client not initialized") + } + res := []*v1pb.Database{} pageToken := "" startTime := time.Now() - query := buildDatabaseQuery(filter) + filterStr := buildDatabaseFilter(filter) for { startTimePerPage := time.Now() - resp, err := c.listDatabasePerPage(ctx, parent, query, pageToken, 500) + + req := connect.NewRequest(&v1pb.ListDatabasesRequest{ + Parent: parent, + Filter: filterStr, + PageSize: 500, + PageToken: pageToken, + }) + + resp, err := c.databaseClient.ListDatabases(ctx, req) if err != nil { return nil, err } - res = append(res, resp.Databases...) + + res = append(res, resp.Msg.Databases...) + tflog.Debug(ctx, "[list database per page]", map[string]interface{}{ - "count": len(resp.Databases), + "count": len(resp.Msg.Databases), "ms": time.Since(startTimePerPage).Milliseconds(), }) - pageToken = resp.NextPageToken + pageToken = resp.Msg.NextPageToken if pageToken == "" || !listAll { break } @@ -104,103 +120,73 @@ func (c *client) ListDatabase(ctx context.Context, parent string, filter *api.Da return res, nil } -// listDatabasePerPage list the databases. -func (c *client) listDatabasePerPage(ctx context.Context, parent, filter, pageToken string, pageSize int) (*v1pb.ListDatabasesResponse, error) { - requestURL := fmt.Sprintf( - "%s/%s/%s/databases?%s&page_size=%d&page_token=%s", - c.url, - c.version, - parent, - filter, - pageSize, - url.QueryEscape(pageToken), - ) - - req, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) - if err != nil { - return nil, err - } - - var res v1pb.ListDatabasesResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err +// UpdateDatabase patches the database using Connect RPC. +func (c *client) UpdateDatabase(ctx context.Context, patch *v1pb.Database, updateMasks []string) (*v1pb.Database, error) { + if c.databaseClient == nil { + return nil, fmt.Errorf("database service client not initialized") } - return &res, nil -} + req := connect.NewRequest(&v1pb.UpdateDatabaseRequest{ + Database: patch, + UpdateMask: &fieldmaskpb.FieldMask{Paths: updateMasks}, + }) -// UpdateDatabase patches the database. -func (c *client) UpdateDatabase(ctx context.Context, patch *v1pb.Database, updateMasks []string) (*v1pb.Database, error) { - body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) + resp, err := c.databaseClient.UpdateDatabase(ctx, req) if err != nil { return nil, err } - var res v1pb.Database - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - return &res, nil + return resp.Msg, nil } -// BatchUpdateDatabases batch updates databases. +// BatchUpdateDatabases batch updates databases using Connect RPC. func (c *client) BatchUpdateDatabases(ctx context.Context, request *v1pb.BatchUpdateDatabasesRequest) (*v1pb.BatchUpdateDatabasesResponse, error) { - requestURL := fmt.Sprintf("%s/%s/instances/-/databases:batchUpdate", c.url, c.version) - payload, err := protojson.Marshal(request) - if err != nil { - return nil, err + if c.databaseClient == nil { + return nil, fmt.Errorf("database service client not initialized") } - req, err := http.NewRequestWithContext(ctx, "POST", requestURL, strings.NewReader(string(payload))) - if err != nil { - return nil, err - } + req := connect.NewRequest(request) - body, err := c.doRequest(req) + resp, err := c.databaseClient.BatchUpdateDatabases(ctx, req) if err != nil { return nil, err } - var res v1pb.BatchUpdateDatabasesResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - return &res, nil + return resp.Msg, nil } -// GetDatabaseCatalog gets the database catalog by the database full name. +// GetDatabaseCatalog gets the database catalog by the database full name using Connect RPC. func (c *client) GetDatabaseCatalog(ctx context.Context, databaseName string) (*v1pb.DatabaseCatalog, error) { - body, err := c.getResource(ctx, fmt.Sprintf("%s/catalog", databaseName), "") - if err != nil { - return nil, err + if c.databaseCatalogClient == nil { + return nil, fmt.Errorf("database catalog service client not initialized") } - var res v1pb.DatabaseCatalog - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetDatabaseCatalogRequest{ + Name: fmt.Sprintf("%s/catalog", databaseName), + }) + + resp, err := c.databaseCatalogClient.GetDatabaseCatalog(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// UpdateDatabaseCatalog patches the database catalog. +// UpdateDatabaseCatalog patches the database catalog using Connect RPC. func (c *client) UpdateDatabaseCatalog(ctx context.Context, patch *v1pb.DatabaseCatalog) (*v1pb.DatabaseCatalog, error) { - body, err := c.updateResource(ctx, patch.Name, patch, nil, false /* allow missing = false*/) - if err != nil { - return nil, err + if c.databaseCatalogClient == nil { + return nil, fmt.Errorf("database catalog service client not initialized") } - var res v1pb.DatabaseCatalog - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.UpdateDatabaseCatalogRequest{ + Catalog: patch, + }) + + resp, err := c.databaseCatalogClient.UpdateDatabaseCatalog(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } diff --git a/client/database_group.go b/client/database_group.go index 2ee414a..46c8ce5 100644 --- a/client/database_group.go +++ b/client/database_group.go @@ -3,86 +3,98 @@ package client import ( "context" "fmt" - "net/http" - "strings" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" - "google.golang.org/protobuf/encoding/protojson" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/fieldmaskpb" ) -// ListDatabaseGroup list all database groups in a project. +// ListDatabaseGroup list all database groups in a project using Connect RPC. 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 + if c.databaseGroupClient == nil { + return nil, fmt.Errorf("database group service client not initialized") } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } + req := connect.NewRequest(&v1pb.ListDatabaseGroupsRequest{ + Parent: project, + }) - var res v1pb.ListDatabaseGroupsResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + resp, err := c.databaseGroupClient.ListDatabaseGroups(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// CreateDatabaseGroup creates the database group. +// CreateDatabaseGroup creates the database group using Connect RPC. 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 + if c.databaseGroupClient == nil { + return nil, fmt.Errorf("database group service client not initialized") } - 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 - } + req := connect.NewRequest(&v1pb.CreateDatabaseGroupRequest{ + Parent: project, + DatabaseGroupId: groupID, + DatabaseGroup: group, + }) - body, err := c.doRequest(req) + resp, err := c.databaseGroupClient.CreateDatabaseGroup(ctx, 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 + return resp.Msg, nil } -// GetDatabaseGroup gets the database group by name. +// GetDatabaseGroup gets the database group by name using Connect RPC. 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 + if c.databaseGroupClient == nil { + return nil, fmt.Errorf("database group service client not initialized") } - var res v1pb.DatabaseGroup - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetDatabaseGroupRequest{ + Name: name, + View: view, + }) + + resp, err := c.databaseGroupClient.GetDatabaseGroup(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// UpdateDatabaseGroup updates the database group. +// UpdateDatabaseGroup updates the database group using Connect RPC. 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 c.databaseGroupClient == nil { + return nil, fmt.Errorf("database group service client not initialized") + } + + req := connect.NewRequest(&v1pb.UpdateDatabaseGroupRequest{ + DatabaseGroup: patch, + UpdateMask: &fieldmaskpb.FieldMask{Paths: updateMasks}, + }) + + resp, err := c.databaseGroupClient.UpdateDatabaseGroup(ctx, req) if err != nil { return nil, err } - var res v1pb.DatabaseGroup - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err + return resp.Msg, nil +} + +// DeleteDatabaseGroup deletes the database group. +func (c *client) DeleteDatabaseGroup(ctx context.Context, databaseGroupName string) error { + if c.databaseGroupClient == nil { + return fmt.Errorf("database group service client not initialized") } - return &res, nil + req := connect.NewRequest(&v1pb.DeleteDatabaseGroupRequest{ + Name: databaseGroupName, + }) + + _, err := c.databaseGroupClient.DeleteDatabaseGroup(ctx, req) + return err } diff --git a/client/group.go b/client/group.go index 7e6cf88..fa3753a 100644 --- a/client/group.go +++ b/client/group.go @@ -3,86 +3,142 @@ package client import ( "context" "fmt" - "net/http" - "net/url" "strings" + "time" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" - "google.golang.org/protobuf/encoding/protojson" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" + "github.com/hashicorp/terraform-plugin-log/tflog" + "google.golang.org/protobuf/types/known/fieldmaskpb" + + "github.com/bytebase/terraform-provider-bytebase/api" ) -// ListGroup list all groups. -func (c *client) ListGroup(ctx context.Context) (*v1pb.ListGroupsResponse, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/groups", c.url, c.version), nil) - if err != nil { - return nil, err +func buildGroupFilter(filter *api.GroupFilter) string { + params := []string{} + + if v := filter.Query; v != "" { + params = append(params, fmt.Sprintf(`(title.matches("%s") || email.matches("%s"))`, strings.ToLower(v), strings.ToLower(v))) + } + if v := filter.Project; v != "" { + params = append(params, fmt.Sprintf(`project == "%s"`, v)) } - body, err := c.doRequest(req) - if err != nil { - return nil, err + return strings.Join(params, " && ") +} + +// ListGroup list all groups using Connect RPC. +func (c *client) ListGroup(ctx context.Context, filter *api.GroupFilter) ([]*v1pb.Group, error) { + if c.groupClient == nil { + return nil, fmt.Errorf("group service client not initialized") } - var res v1pb.ListGroupsResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err + res := []*v1pb.Group{} + pageToken := "" + startTime := time.Now() + filterStr := buildGroupFilter(filter) + + for { + startTimePerPage := time.Now() + + req := connect.NewRequest(&v1pb.ListGroupsRequest{ + PageSize: 500, + PageToken: pageToken, + Filter: filterStr, + }) + resp, err := c.groupClient.ListGroups(ctx, req) + if err != nil { + return nil, err + } + + res = append(res, resp.Msg.Groups...) + + tflog.Debug(ctx, "[list group per page]", map[string]interface{}{ + "count": len(resp.Msg.Groups), + "ms": time.Since(startTimePerPage).Milliseconds(), + }) + + pageToken = resp.Msg.NextPageToken + if pageToken == "" { + break + } } - return &res, nil + tflog.Debug(ctx, "[list group]", map[string]interface{}{ + "total": len(res), + "ms": time.Since(startTime).Milliseconds(), + }) + + return res, nil } -// CreateGroup creates the group. +// CreateGroup creates the group using Connect RPC. func (c *client) CreateGroup(ctx context.Context, email string, group *v1pb.Group) (*v1pb.Group, error) { - payload, err := protojson.Marshal(group) - if err != nil { - return nil, err + if c.groupClient == nil { + return nil, fmt.Errorf("group service client not initialized") } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/groups?groupEmail=%s", c.url, c.version, url.QueryEscape(email)), strings.NewReader(string(payload))) + req := connect.NewRequest(&v1pb.CreateGroupRequest{ + Group: group, + GroupEmail: email, + }) + resp, err := c.groupClient.CreateGroup(ctx, req) if err != nil { return nil, err } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } - - var res v1pb.Group - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - return &res, nil + return resp.Msg, nil } -// GetGroup gets the group by name. +// GetGroup gets the group by name using Connect RPC. func (c *client) GetGroup(ctx context.Context, name string) (*v1pb.Group, error) { - body, err := c.getResource(ctx, name, "") - if err != nil { - return nil, err + if c.groupClient == nil { + return nil, fmt.Errorf("group service client not initialized") } - var res v1pb.Group - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetGroupRequest{ + Name: name, + }) + + resp, err := c.groupClient.GetGroup(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// UpdateGroup updates the group. +// UpdateGroup updates the group using Connect RPC. func (c *client) UpdateGroup(ctx context.Context, patch *v1pb.Group, updateMasks []string) (*v1pb.Group, error) { - body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) + if c.groupClient == nil { + return nil, fmt.Errorf("group service client not initialized") + } + + req := connect.NewRequest(&v1pb.UpdateGroupRequest{ + Group: patch, + AllowMissing: true, + UpdateMask: &fieldmaskpb.FieldMask{Paths: updateMasks}, + }) + + resp, err := c.groupClient.UpdateGroup(ctx, req) if err != nil { return nil, err } - var res v1pb.Group - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err + return resp.Msg, nil +} + +// DeleteGroup deletes the group. +func (c *client) DeleteGroup(ctx context.Context, name string) error { + if c.groupClient == nil { + return fmt.Errorf("group service client not initialized") } - return &res, nil + req := connect.NewRequest(&v1pb.DeleteGroupRequest{ + Name: name, + }) + + _, err := c.groupClient.DeleteGroup(ctx, req) + return err } diff --git a/client/instance.go b/client/instance.go index ff7bf0d..062e6f7 100644 --- a/client/instance.go +++ b/client/instance.go @@ -3,21 +3,19 @@ package client import ( "context" "fmt" - "net/http" - "net/url" "strings" "time" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" "github.com/hashicorp/terraform-plugin-log/tflog" - "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/bytebase/terraform-provider-bytebase/api" ) -func buildInstanceQuery(filter *api.InstanceFilter) string { +func buildInstanceFilter(filter *api.InstanceFilter) string { params := []string{} - showDeleted := v1pb.State_DELETED == filter.State if v := filter.Query; v != "" { params = append(params, fmt.Sprintf(`(name.matches("%s") || resource_id.matches("%s"))`, strings.ToLower(v), strings.ToLower(v))) @@ -41,37 +39,48 @@ func buildInstanceQuery(filter *api.InstanceFilter) string { } params = append(params, fmt.Sprintf(`engine in [%s]`, strings.Join(engines, ", "))) } - if showDeleted { + if filter.State == v1pb.State_DELETED { params = append(params, fmt.Sprintf(`state == "%s"`, filter.State.String())) } - if len(params) == 0 { - return fmt.Sprintf("showDeleted=%v", showDeleted) - } - - return fmt.Sprintf("filter=%s&showDeleted=%v", url.QueryEscape(strings.Join(params, " && ")), showDeleted) + return strings.Join(params, " && ") } -// ListInstance will return instances. +// ListInstance will return instances using Connect RPC. func (c *client) ListInstance(ctx context.Context, filter *api.InstanceFilter) ([]*v1pb.Instance, error) { + if c.instanceClient == nil { + return nil, fmt.Errorf("instance service client not initialized") + } + res := []*v1pb.Instance{} pageToken := "" startTime := time.Now() - query := buildInstanceQuery(filter) + filterStr := buildInstanceFilter(filter) + showDeleted := filter.State == v1pb.State_DELETED for { startTimePerPage := time.Now() - resp, err := c.listInstancePerPage(ctx, query, pageToken, 500) + + req := connect.NewRequest(&v1pb.ListInstancesRequest{ + Filter: filterStr, + PageSize: 500, + PageToken: pageToken, + ShowDeleted: showDeleted, + }) + + resp, err := c.instanceClient.ListInstances(ctx, req) if err != nil { return nil, err } - res = append(res, resp.Instances...) + + res = append(res, resp.Msg.Instances...) + tflog.Debug(ctx, "[list instance per page]", map[string]interface{}{ - "count": len(resp.Instances), + "count": len(resp.Msg.Instances), "ms": time.Since(startTimePerPage).Milliseconds(), }) - pageToken = resp.NextPageToken + pageToken = resp.Msg.NextPageToken if pageToken == "" { break } @@ -85,117 +94,104 @@ func (c *client) ListInstance(ctx context.Context, filter *api.InstanceFilter) ( return res, nil } -// listInstancePerPage list the instance. -func (c *client) listInstancePerPage(ctx context.Context, query, pageToken string, pageSize int) (*v1pb.ListInstancesResponse, error) { - requestURL := fmt.Sprintf( - "%s/%s/instances?%s&page_size=%d&page_token=%s", - c.url, - c.version, - query, - pageSize, - url.QueryEscape(pageToken), - ) - - req, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) - if err != nil { - return nil, err - } - - var res v1pb.ListInstancesResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err +// GetInstance gets the instance by full name using Connect RPC. +func (c *client) GetInstance(ctx context.Context, instanceName string) (*v1pb.Instance, error) { + if c.instanceClient == nil { + return nil, fmt.Errorf("instance service client not initialized") } - return &res, nil -} + req := connect.NewRequest(&v1pb.GetInstanceRequest{ + Name: instanceName, + }) -// 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, "") + resp, err := c.instanceClient.GetInstance(ctx, req) if err != nil { return nil, err } - var res v1pb.Instance - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - return &res, nil + return resp.Msg, nil } -// CreateInstance creates the instance. +// CreateInstance creates the instance using Connect RPC. func (c *client) CreateInstance(ctx context.Context, instanceID string, instance *v1pb.Instance) (*v1pb.Instance, error) { - payload, err := protojson.Marshal(instance) - if err != nil { - return nil, err + if c.instanceClient == nil { + return nil, fmt.Errorf("instance service client not initialized") } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/instances?instanceId=%s", c.url, c.version, instanceID), strings.NewReader(string(payload))) - - if err != nil { - return nil, err - } + req := connect.NewRequest(&v1pb.CreateInstanceRequest{ + InstanceId: instanceID, + Instance: instance, + }) - body, err := c.doRequest(req) + resp, err := c.instanceClient.CreateInstance(ctx, req) if err != nil { return nil, err } - var res v1pb.Instance - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - return &res, nil + return resp.Msg, nil } -// UpdateInstance updates the instance. +// UpdateInstance updates the instance using Connect RPC. func (c *client) UpdateInstance(ctx context.Context, patch *v1pb.Instance, updateMasks []string) (*v1pb.Instance, error) { - body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) - if err != nil { - return nil, err + if c.instanceClient == nil { + return nil, fmt.Errorf("instance service client not initialized") } - var res v1pb.Instance - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.UpdateInstanceRequest{ + Instance: patch, + UpdateMask: &fieldmaskpb.FieldMask{Paths: updateMasks}, + }) + + resp, err := c.instanceClient.UpdateInstance(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// UndeleteInstance undeletes the instance. +// UndeleteInstance undeletes the instance using Connect RPC. func (c *client) UndeleteInstance(ctx context.Context, instanceName string) (*v1pb.Instance, error) { - body, err := c.undeleteResource(ctx, instanceName) - if err != nil { - return nil, err + if c.instanceClient == nil { + return nil, fmt.Errorf("instance service client not initialized") } - var res v1pb.Instance - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.UndeleteInstanceRequest{ + Name: instanceName, + }) + + resp, err := c.instanceClient.UndeleteInstance(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// SyncInstanceSchema will trigger the schema sync for an instance. +// SyncInstanceSchema will trigger the schema sync for an instance using Connect RPC. func (c *client) SyncInstanceSchema(ctx context.Context, instanceName string) error { - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s:sync", c.url, c.version, instanceName), nil) - - if err != nil { - return err + if c.instanceClient == nil { + return fmt.Errorf("instance service client not initialized") } - if _, err := c.doRequest(req); err != nil { - return err + req := connect.NewRequest(&v1pb.SyncInstanceRequest{ + Name: instanceName, + }) + + _, err := c.instanceClient.SyncInstance(ctx, req) + return err +} + +// DeleteInstance deletes the instance. +func (c *client) DeleteInstance(ctx context.Context, name string) error { + if c.instanceClient == nil { + return fmt.Errorf("instance service client not initialized") } - return nil + req := connect.NewRequest(&v1pb.DeleteInstanceRequest{ + Name: name, + }) + + _, err := c.instanceClient.DeleteInstance(ctx, req) + return err } diff --git a/client/policy.go b/client/policy.go index 393f178..728865b 100644 --- a/client/policy.go +++ b/client/policy.go @@ -3,68 +3,80 @@ package client import ( "context" "fmt" - "net/http" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/fieldmaskpb" ) // ListPolicies lists policies in a specific resource. func (c *client) ListPolicies(ctx context.Context, parent string) (*v1pb.ListPoliciesResponse, error) { - var url string - if parent == "" { - url = fmt.Sprintf("%s/%s/policies", c.url, c.version) - } else { - url = fmt.Sprintf("%s/%s/%s/policies", c.url, c.version, parent) - } - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, err + if c.orgPolicyClient == nil { + return nil, fmt.Errorf("org policy service client not initialized") } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } + req := connect.NewRequest(&v1pb.ListPoliciesRequest{ + Parent: parent, + }) - var res v1pb.ListPoliciesResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + resp, err := c.orgPolicyClient.ListPolicies(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } // 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, "") - if err != nil { - return nil, err + if c.orgPolicyClient == nil { + return nil, fmt.Errorf("org policy service client not initialized") } - var res v1pb.Policy - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetPolicyRequest{ + Name: policyName, + }) + + resp, err := c.orgPolicyClient.GetPolicy(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } // UpsertPolicy creates or updates the policy. func (c *client) UpsertPolicy(ctx context.Context, policy *v1pb.Policy, updateMasks []string) (*v1pb.Policy, error) { - body, err := c.updateResource(ctx, policy.Name, policy, updateMasks, true /* allow missing = true*/) - if err != nil { - return nil, err + if c.orgPolicyClient == nil { + return nil, fmt.Errorf("org policy service client not initialized") } - var res v1pb.Policy - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.UpdatePolicyRequest{ + Policy: policy, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: updateMasks, + }, + AllowMissing: true, + }) + + resp, err := c.orgPolicyClient.UpdatePolicy(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } // DeletePolicy deletes the policy. func (c *client) DeletePolicy(ctx context.Context, policyName string) error { - return c.execDelete(ctx, policyName) -} + if c.orgPolicyClient == nil { + return fmt.Errorf("org policy service client not initialized") + } + + req := connect.NewRequest(&v1pb.DeletePolicyRequest{ + Name: policyName, + }) + + _, err := c.orgPolicyClient.DeletePolicy(ctx, req) + return err +} \ No newline at end of file diff --git a/client/project.go b/client/project.go index 06aa2c2..85bc15c 100644 --- a/client/project.go +++ b/client/project.go @@ -3,14 +3,11 @@ package client import ( "context" "fmt" - "net/http" - "net/url" "strings" - "time" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" "github.com/hashicorp/terraform-plugin-log/tflog" - "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/bytebase/terraform-provider-bytebase/api" @@ -18,267 +15,266 @@ 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, "") - if err != nil { - return nil, err + if c.projectClient == nil { + return nil, fmt.Errorf("project service client not initialized") } - var res v1pb.Project - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetProjectRequest{ + Name: projectName, + }) + + resp, err := c.projectClient.GetProject(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } // 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), "") - if err != nil { - return nil, err + if c.projectClient == nil { + return nil, fmt.Errorf("project service client not initialized") } - var res v1pb.IamPolicy - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetIamPolicyRequest{ + Resource: projectName, + }) + + resp, err := c.projectClient.GetIamPolicy(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } // SetProjectIAMPolicy sets the project IAM policy. func (c *client) SetProjectIAMPolicy(ctx context.Context, projectName string, update *v1pb.SetIamPolicyRequest) (*v1pb.IamPolicy, error) { - payload, err := protojson.Marshal(update) - if err != nil { - return nil, err + if c.projectClient == nil { + return nil, fmt.Errorf("project service client not initialized") } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s:setIamPolicy", c.url, c.version, projectName), strings.NewReader(string(payload))) + // Update the resource field to match the project name + update.Resource = projectName - if err != nil { - return nil, err - } + req := connect.NewRequest(update) - body, err := c.doRequest(req) + resp, err := c.projectClient.SetIamPolicy(ctx, req) if err != nil { return nil, err } - var res v1pb.IamPolicy - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - return &res, nil + return resp.Msg, nil } // CreateProjectWebhook creates the webhook in the project. func (c *client) CreateProjectWebhook(ctx context.Context, projectName string, webhook *v1pb.Webhook) (*v1pb.Webhook, error) { - payload, err := protojson.Marshal(&v1pb.AddWebhookRequest{ + if c.projectClient == nil { + return nil, fmt.Errorf("project service client not initialized") + } + + req := connect.NewRequest(&v1pb.AddWebhookRequest{ Project: projectName, Webhook: webhook, }) - if err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s:addWebhook", c.url, c.version, projectName), strings.NewReader(string(payload))) - - if err != nil { - return nil, err - } - body, err := c.doRequest(req) + resp, err := c.projectClient.AddWebhook(ctx, req) if err != nil { return nil, err } - var res v1pb.Webhook - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err + // AddWebhook returns the updated project, find the webhook we just added + if resp.Msg != nil && len(resp.Msg.Webhooks) > 0 { + // Return the last webhook (the one we just added) + return resp.Msg.Webhooks[len(resp.Msg.Webhooks)-1], nil } - return &res, nil + return nil, fmt.Errorf("webhook not found in response") } // UpdateProjectWebhook updates the webhook. func (c *client) UpdateProjectWebhook(ctx context.Context, patch *v1pb.Webhook, updateMasks []string) (*v1pb.Webhook, error) { - payload, err := protojson.Marshal(&v1pb.UpdateWebhookRequest{ + if c.projectClient == nil { + return nil, fmt.Errorf("project service client not initialized") + } + + req := connect.NewRequest(&v1pb.UpdateWebhookRequest{ Webhook: patch, UpdateMask: &fieldmaskpb.FieldMask{ Paths: updateMasks, }, }) - if err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s:updateWebhook", c.url, c.version, patch.Name), strings.NewReader(string(payload))) - if err != nil { - return nil, err - } - body, err := c.doRequest(req) + resp, err := c.projectClient.UpdateWebhook(ctx, req) if err != nil { return nil, err } - var res v1pb.Webhook - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err + // UpdateWebhook returns the updated project, find the updated webhook + if resp.Msg != nil && resp.Msg.Webhooks != nil { + for _, wh := range resp.Msg.Webhooks { + if wh.Name == patch.Name { + return wh, nil + } + } } - return &res, nil + return nil, fmt.Errorf("updated webhook not found in response") } // DeleteProjectWebhook deletes the webhook. func (c *client) DeleteProjectWebhook(ctx context.Context, webhookName string) error { - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s:removeWebhook", c.url, c.version, url.QueryEscape(webhookName)), nil) - if err != nil { - return err - } - - if _, err := c.doRequest(req); err != nil { - return err - } - return nil -} - -func buildProjectQuery(filter *api.ProjectFilter) string { - params := []string{} - showDeleted := v1pb.State_DELETED == filter.State - - if v := filter.Query; v != "" { - params = append(params, fmt.Sprintf(`(name.matches("%s") || resource_id.matches("%s"))`, strings.ToLower(v), strings.ToLower(v))) - } - if filter.ExcludeDefault { - params = append(params, "exclude_default == true") - } - if showDeleted { - params = append(params, fmt.Sprintf(`state == "%s"`, filter.State.String())) + if c.projectClient == nil { + return fmt.Errorf("project service client not initialized") } - if len(params) == 0 { - return fmt.Sprintf("showDeleted=%v", showDeleted) - } + req := connect.NewRequest(&v1pb.RemoveWebhookRequest{ + Webhook: &v1pb.Webhook{ + Name: webhookName, + }, + }) - return fmt.Sprintf("filter=%s&showDeleted=%v", url.QueryEscape(strings.Join(params, " && ")), showDeleted) + _, err := c.projectClient.RemoveWebhook(ctx, req) + return err } // ListProject list all projects. func (c *client) ListProject(ctx context.Context, filter *api.ProjectFilter) ([]*v1pb.Project, error) { - res := []*v1pb.Project{} + if c.projectClient == nil { + return nil, fmt.Errorf("project service client not initialized") + } + + var projects []*v1pb.Project pageToken := "" - startTime := time.Now() - query := buildProjectQuery(filter) for { - startTimePerPage := time.Now() - resp, err := c.listProjectPerPage(ctx, query, pageToken, 500) + req := connect.NewRequest(&v1pb.ListProjectsRequest{ + PageSize: 500, + PageToken: pageToken, + ShowDeleted: filter.State == v1pb.State_DELETED, + }) + + resp, err := c.projectClient.ListProjects(ctx, req) if err != nil { return nil, err } - res = append(res, resp.Projects...) - tflog.Debug(ctx, "[list project per page]", map[string]interface{}{ - "count": len(resp.Projects), - "ms": time.Since(startTimePerPage).Milliseconds(), - }) - pageToken = resp.NextPageToken + // Filter projects based on the filter criteria + for _, project := range resp.Msg.Projects { + // Apply filter logic + if filter.Query != "" { + // Check if name or resource_id matches the query + // Extract resource ID from name (e.g., "projects/my-project" -> "my-project") + resourceID := "" + if parts := strings.Split(project.Name, "/"); len(parts) > 1 { + resourceID = parts[len(parts)-1] + } + if !containsIgnoreCase(project.Name, filter.Query) && !containsIgnoreCase(resourceID, filter.Query) { + continue + } + } + + if filter.ExcludeDefault { + // Skip default project (you might need to adjust this logic) + if project.Name == "projects/default" { + continue + } + } + + if filter.State != v1pb.State_STATE_UNSPECIFIED && project.State != filter.State { + continue + } + + projects = append(projects, project) + } + + pageToken = resp.Msg.NextPageToken if pageToken == "" { break } } tflog.Debug(ctx, "[list project]", map[string]interface{}{ - "total": len(res), - "ms": time.Since(startTime).Milliseconds(), + "total": len(projects), }) - return res, nil -} - -// listProjectPerPage list the projects. -func (c *client) listProjectPerPage(ctx context.Context, query, pageToken string, pageSize int) (*v1pb.ListProjectsResponse, error) { - requestURL := fmt.Sprintf( - "%s/%s/projects?%s&page_size=%d&page_token=%s", - c.url, - c.version, - query, - pageSize, - url.QueryEscape(pageToken), - ) - - req, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) - if err != nil { - return nil, err - } - - var res v1pb.ListProjectsResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - return &res, nil + return projects, nil } // CreateProject creates the project. func (c *client) CreateProject(ctx context.Context, projectID string, project *v1pb.Project) (*v1pb.Project, error) { - payload, err := protojson.Marshal(project) - if err != nil { - return nil, err + if c.projectClient == nil { + return nil, fmt.Errorf("project service client not initialized") } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/projects?projectId=%s", c.url, c.version, projectID), strings.NewReader(string(payload))) - - if err != nil { - return nil, err - } + req := connect.NewRequest(&v1pb.CreateProjectRequest{ + ProjectId: projectID, + Project: project, + }) - body, err := c.doRequest(req) + resp, err := c.projectClient.CreateProject(ctx, req) if err != nil { return nil, err } - var res v1pb.Project - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - return &res, nil + return resp.Msg, nil } // UpdateProject updates the project. func (c *client) UpdateProject(ctx context.Context, patch *v1pb.Project, updateMasks []string) (*v1pb.Project, error) { - body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) - if err != nil { - return nil, err + if c.projectClient == nil { + return nil, fmt.Errorf("project service client not initialized") } - var res v1pb.Project - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.UpdateProjectRequest{ + Project: patch, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: updateMasks, + }, + }) + + resp, err := c.projectClient.UpdateProject(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } // UndeleteProject undeletes the project. func (c *client) UndeleteProject(ctx context.Context, projectName string) (*v1pb.Project, error) { - body, err := c.undeleteResource(ctx, projectName) + if c.projectClient == nil { + return nil, fmt.Errorf("project service client not initialized") + } + + req := connect.NewRequest(&v1pb.UndeleteProjectRequest{ + Name: projectName, + }) + + resp, err := c.projectClient.UndeleteProject(ctx, req) if err != nil { return nil, err } - var res v1pb.Project - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err + return resp.Msg, nil +} + +// DeleteProject deletes the project. +func (c *client) DeleteProject(ctx context.Context, name string) error { + if c.projectClient == nil { + return fmt.Errorf("project service client not initialized") } - return &res, nil + req := connect.NewRequest(&v1pb.DeleteProjectRequest{ + Name: name, + }) + + _, err := c.projectClient.DeleteProject(ctx, req) + return err } + +// Helper function for case-insensitive string contains +func containsIgnoreCase(s, substr string) bool { + return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) +} \ No newline at end of file diff --git a/client/review_config.go b/client/review_config.go index f7205dc..1741802 100644 --- a/client/review_config.go +++ b/client/review_config.go @@ -3,57 +3,76 @@ package client import ( "context" "fmt" - "net/http" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/fieldmaskpb" ) -// ListReviewConfig will return review configs. +// ListReviewConfig will return review configs using Connect RPC. 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 + if c.reviewConfigClient == nil { + return nil, fmt.Errorf("review config service client not initialized") } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } + req := connect.NewRequest(&v1pb.ListReviewConfigsRequest{}) - var res v1pb.ListReviewConfigsResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + resp, err := c.reviewConfigClient.ListReviewConfigs(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// GetReviewConfig gets the review config by full name. +// GetReviewConfig gets the review config by full name using Connect RPC. func (c *client) GetReviewConfig(ctx context.Context, reviewName string) (*v1pb.ReviewConfig, error) { - body, err := c.getResource(ctx, reviewName, "") - if err != nil { - return nil, err + if c.reviewConfigClient == nil { + return nil, fmt.Errorf("review config service client not initialized") } - var res v1pb.ReviewConfig - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetReviewConfigRequest{ + Name: reviewName, + }) + + resp, err := c.reviewConfigClient.GetReviewConfig(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// UpsertReviewConfig updates or creates the review config. +// UpsertReviewConfig updates or creates the review config using Connect RPC. 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 c.reviewConfigClient == nil { + return nil, fmt.Errorf("review config service client not initialized") + } + + req := connect.NewRequest(&v1pb.UpdateReviewConfigRequest{ + ReviewConfig: patch, + AllowMissing: true, + UpdateMask: &fieldmaskpb.FieldMask{Paths: updateMasks}, + }) + + resp, err := c.reviewConfigClient.UpdateReviewConfig(ctx, req) if err != nil { return nil, err } - var res v1pb.ReviewConfig - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err + return resp.Msg, nil +} + +// DeleteReviewConfig deletes the review config. +func (c *client) DeleteReviewConfig(ctx context.Context, name string) error { + if c.reviewConfigClient == nil { + return fmt.Errorf("review config service client not initialized") } - return &res, nil + req := connect.NewRequest(&v1pb.DeleteReviewConfigRequest{ + Name: name, + }) + + _, err := c.reviewConfigClient.DeleteReviewConfig(ctx, req) + return err } diff --git a/client/risk.go b/client/risk.go index 8c4f31e..6a2053d 100644 --- a/client/risk.go +++ b/client/risk.go @@ -3,85 +3,93 @@ package client import ( "context" "fmt" - "net/http" - "strings" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" - "google.golang.org/protobuf/encoding/protojson" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/fieldmaskpb" ) -// ListRisk lists the risk. +// ListRisk lists the risk using Connect RPC. func (c *client) ListRisk(ctx context.Context) ([]*v1pb.Risk, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/risks", c.url, c.version), nil) - if err != nil { - return nil, err + if c.riskClient == nil { + return nil, fmt.Errorf("risk service client not initialized") } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } + req := connect.NewRequest(&v1pb.ListRisksRequest{}) - var res v1pb.ListRisksResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + resp, err := c.riskClient.ListRisks(ctx, req) + if err != nil { return nil, err } - return res.Risks, nil + return resp.Msg.Risks, nil } -// GetRisk gets the risk by full name. +// GetRisk gets the risk by full name using Connect RPC. func (c *client) GetRisk(ctx context.Context, name string) (*v1pb.Risk, error) { - body, err := c.getResource(ctx, name, "") - if err != nil { - return nil, err + if c.riskClient == nil { + return nil, fmt.Errorf("risk service client not initialized") } - var res v1pb.Risk - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetRiskRequest{ + Name: name, + }) + + resp, err := c.riskClient.GetRisk(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// CreateRisk creates the risk. +// CreateRisk creates the risk using Connect RPC. func (c *client) CreateRisk(ctx context.Context, risk *v1pb.Risk) (*v1pb.Risk, error) { - payload, err := protojson.Marshal(risk) - if err != nil { - return nil, err + if c.riskClient == nil { + return nil, fmt.Errorf("risk service client not initialized") } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/risks", c.url, c.version), strings.NewReader(string(payload))) + req := connect.NewRequest(&v1pb.CreateRiskRequest{ + Risk: risk, + }) + resp, err := c.riskClient.CreateRisk(ctx, req) if err != nil { return nil, err } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } + return resp.Msg, nil +} - var res v1pb.Risk - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err +// UpdateRisk updates the risk using Connect RPC. +func (c *client) UpdateRisk(ctx context.Context, patch *v1pb.Risk, updateMasks []string) (*v1pb.Risk, error) { + if c.riskClient == nil { + return nil, fmt.Errorf("risk service client not initialized") } - return &res, nil -} + req := connect.NewRequest(&v1pb.UpdateRiskRequest{ + Risk: patch, + UpdateMask: &fieldmaskpb.FieldMask{Paths: updateMasks}, + }) -// UpdateRisk updates the risk. -func (c *client) UpdateRisk(ctx context.Context, patch *v1pb.Risk, updateMasks []string) (*v1pb.Risk, error) { - body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) + resp, err := c.riskClient.UpdateRisk(ctx, req) if err != nil { return nil, err } - var res v1pb.Risk - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err + return resp.Msg, nil +} + +// DeleteRisk deletes the risk. +func (c *client) DeleteRisk(ctx context.Context, name string) error { + if c.riskClient == nil { + return fmt.Errorf("risk service client not initialized") } - return &res, nil + req := connect.NewRequest(&v1pb.DeleteRiskRequest{ + Name: name, + }) + + _, err := c.riskClient.DeleteRisk(ctx, req) + return err } diff --git a/client/role.go b/client/role.go index 4988b0a..88c9816 100644 --- a/client/role.go +++ b/client/role.go @@ -3,85 +3,95 @@ package client import ( "context" "fmt" - "net/http" - "strings" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" - "google.golang.org/protobuf/encoding/protojson" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/fieldmaskpb" ) -// GetRole gets the role by full name. +// GetRole gets the role by full name using Connect RPC. func (c *client) GetRole(ctx context.Context, name string) (*v1pb.Role, error) { - body, err := c.getResource(ctx, name, "") - if err != nil { - return nil, err + if c.roleClient == nil { + return nil, fmt.Errorf("role service client not initialized") } - var res v1pb.Role - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetRoleRequest{ + Name: name, + }) + + resp, err := c.roleClient.GetRole(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// CreateRole creates the role. +// CreateRole creates the role using Connect RPC. func (c *client) CreateRole(ctx context.Context, roleID string, role *v1pb.Role) (*v1pb.Role, error) { - payload, err := protojson.Marshal(role) - if err != nil { - return nil, err + if c.roleClient == nil { + return nil, fmt.Errorf("role service client not initialized") } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/roles?roleId=%s", c.url, c.version, roleID), strings.NewReader(string(payload))) + req := connect.NewRequest(&v1pb.CreateRoleRequest{ + Role: role, + RoleId: roleID, + }) + resp, err := c.roleClient.CreateRole(ctx, req) if err != nil { return nil, err } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } - - var res v1pb.Role - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - return &res, nil + return resp.Msg, nil } -// UpdateRole updates the role. +// UpdateRole updates the role using Connect RPC. func (c *client) UpdateRole(ctx context.Context, patch *v1pb.Role, updateMasks []string) (*v1pb.Role, error) { - body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, true /* allow missing = true*/) - if err != nil { - return nil, err + if c.roleClient == nil { + return nil, fmt.Errorf("role service client not initialized") } - var res v1pb.Role - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.UpdateRoleRequest{ + Role: patch, + AllowMissing: true, + UpdateMask: &fieldmaskpb.FieldMask{Paths: updateMasks}, + }) + + resp, err := c.roleClient.UpdateRole(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// ListRole will returns all roles. +// ListRole will returns all roles using Connect RPC. func (c *client) ListRole(ctx context.Context) (*v1pb.ListRolesResponse, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/roles", c.url, c.version), nil) - if err != nil { - return nil, err + if c.roleClient == nil { + return nil, fmt.Errorf("role service client not initialized") } - body, err := c.doRequest(req) + req := connect.NewRequest(&v1pb.ListRolesRequest{}) + + resp, err := c.roleClient.ListRoles(ctx, req) if err != nil { return nil, err } - var res v1pb.ListRolesResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err + return resp.Msg, nil +} + +// DeleteRole deletes the role. +func (c *client) DeleteRole(ctx context.Context, name string) error { + if c.roleClient == nil { + return fmt.Errorf("role service client not initialized") } - return &res, nil + req := connect.NewRequest(&v1pb.DeleteRoleRequest{ + Name: name, + }) + + _, err := c.roleClient.DeleteRole(ctx, req) + return err } diff --git a/client/setting.go b/client/setting.go index b70622c..82dce29 100644 --- a/client/setting.go +++ b/client/setting.go @@ -3,57 +3,62 @@ package client import ( "context" "fmt" - "net/http" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/fieldmaskpb" ) -// ListSettings lists all settings. +// ListSettings lists all settings using Connect RPC. func (c *client) ListSettings(ctx context.Context) (*v1pb.ListSettingsResponse, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/settings", c.url, c.version), nil) - if err != nil { - return nil, err + if c.settingClient == nil { + return nil, fmt.Errorf("setting service client not initialized") } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } + req := connect.NewRequest(&v1pb.ListSettingsRequest{}) - var res v1pb.ListSettingsResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + resp, err := c.settingClient.ListSettings(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// GetSetting gets the setting by the name. +// GetSetting gets the setting by the name using Connect RPC. func (c *client) GetSetting(ctx context.Context, settingName string) (*v1pb.Setting, error) { - body, err := c.getResource(ctx, settingName, "") - if err != nil { - return nil, err + if c.settingClient == nil { + return nil, fmt.Errorf("setting service client not initialized") } - var res v1pb.Setting - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetSettingRequest{ + Name: settingName, + }) + + resp, err := c.settingClient.GetSetting(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// UpsertSetting updates or creates the setting. +// UpsertSetting updates or creates the setting using Connect RPC. func (c *client) UpsertSetting(ctx context.Context, upsert *v1pb.Setting, updateMasks []string) (*v1pb.Setting, error) { - body, err := c.updateResource(ctx, upsert.Name, upsert, updateMasks, true /* allow missing = true*/) - if err != nil { - return nil, err + if c.settingClient == nil { + return nil, fmt.Errorf("setting service client not initialized") } - var res v1pb.Setting - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.UpdateSettingRequest{ + Setting: upsert, + AllowMissing: true, + UpdateMask: &fieldmaskpb.FieldMask{Paths: updateMasks}, + }) + + resp, err := c.settingClient.UpdateSetting(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } diff --git a/client/user.go b/client/user.go index d7bd923..84c1045 100644 --- a/client/user.go +++ b/client/user.go @@ -3,21 +3,19 @@ package client import ( "context" "fmt" - "net/http" - "net/url" "strings" "time" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" "github.com/hashicorp/terraform-plugin-log/tflog" - "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/bytebase/terraform-provider-bytebase/api" ) -func buildUserQuery(filter *api.UserFilter) string { +func buildUserFilter(filter *api.UserFilter) string { params := []string{} - showDeleted := v1pb.State_DELETED == filter.State if v := filter.Name; v != "" { params = append(params, fmt.Sprintf(`name == "%s"`, strings.ToLower(v))) @@ -35,37 +33,48 @@ func buildUserQuery(filter *api.UserFilter) string { } params = append(params, fmt.Sprintf(`user_type in [%s]`, strings.Join(userTypes, ", "))) } - if showDeleted { + if filter.State == v1pb.State_DELETED { params = append(params, fmt.Sprintf(`state == "%s"`, filter.State.String())) } - if len(params) == 0 { - return fmt.Sprintf("showDeleted=%v", showDeleted) - } - - return fmt.Sprintf("filter=%s&showDeleted=%v", url.QueryEscape(strings.Join(params, " && ")), showDeleted) + return strings.Join(params, " && ") } -// ListUser list all users. +// ListUser list all users using Connect RPC. func (c *client) ListUser(ctx context.Context, filter *api.UserFilter) ([]*v1pb.User, error) { + if c.userClient == nil { + return nil, fmt.Errorf("user service client not initialized") + } + res := []*v1pb.User{} pageToken := "" startTime := time.Now() - query := buildUserQuery(filter) + filterStr := buildUserFilter(filter) + showDeleted := filter.State == v1pb.State_DELETED for { startTimePerPage := time.Now() - resp, err := c.listUserPerPage(ctx, query, pageToken, 500) + + req := connect.NewRequest(&v1pb.ListUsersRequest{ + Filter: filterStr, + PageSize: 500, + PageToken: pageToken, + ShowDeleted: showDeleted, + }) + + resp, err := c.userClient.ListUsers(ctx, req) if err != nil { return nil, err } - res = append(res, resp.Users...) + + res = append(res, resp.Msg.Users...) + tflog.Debug(ctx, "[list user per page]", map[string]interface{}{ - "count": len(resp.Users), + "count": len(resp.Msg.Users), "ms": time.Since(startTimePerPage).Milliseconds(), }) - pageToken = resp.NextPageToken + pageToken = resp.Msg.NextPageToken if pageToken == "" { break } @@ -79,102 +88,89 @@ func (c *client) ListUser(ctx context.Context, filter *api.UserFilter) ([]*v1pb. return res, nil } -// listUserPerPage list the users. -func (c *client) listUserPerPage(ctx context.Context, query, pageToken string, pageSize int) (*v1pb.ListUsersResponse, error) { - requestURL := fmt.Sprintf( - "%s/%s/users?%s&page_size=%d&page_token=%s", - c.url, - c.version, - query, - pageSize, - url.QueryEscape(pageToken), - ) - - req, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil) - if err != nil { - return nil, err +// CreateUser creates the user using Connect RPC. +func (c *client) CreateUser(ctx context.Context, user *v1pb.User) (*v1pb.User, error) { + if c.userClient == nil { + return nil, fmt.Errorf("user service client not initialized") } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } + req := connect.NewRequest(&v1pb.CreateUserRequest{ + User: user, + }) - var res v1pb.ListUsersResponse - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + resp, err := c.userClient.CreateUser(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// CreateUser creates the user. -func (c *client) CreateUser(ctx context.Context, user *v1pb.User) (*v1pb.User, error) { - payload, err := protojson.Marshal(user) - if err != nil { - return nil, err +// GetUser gets the user by name using Connect RPC. +func (c *client) GetUser(ctx context.Context, userName string) (*v1pb.User, error) { + if c.userClient == nil { + return nil, fmt.Errorf("user service client not initialized") } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/users", c.url, c.version), strings.NewReader(string(payload))) + req := connect.NewRequest(&v1pb.GetUserRequest{ + Name: userName, + }) + resp, err := c.userClient.GetUser(ctx, req) if err != nil { return nil, err } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } + return resp.Msg, nil +} - var res v1pb.User - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err +// UpdateUser updates the user using Connect RPC. +func (c *client) UpdateUser(ctx context.Context, patch *v1pb.User, updateMasks []string) (*v1pb.User, error) { + if c.userClient == nil { + return nil, fmt.Errorf("user service client not initialized") } - return &res, nil -} + req := connect.NewRequest(&v1pb.UpdateUserRequest{ + User: patch, + UpdateMask: &fieldmaskpb.FieldMask{Paths: updateMasks}, + }) -// GetUser gets the user by name. -func (c *client) GetUser(ctx context.Context, userName string) (*v1pb.User, error) { - body, err := c.getResource(ctx, userName, "") + resp, err := c.userClient.UpdateUser(ctx, req) if err != nil { return nil, err } - var res v1pb.User - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } - - return &res, nil + return resp.Msg, nil } -// UpdateUser updates the user. -func (c *client) UpdateUser(ctx context.Context, patch *v1pb.User, updateMasks []string) (*v1pb.User, error) { - body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) - if err != nil { - return nil, err +// UndeleteUser undeletes the user by name using Connect RPC. +func (c *client) UndeleteUser(ctx context.Context, userName string) (*v1pb.User, error) { + if c.userClient == nil { + return nil, fmt.Errorf("user service client not initialized") } - var res v1pb.User - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.UndeleteUserRequest{ + Name: userName, + }) + + resp, err := c.userClient.UndeleteUser(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } -// UndeleteUser undeletes the user by name. -func (c *client) UndeleteUser(ctx context.Context, userName string) (*v1pb.User, error) { - body, err := c.undeleteResource(ctx, userName) - if err != nil { - return nil, err +// DeleteUser deletes the user. +func (c *client) DeleteUser(ctx context.Context, name string) error { + if c.userClient == nil { + return fmt.Errorf("user service client not initialized") } - var res v1pb.User - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { - return nil, err - } + req := connect.NewRequest(&v1pb.DeleteUserRequest{ + Name: name, + }) - return &res, nil -} + _, err := c.userClient.DeleteUser(ctx, req) + return err +} \ No newline at end of file diff --git a/client/workspace.go b/client/workspace.go index 6732370..c822c2c 100644 --- a/client/workspace.go +++ b/client/workspace.go @@ -3,50 +3,46 @@ package client import ( "context" "fmt" - "net/http" - "strings" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" - "google.golang.org/protobuf/encoding/protojson" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" + "connectrpc.com/connect" ) // GetWorkspaceIAMPolicy gets the workspace IAM policy. func (c *client) GetWorkspaceIAMPolicy(ctx context.Context) (*v1pb.IamPolicy, error) { - body, err := c.getResource(ctx, "workspaces/-:getIamPolicy", "") - if err != nil { - return nil, err + if c.workspaceClient == nil { + return nil, fmt.Errorf("workspace service client not initialized") } - var res v1pb.IamPolicy - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + req := connect.NewRequest(&v1pb.GetIamPolicyRequest{ + Resource: "workspaces/-", + }) + + resp, err := c.workspaceClient.GetIamPolicy(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } // SetWorkspaceIAMPolicy sets the workspace IAM policy. func (c *client) SetWorkspaceIAMPolicy(ctx context.Context, setIamPolicyRequest *v1pb.SetIamPolicyRequest) (*v1pb.IamPolicy, error) { - payload, err := protojson.Marshal(setIamPolicyRequest) - if err != nil { - return nil, err + if c.workspaceClient == nil { + return nil, fmt.Errorf("workspace service client not initialized") } - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s:setIamPolicy", c.url, c.version, "workspaces/-"), strings.NewReader(string(payload))) - - if err != nil { - return nil, err + // Ensure the resource is set correctly + if setIamPolicyRequest.Resource == "" { + setIamPolicyRequest.Resource = "workspaces/-" } - body, err := c.doRequest(req) - if err != nil { - return nil, err - } + req := connect.NewRequest(setIamPolicyRequest) - var res v1pb.IamPolicy - if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + resp, err := c.workspaceClient.SetIamPolicy(ctx, req) + if err != nil { return nil, err } - return &res, nil + return resp.Msg, nil } diff --git a/docs/data-sources/group_list.md b/docs/data-sources/group_list.md index a21bd4f..eadfd6f 100644 --- a/docs/data-sources/group_list.md +++ b/docs/data-sources/group_list.md @@ -15,6 +15,11 @@ The group data source list. ## Schema +### Optional + +- `project` (String) The project fullname in projects/{id} format. +- `query` (String) Filter groups by title or email with wildcard + ### Read-Only - `groups` (List of Object) (see [below for nested schema](#nestedatt--groups)) diff --git a/docs/data-sources/policy.md b/docs/data-sources/policy.md index 36a894f..113cba2 100644 --- a/docs/data-sources/policy.md +++ b/docs/data-sources/policy.md @@ -88,12 +88,12 @@ Optional: Required: - `action` (String) -- `database` (String) The database full name in instances/{instance resource id}/databases/{database name} format - `member` (String) The member in user:{email} or group:{email} format. Optional: - `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 - `reason` (String) The reason for the masking exemption - `schema` (String) diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md index 528f806..8d294c9 100644 --- a/docs/data-sources/project.md +++ b/docs/data-sources/project.md @@ -32,7 +32,7 @@ The project data source. - `postgres_database_tenant_mode` (Boolean) Whether to enable the database tenant mode for PostgreSQL. If enabled, the issue will be created with the pre-appended "set role " statement. - `skip_backup_errors` (Boolean) Whether to skip backup errors and continue the data migration. - `title` (String) The project title. -- `webhooks` (List of Object) The webhooks in the project. (see [below for nested schema](#nestedatt--webhooks)) +- `webhooks` (Set of Object) The webhooks in the project. (see [below for nested schema](#nestedatt--webhooks)) ### Nested Schema for `webhooks` diff --git a/docs/data-sources/project_list.md b/docs/data-sources/project_list.md index b1835c7..05877fe 100644 --- a/docs/data-sources/project_list.md +++ b/docs/data-sources/project_list.md @@ -42,7 +42,7 @@ Read-Only: - `resource_id` (String) - `skip_backup_errors` (Boolean) - `title` (String) -- `webhooks` (List of Object) (see [below for nested schema](#nestedobjatt--projects--webhooks)) +- `webhooks` (Set of Object) (see [below for nested schema](#nestedobjatt--projects--webhooks)) ### Nested Schema for `projects.webhooks` diff --git a/docs/data-sources/review_config.md b/docs/data-sources/review_config.md index d7c42e1..bae2a43 100644 --- a/docs/data-sources/review_config.md +++ b/docs/data-sources/review_config.md @@ -21,7 +21,7 @@ The review config data source. - `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` (List of Object) The SQL review rules. (see [below for nested schema](#nestedatt--rules)) +- `rules` (Set of Object) The SQL review rules. (see [below for nested schema](#nestedatt--rules)) - `title` (String) The title for the review config. diff --git a/docs/data-sources/review_config_list.md b/docs/data-sources/review_config_list.md index 73cbf1e..c3bcb7a 100644 --- a/docs/data-sources/review_config_list.md +++ b/docs/data-sources/review_config_list.md @@ -28,7 +28,7 @@ Read-Only: - `enabled` (Boolean) - `resource_id` (String) - `resources` (Set of String) -- `rules` (List of Object) (see [below for nested schema](#nestedobjatt--review_configs--rules)) +- `rules` (Set of Object) (see [below for nested schema](#nestedobjatt--review_configs--rules)) - `title` (String) diff --git a/docs/data-sources/setting.md b/docs/data-sources/setting.md index cd012e1..75c8030 100644 --- a/docs/data-sources/setting.md +++ b/docs/data-sources/setting.md @@ -23,7 +23,7 @@ The setting data source. - `classification` (Block List, Max: 1) Classification for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--classification)) - `password_restriction` (Block List, Max: 1) Restrict for login password (see [below for nested schema](#nestedblock--password_restriction)) -- `semantic_types` (Block List) Semantic types for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--semantic_types)) +- `semantic_types` (Block Set) Semantic types for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--semantic_types)) - `sql_query_restriction` (Block List, Max: 1) Restrict for SQL query result (see [below for nested schema](#nestedblock--sql_query_restriction)) - `workspace_profile` (Block List, Max: 1) (see [below for nested schema](#nestedblock--workspace_profile)) @@ -38,9 +38,9 @@ The setting data source. Required: -- `classifications` (Block List, Min: 1) (see [below for nested schema](#nestedblock--classification--classifications)) +- `classifications` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--classification--classifications)) - `id` (String) The classification unique uuid. -- `levels` (Block List, Min: 1) (see [below for nested schema](#nestedblock--classification--levels)) +- `levels` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--classification--levels)) - `title` (String) The classification title. Optional. Optional: @@ -186,14 +186,11 @@ Optional: ### Nested Schema for `workspace_profile.announcement` -Required: - -- `level` (String) The alert level of announcement -- `text` (String) The text of announcement. Leave it as empty string can clear the announcement - Optional: +- `level` (String) The alert level of announcement - `link` (String) The optional link, user can follow the link to check extra details +- `text` (String) The text of announcement. Leave it as empty string can clear the announcement diff --git a/docs/resources/group.md b/docs/resources/group.md index 65c8a0f..43f500d 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -36,7 +36,7 @@ The group resource. Workspace domain is required for creating groups. Required: -- `member` (String) The member in users/{email} format. +- `member` (String) The member in users/{email} format. Only allow add end users to the group. - `role` (String) The member's role in the group, should be OWNER or MEMBER. diff --git a/docs/resources/instance.md b/docs/resources/instance.md index 0b07d8a..aff45db 100644 --- a/docs/resources/instance.md +++ b/docs/resources/instance.md @@ -19,17 +19,18 @@ The instance resource. - `data_sources` (Block Set, Min: 1) The connection for the instance. You can configure read-only or admin connection account here. (see [below for nested schema](#nestedblock--data_sources)) - `engine` (String) The instance engine. Support MYSQL, POSTGRES, TIDB, SNOWFLAKE, CLICKHOUSE, MONGODB, SQLITE, REDIS, ORACLE, SPANNER, MSSQL, REDSHIFT, MARIADB, OCEANBASE, COCKROACHDB. -- `environment` (String) The environment full name for the instance in environments/{environment id} format. - `resource_id` (String) The instance unique resource id. - `title` (String) The instance title. ### Optional - `activation` (Boolean) Whether assign license for this instance or not. +- `environment` (String) The environment full name for the instance in environments/{environment id} format. - `external_link` (String) The external console URL managing this instance (e.g. AWS RDS console, your in-house DB instance console) - `list_all_databases` (Boolean) List all databases in this instance. If false, will only list 500 databases. -- `maximum_connections` (Number) The maximum number of connections. -- `sync_interval` (Number) How often the instance is synced in seconds. Default 0, means never sync. +- `maximum_connections` (Number) The maximum number of connections. Require instance license to enable this feature. +- `sync_databases` (Set of String) Enable sync for following databases. Default empty, means sync all schemas & databases. +- `sync_interval` (Number) How often the instance is synced in seconds. Default 0, means never sync. Require instance license to enable this feature. ### Read-Only @@ -37,7 +38,6 @@ The instance resource. - `engine_version` (String) The engine version. - `id` (String) The ID of this resource. - `name` (String) The instance full name in instances/{resource id} format. -- `sync_databases` (Set of String) Enable sync for following databases. Default empty, means sync all schemas & databases. ### Nested Schema for `data_sources` @@ -47,12 +47,12 @@ Required: - `host` (String) Host or socket for your instance, or the account name if the instance type is Snowflake. - `id` (String) The unique data source id in this instance. - `port` (String) The port for your instance. -- `type` (String) The data source type. Should be ADMIN or READ_ONLY. +- `type` (String) The data source type. Should be ADMIN or READ_ONLY. The READ_ONLY data source requires the instance license. Optional: - `database` (String) The database for the instance, you can set this if the engine type is POSTGRES. -- `external_secret` (Block List, Max: 1) The external secret to get the database password. Learn more: https://www.bytebase.com/docs/get-started/instance/#use-external-secret-manager (see [below for nested schema](#nestedblock--data_sources--external_secret)) +- `external_secret` (Block List, Max: 1) The external secret to get the database password. Require instance license to enable this feature. Learn more: https://www.bytebase.com/docs/get-started/instance/#use-external-secret-manager (see [below for nested schema](#nestedblock--data_sources--external_secret)) - `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. diff --git a/docs/resources/policy.md b/docs/resources/policy.md index 394b698..4d5d1a7 100644 --- a/docs/resources/policy.md +++ b/docs/resources/policy.md @@ -88,12 +88,12 @@ Optional: Required: - `action` (String) -- `database` (String) The database full name in instances/{instance resource id}/databases/{database name} format - `member` (String) The member in user:{email} or group:{email} format. Optional: - `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 - `reason` (String) The reason for the masking exemption - `schema` (String) diff --git a/docs/resources/project.md b/docs/resources/project.md index 9bbe709..08767b1 100644 --- a/docs/resources/project.md +++ b/docs/resources/project.md @@ -33,7 +33,7 @@ The project resource. - `parallel_tasks_per_rollout` (Number) The maximum number of parallel tasks to run during the rollout. - `postgres_database_tenant_mode` (Boolean) Whether to enable the database tenant mode for PostgreSQL. If enabled, the issue will be created with the pre-appended "set role " statement. - `skip_backup_errors` (Boolean) Whether to skip backup errors and continue the data migration. -- `webhooks` (List of Object) The webhooks in the project. (see [below for nested schema](#nestedatt--webhooks)) +- `webhooks` (Set of Object) The webhooks in the project. (see [below for nested schema](#nestedatt--webhooks)) ### Read-Only diff --git a/docs/resources/review_config.md b/docs/resources/review_config.md index 40b4edb..c626056 100644 --- a/docs/resources/review_config.md +++ b/docs/resources/review_config.md @@ -19,7 +19,7 @@ The review config resource. - `enabled` (Boolean) Enable the SQL review config - `resource_id` (String) The unique resource id for the review config. -- `rules` (Block List, Min: 1) The SQL review rules. (see [below for nested schema](#nestedblock--rules)) +- `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 diff --git a/docs/resources/setting.md b/docs/resources/setting.md index 55337de..93a5adc 100644 --- a/docs/resources/setting.md +++ b/docs/resources/setting.md @@ -25,7 +25,7 @@ The setting resource. - `classification` (Block List, Max: 1) Classification for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--classification)) - `environment_setting` (Block List) The environment (see [below for nested schema](#nestedblock--environment_setting)) - `password_restriction` (Block List, Max: 1) Restrict for login password (see [below for nested schema](#nestedblock--password_restriction)) -- `semantic_types` (Block List) Semantic types for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--semantic_types)) +- `semantic_types` (Block Set) Semantic types for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--semantic_types)) - `sql_query_restriction` (Block List, Max: 1) Restrict for SQL query result (see [below for nested schema](#nestedblock--sql_query_restriction)) - `workspace_profile` (Block List, Max: 1) (see [below for nested schema](#nestedblock--workspace_profile)) @@ -88,9 +88,9 @@ Optional: Required: -- `classifications` (Block List, Min: 1) (see [below for nested schema](#nestedblock--classification--classifications)) +- `classifications` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--classification--classifications)) - `id` (String) The classification unique uuid. -- `levels` (Block List, Min: 1) (see [below for nested schema](#nestedblock--classification--levels)) +- `levels` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--classification--levels)) - `title` (String) The classification title. Optional. Optional: @@ -262,13 +262,10 @@ Optional: ### Nested Schema for `workspace_profile.announcement` -Required: - -- `level` (String) The alert level of announcement -- `text` (String) The text of announcement. Leave it as empty string can clear the announcement - Optional: +- `level` (String) The alert level of announcement - `link` (String) The optional link, user can follow the link to check extra details +- `text` (String) The text of announcement. Leave it as empty string can clear the announcement diff --git a/examples/groups/main.tf b/examples/groups/main.tf index b8f78ef..059fcac 100644 --- a/examples/groups/main.tf +++ b/examples/groups/main.tf @@ -17,17 +17,30 @@ provider "bytebase" { url = "https://bytebase.example.com" } -data "bytebase_group_list" "all" { +data "bytebase_group_list" "all_groups" { } output "all_groups" { - value = data.bytebase_group_list.all + value = data.bytebase_group_list.all_groups.groups } -data "bytebase_group" "sample" { - name = "groups/group@bytebase.com" +data "bytebase_project" "sample_project" { + resource_id = "sample" } -output "sample_group" { - value = data.bytebase_group.sample +data "bytebase_group_list" "groups_in_project" { + project = data.bytebase_project.sample_project.name + query = "Bytebase" } + +output "groups_in_project" { + value = data.bytebase_group_list.groups_in_project.groups +} + +# Query single group +# data "bytebase_group" "sample" { +# name = "groups/group@bytebase.com" +# } +# output "sample_group" { +# value = data.bytebase_group.sample +# } diff --git a/examples/setup/environment.tf b/examples/setup/environment.tf index 28c141f..796adaf 100644 --- a/examples/setup/environment.tf +++ b/examples/setup/environment.tf @@ -2,45 +2,44 @@ resource "bytebase_setting" "environments" { name = "settings/ENVIRONMENT" environment_setting { - environment { - id = local.environment_id_prod - title = "Prod" - protected = true - } - environment { id = local.environment_id_test title = "Test" protected = false } + + environment { + id = local.environment_id_prod + title = "Prod" + protected = true + } } } -# Upsert test environment. -resource "bytebase_environment" "test" { - depends_on = [ - bytebase_setting.environments - ] - resource_id = local.environment_id_test - title = "Staging" // rename to "Staging" - order = 0 // change order to 0 - protected = false -} +# Example to upsert single environment. +# resource "bytebase_environment" "test" { +# depends_on = [ +# bytebase_setting.environments +# ] +# resource_id = local.environment_id_test +# title = "Staging" // rename to "Staging" +# order = 0 // change order to 0 +# protected = false +# } -# Upsert prod environment. -resource "bytebase_environment" "prod" { - depends_on = [ - bytebase_environment.test - ] - resource_id = local.environment_id_prod - title = "Prod" - order = 1 // change order to 1 - protected = true -} +# resource "bytebase_environment" "prod" { +# depends_on = [ +# bytebase_environment.test +# ] +# resource_id = local.environment_id_prod +# title = "Prod" +# order = 1 // change order to 1 +# protected = true +# } resource "bytebase_policy" "rollout_policy" { - depends_on = [bytebase_environment.test] - parent = bytebase_environment.test.name + depends_on = [bytebase_setting.environments] + parent = bytebase_setting.environments.environment_setting[0].environment[0].name type = "ROLLOUT_POLICY" rollout_policy { @@ -55,18 +54,18 @@ resource "bytebase_policy" "rollout_policy" { } resource "bytebase_policy" "disable_copy_data_policy" { - depends_on = [bytebase_environment.test] - parent = bytebase_environment.test.name + depends_on = [bytebase_setting.environments] + parent = bytebase_setting.environments.environment_setting[0].environment[0].name type = "DISABLE_COPY_DATA" disable_copy_data_policy { - enable = true + enable = false } } resource "bytebase_policy" "data_source_query_policy" { - depends_on = [bytebase_environment.test] - parent = bytebase_environment.test.name + depends_on = [bytebase_setting.environments] + parent = bytebase_setting.environments.environment_setting[0].environment[0].name type = "DATA_SOURCE_QUERY" data_source_query_policy { diff --git a/examples/setup/iam.tf b/examples/setup/iam.tf index 1ebfdbc..bd11ef9 100644 --- a/examples/setup/iam.tf +++ b/examples/setup/iam.tf @@ -15,8 +15,7 @@ resource "bytebase_iam_policy" "workspace_iam" { binding { role = "roles/workspaceAdmin" members = [ - format("user:%s", local.service_account), - format("user:%s", bytebase_user.workspace_dba.email), + format("user:%s", bytebase_user.service_account.email), ] } @@ -24,7 +23,6 @@ resource "bytebase_iam_policy" "workspace_iam" { role = "roles/workspaceDBA" members = [ format("user:%s", bytebase_user.workspace_dba.email), - format("user:%s", bytebase_user.service_account.email), ] } @@ -51,7 +49,8 @@ resource "bytebase_iam_policy" "project_iam" { bytebase_project.sample_project, bytebase_user.workspace_dba, bytebase_user.project_developer, - bytebase_group.developers + bytebase_group.developers, + bytebase_group.project_owners ] parent = bytebase_project.sample_project.name @@ -60,7 +59,8 @@ resource "bytebase_iam_policy" "project_iam" { binding { role = "roles/projectOwner" members = [ - format("user:%s", bytebase_user.workspace_dba.email) + format("user:%s", bytebase_user.workspace_dba.email), + format("group:%s", bytebase_group.project_owners.email), ] } diff --git a/examples/setup/instance.tf b/examples/setup/instance.tf index a8c46e9..6ed2c34 100644 --- a/examples/setup/instance.tf +++ b/examples/setup/instance.tf @@ -20,15 +20,18 @@ resource "bytebase_instance" "test" { port = "3366" username = "bytebase" - external_secret { - vault { - url = "http://127.0.0.1:8200" - token = "" - engine_name = "secret" - secret_name = "bytebase" - password_key_name = "database_pwd" - } - } + password = "YOUR_DB_PWD" + + # You can also use external_secret for password (require instance license) + # external_secret { + # vault { + # url = "http://127.0.0.1:8200" + # token = "" + # engine_name = "secret" + # secret_name = "bytebase" + # password_key_name = "database_pwd" + # } + # } } # And you can add another data_sources with RO type @@ -40,6 +43,11 @@ resource "bytebase_instance" "test" { host = "127.0.0.1" port = "3366" } + + # You can specific the databases to sync. + # sync_databases = [ + # "employee" + # ] } # Create a new instance named "prod instance" @@ -52,6 +60,12 @@ resource "bytebase_instance" "prod" { environment = bytebase_setting.environments.environment_setting[0].environment[1].name title = "prod instance" engine = "POSTGRES" + activation = true + + # Require instance license + sync_interval = 60 * 60 * 2 # 2 hour + # Require instance license + maximum_connections = 20 # You need to specific the data source data_sources { diff --git a/examples/setup/users.tf b/examples/setup/users.tf index 43fd6a4..71d1134 100644 --- a/examples/setup/users.tf +++ b/examples/setup/users.tf @@ -4,6 +4,11 @@ resource "bytebase_user" "workspace_dba" { email = "dba@bytebase.com" } +resource "bytebase_user" "project_owner" { + title = "Project Owner" + email = "project-owner@bytebase.com" +} + # Create or update the user. resource "bytebase_user" "workspace_auditor" { title = "Auditor" @@ -18,7 +23,7 @@ resource "bytebase_user" "project_developer" { resource "bytebase_user" "service_account" { title = "CI Bot" - email = "ci-bot@service.bytebase.com" + email = local.service_account type = "SERVICE_ACCOUNT" } @@ -44,3 +49,25 @@ resource "bytebase_group" "developers" { role = "MEMBER" } } + +resource "bytebase_group" "project_owners" { + depends_on = [ + bytebase_user.project_owner, + bytebase_user.workspace_dba, + # group requires the domain. + bytebase_setting.workspace_profile + ] + + email = "owner+dba@bytebase.com" + title = "Bytebase Project Owners" + + members { + member = format("users/%s", bytebase_user.project_owner.email) + role = "OWNER" + } + + members { + member = format("users/%s", bytebase_user.workspace_dba.email) + role = "MEMBER" + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 5333177..de06724 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.24.4 toolchain go1.24.5 require ( - github.com/bytebase/bytebase v0.0.0-20250718095332-26793303f26f + buf.build/gen/go/bytebase/bytebase/connectrpc/go v1.18.1-20250821091751-ab434d709c89.1 + buf.build/gen/go/bytebase/bytebase/protocolbuffers/go v1.36.8-20250821091751-ab434d709c89.1 + connectrpc.com/connect v1.18.1 github.com/hashicorp/go-cty v1.5.0 github.com/hashicorp/terraform-plugin-docs v0.13.0 github.com/hashicorp/terraform-plugin-log v0.9.0 @@ -13,7 +15,7 @@ require ( github.com/pkg/errors v0.9.1 google.golang.org/genproto v0.0.0-20250528174236-200df99c418a google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 - google.golang.org/protobuf v1.36.6 + google.golang.org/protobuf v1.36.8 ) require ( @@ -30,7 +32,6 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -51,6 +52,7 @@ require ( github.com/hashicorp/yamux v0.1.1 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.15 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/cli v1.1.4 // indirect @@ -78,4 +80,5 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index a1c975b..1f89036 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +buf.build/gen/go/bytebase/bytebase/connectrpc/go v1.18.1-20250821091751-ab434d709c89.1 h1:ZmVAFhx6C+5hh2JwHIicGVXPU0P+dWWUMYsSzXJGSZM= +buf.build/gen/go/bytebase/bytebase/connectrpc/go v1.18.1-20250821091751-ab434d709c89.1/go.mod h1:cCayz9URf5ApvJZGfIE7bfasYECFG9ECIVNNOm4Vci0= +buf.build/gen/go/bytebase/bytebase/protocolbuffers/go v1.36.8-20250821091751-ab434d709c89.1 h1:uHBwD119t1nqozj6q/Zh/+NBfzUb/qLVRK/dhvZkbqw= +buf.build/gen/go/bytebase/bytebase/protocolbuffers/go v1.36.8-20250821091751-ab434d709c89.1/go.mod h1:dwdKUX0jGgJ7OJe024SNHvANb1TKuBzIrZOzL/3Njtk= +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= @@ -25,16 +31,14 @@ github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= -github.com/bytebase/bytebase v0.0.0-20250718095332-26793303f26f h1:GOHLNccIwjPS/P0R0xR5BXb3YSugwYIqBDAw8IMhoGU= -github.com/bytebase/bytebase v0.0.0-20250718095332-26793303f26f/go.mod h1:aRuoDfrE19CbRcvrOV5o6nqLYda5yvtHDtk42I2DK3k= github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -49,8 +53,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= @@ -70,8 +74,6 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -134,6 +136,7 @@ github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gav github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -168,16 +171,16 @@ github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -196,8 +199,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -214,16 +217,16 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -291,8 +294,8 @@ google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/provider/data_source_database.go b/provider/data_source_database.go index 8b500c8..5f9712f 100644 --- a/provider/data_source_database.go +++ b/provider/data_source_database.go @@ -1,9 +1,7 @@ package provider import ( - "bytes" "context" - "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -108,15 +106,11 @@ func dataSourceDatabase() *schema.Resource { }, }, }, - Set: func(i interface{}) int { - return internal.ToHashcodeInt(columnHash(i)) - }, + Set: columnHash, }, }, }, - Set: func(i interface{}) int { - return internal.ToHashcodeInt(tableHash(i)) - }, + Set: tableHash, }, }, }, @@ -143,60 +137,23 @@ func dataSourceDatabaseRead(ctx context.Context, d *schema.ResourceData, m inter return setDatabase(ctx, c, d, database) } -func columnHash(rawColumn interface{}) string { - var buf bytes.Buffer - column := rawColumn.(map[string]interface{}) - - if v, ok := column["name"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - if v, ok := column["semantic_type"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - if v, ok := column["classification"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - if v, ok := column["labels"].(map[string]interface{}); ok { - for key, val := range v { - _, _ = buf.WriteString(fmt.Sprintf("[%s:%s]-", key, val.(string))) - } - } - return buf.String() +func columnHash(rawColumn interface{}) int { + column := convertToV1ColumnCatalog(rawColumn) + return internal.ToHash(column) } -func tableHash(rawTable interface{}) string { - var buf bytes.Buffer - table := rawTable.(map[string]interface{}) - - if v, ok := table["name"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - if v, ok := table["classification"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - if columns, ok := table["columns"].(*schema.Set); ok { - for _, column := range columns.List() { - rawColumn := column.(map[string]interface{}) - _, _ = buf.WriteString(columnHash(rawColumn)) - } +func tableHash(rawTable interface{}) int { + table, err := convertToV1TableCatalog(rawTable) + if err != nil { + return 0 } - - return buf.String() + return internal.ToHash(table) } func schemaHash(rawSchema interface{}) int { - var buf bytes.Buffer - raw := rawSchema.(map[string]interface{}) - - if v, ok := raw["name"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - if tables, ok := raw["tables"].(*schema.Set); ok { - for _, table := range tables.List() { - rawTable := table.(map[string]interface{}) - _, _ = buf.WriteString(tableHash(rawTable)) - } + schema, err := convertToV1SchemaCatalog(rawSchema) + if err != nil { + return 0 } - - return internal.ToHashcodeInt(buf.String()) + return internal.ToHash(schema) } diff --git a/provider/data_source_database_group.go b/provider/data_source_database_group.go index f19a7ac..50fd877 100644 --- a/provider/data_source_database_group.go +++ b/provider/data_source_database_group.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" diff --git a/provider/data_source_database_list.go b/provider/data_source_database_list.go index 84bc100..bf5c5b0 100644 --- a/provider/data_source_database_list.go +++ b/provider/data_source_database_list.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -163,7 +163,9 @@ func dataSourceDatabaseListRead(ctx context.Context, d *schema.ResourceData, m i db := map[string]interface{}{} db["name"] = database.Name db["project"] = database.Project - db["environment"] = database.Environment + if v := database.EffectiveEnvironment; v != nil { + db["environment"] = *v + } db["state"] = database.State.String() db["successful_sync_time"] = database.SuccessfulSyncTime.AsTime().UTC().Format(time.RFC3339) db["schema_version"] = database.SchemaVersion diff --git a/provider/data_source_group.go b/provider/data_source_group.go index bdadaf5..74c5709 100644 --- a/provider/data_source_group.go +++ b/provider/data_source_group.go @@ -1,14 +1,13 @@ package provider import ( - "bytes" "context" "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -107,15 +106,6 @@ func setGroup(d *schema.ResourceData, group *v1pb.Group) diag.Diagnostics { } func memberHash(rawMember interface{}) int { - var buf bytes.Buffer - member := rawMember.(map[string]interface{}) - - if v, ok := member["member"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - if v, ok := member["role"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - - return internal.ToHashcodeInt(buf.String()) + member := convertToV1Member(rawMember) + return internal.ToHash(member) } diff --git a/provider/data_source_group_list.go b/provider/data_source_group_list.go index 3e320cc..4cc3248 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 { @@ -16,6 +18,19 @@ func dataSourceGroupList() *schema.Resource { Description: "The group data source list.", ReadContext: dataSourceGroupListRead, Schema: map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: internal.ResourceNameValidation( + fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern), + ), + Description: "The project fullname in projects/{id} format.", + }, + "query": { + Type: schema.TypeString, + Optional: true, + Description: "Filter groups by title or email with wildcard", + }, "groups": { Type: schema.TypeList, Computed: true, @@ -71,13 +86,16 @@ func dataSourceGroupList() *schema.Resource { func dataSourceGroupListRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { c := m.(api.Client) - response, err := c.ListGroup(ctx) + response, err := c.ListGroup(ctx, &api.GroupFilter{ + Query: d.Get("query").(string), + Project: d.Get("project").(string), + }) if err != nil { return diag.FromErr(err) } groups := make([]map[string]interface{}, 0) - for _, group := range response.Groups { + for _, group := range response { raw := make(map[string]interface{}) raw["name"] = group.Name raw["title"] = group.Title diff --git a/provider/data_source_iam_policy.go b/provider/data_source_iam_policy.go index 5d5bc40..fed4c19 100644 --- a/provider/data_source_iam_policy.go +++ b/provider/data_source_iam_policy.go @@ -1,7 +1,6 @@ package provider import ( - "bytes" "context" "fmt" "strconv" @@ -11,7 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/pkg/errors" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -128,9 +127,7 @@ func getIAMBindingSchema(computed bool) *schema.Schema { }, }, }, - Set: func(i interface{}) int { - return internal.ToHashcodeInt(conditionHash(i)) - }, + Set: conditionHash, }, }, }, @@ -198,14 +195,14 @@ func flattenIAMPolicy(p *v1pb.IamPolicy) ([]interface{}, error) { strings.TrimPrefix(expression, `resource.table in [`), `]`, ) - rawTableList := []string{} + rawTableList := []interface{}{} for _, t := range strings.Split(tableStr, ",") { rawTableList = append(rawTableList, strings.TrimSuffix( strings.TrimPrefix(t, `"`), `"`, )) } - rawCondition["tables"] = rawTableList + rawCondition["tables"] = schema.NewSet(schema.HashString, rawTableList) } if strings.HasPrefix(expression, `request.row_limit <= `) { i, err := strconv.Atoi(strings.TrimPrefix(expression, `request.row_limit <= `)) @@ -223,11 +220,17 @@ func flattenIAMPolicy(p *v1pb.IamPolicy) ([]interface{}, error) { } } - rawBinding["condition"] = schema.NewSet(func(i interface{}) int { - return internal.ToHashcodeInt(conditionHash(i)) - }, []interface{}{rawCondition}) + // Only set condition if it's not empty + if len(rawCondition) > 0 { + rawBinding["condition"] = schema.NewSet(conditionHash, []interface{}{rawCondition}) + } rawBinding["role"] = binding.Role - rawBinding["members"] = binding.Members + // Convert members slice to a set with proper interface conversion + membersList := make([]interface{}, len(binding.Members)) + for i, member := range binding.Members { + membersList[i] = member + } + rawBinding["members"] = schema.NewSet(schema.HashString, membersList) bindingList = append(bindingList, rawBinding) } @@ -238,48 +241,17 @@ func flattenIAMPolicy(p *v1pb.IamPolicy) ([]interface{}, error) { } func bindingHash(rawBinding interface{}) int { - var buf bytes.Buffer - binding := rawBinding.(map[string]interface{}) - - if v, ok := binding["role"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - - if condition, ok := binding["condition"].(*schema.Set); ok && condition.Len() > 0 && condition.List()[0] != nil { - rawCondition := condition.List()[0].(map[string]interface{}) - _, _ = buf.WriteString(conditionHash(rawCondition)) - } - - if members, ok := binding["members"].(*schema.Set); ok && members.Len() > 0 { - for _, member := range members.List() { - _, _ = buf.WriteString(fmt.Sprintf("[member] %s", member)) - } + binding, err := convertToV1Binding(rawBinding) + if err != nil { + return 0 } - - return internal.ToHashcodeInt(buf.String()) + return internal.ToHash(binding) } -func conditionHash(rawCondition interface{}) string { - var buf bytes.Buffer - condition := rawCondition.(map[string]interface{}) - - if v, ok := condition["database"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - if v, ok := condition["schema"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) - } - if v, ok := condition["tables"].(*schema.Set); ok { - for _, t := range v.List() { - _, _ = buf.WriteString(fmt.Sprintf("table.%s-", t.(string))) - } - } - if v, ok := condition["row_limit"].(int); ok { - _, _ = buf.WriteString(fmt.Sprintf("%d-", v)) - } - if v, ok := condition["expire_timestamp"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) +func conditionHash(rawCondition interface{}) int { + condition, err := convertToV1Condition(rawCondition) + if err != nil { + return 0 } - - return buf.String() + return internal.ToHashcodeInt(condition.Expression) } diff --git a/provider/data_source_instance_list.go b/provider/data_source_instance_list.go index cf0b370..232bdea 100644 --- a/provider/data_source_instance_list.go +++ b/provider/data_source_instance_list.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -251,7 +251,9 @@ func dataSourceInstanceListRead(ctx context.Context, d *schema.ResourceData, m i ins["engine"] = instance.Engine.String() ins["engine_version"] = instance.EngineVersion ins["external_link"] = instance.ExternalLink - ins["environment"] = instance.Environment + if v := instance.Environment; v != nil { + ins["environment"] = *v + } if v := instance.GetSyncInterval(); v != nil { ins["sync_interval"] = v.GetSeconds() } diff --git a/provider/data_source_policy.go b/provider/data_source_policy.go index e96a5b6..0c90ca9 100644 --- a/provider/data_source_policy.go +++ b/provider/data_source_policy.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/pkg/errors" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -146,6 +146,7 @@ func getMaskingExceptionPolicySchema(computed bool) *schema.Schema { }, }, }, + Set: exceptionHash, }, }, }, @@ -490,3 +491,11 @@ func flattenMaskingExceptionPolicy(p *v1pb.MaskingExceptionPolicy) ([]interface{ } return []interface{}{policy}, nil } + +func exceptionHash(rawSchema interface{}) int { + exception, err := convertToV1Exception(rawSchema) + if err != nil { + return 0 + } + return internal.ToHash(exception) +} diff --git a/provider/data_source_policy_list.go b/provider/data_source_policy_list.go index acc8904..a7832ac 100644 --- a/provider/data_source_policy_list.go +++ b/provider/data_source_policy_list.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" diff --git a/provider/data_source_policy_list_test.go b/provider/data_source_policy_list_test.go index 4171073..fef8cb9 100644 --- a/provider/data_source_policy_list_test.go +++ b/provider/data_source_policy_list_test.go @@ -5,7 +5,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/provider/internal" ) diff --git a/provider/data_source_policy_test.go b/provider/data_source_policy_test.go index d869011..ab9b391 100644 --- a/provider/data_source_policy_test.go +++ b/provider/data_source_policy_test.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/provider/internal" ) diff --git a/provider/data_source_project.go b/provider/data_source_project.go index d628473..3fb8f78 100644 --- a/provider/data_source_project.go +++ b/provider/data_source_project.go @@ -13,7 +13,7 @@ import ( "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" ) func dataSourceProject() *schema.Resource { @@ -80,7 +80,7 @@ func dataSourceProject() *schema.Resource { func getWebhooksSchema(computed bool) *schema.Schema { return &schema.Schema{ - Type: schema.TypeList, + Type: schema.TypeSet, Optional: !computed, Computed: computed, Description: "The webhooks in the project.", @@ -147,6 +147,7 @@ func getWebhooksSchema(computed bool) *schema.Schema { }, }, }, + Set: webhookHash, } } @@ -182,22 +183,26 @@ func flattenDatabaseList(databases []*v1pb.Database) []interface{} { return dbList } -func flattenWebhookList(webhooks []*v1pb.Webhook) []map[string]interface{} { - rawWebhooks := []map[string]interface{}{} +func flattenWebhookList(webhooks []*v1pb.Webhook) []interface{} { + rawWebhooks := []interface{}{} for _, webhook := range webhooks { rawWebhook := make(map[string]interface{}) rawWebhook["title"] = webhook.Title rawWebhook["type"] = webhook.Type.String() rawWebhook["url"] = webhook.Url + // Include name for reference but it shouldn't affect the hash rawWebhook["name"] = webhook.Name rawWebhook["direct_message"] = webhook.DirectMessage - rawWebhooks = append(rawWebhooks, rawWebhook) - notificationTypes := []string{} + // Convert notification types to interface slice, then wrap in Set + notificationTypes := []interface{}{} for _, notificationType := range webhook.NotificationTypes { notificationTypes = append(notificationTypes, notificationType.String()) } - rawWebhook["notification_types"] = notificationTypes + // Use schema.NewSet for consistency with the schema definition + rawWebhook["notification_types"] = schema.NewSet(schema.HashString, notificationTypes) + + rawWebhooks = append(rawWebhooks, rawWebhook) } return rawWebhooks } @@ -268,9 +273,14 @@ func setProject( "ms": time.Since(startTime).Milliseconds(), }) - if err := d.Set("webhooks", flattenWebhookList(project.Webhooks)); err != nil { + if err := d.Set("webhooks", schema.NewSet(webhookHash, flattenWebhookList(project.Webhooks))); err != nil { return diag.Errorf("cannot set webhooks for project: %s", err.Error()) } return nil } + +func webhookHash(rawSchema interface{}) int { + webhook := convertToV1Webhook(rawSchema) + return internal.ToHash(webhook) +} diff --git a/provider/data_source_project_list.go b/provider/data_source_project_list.go index 8ff7d1b..4c79c31 100644 --- a/provider/data_source_project_list.go +++ b/provider/data_source_project_list.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -151,7 +151,7 @@ func dataSourceProjectListRead(ctx context.Context, d *schema.ResourceData, m in databaseList := flattenDatabaseList(databases) proj["databases"] = databaseList - proj["webhooks"] = flattenWebhookList(project.Webhooks) + proj["webhooks"] = schema.NewSet(webhookHash, flattenWebhookList(project.Webhooks)) projects = append(projects, proj) } diff --git a/provider/data_source_review_config.go b/provider/data_source_review_config.go index b407099..9ad7f6f 100644 --- a/provider/data_source_review_config.go +++ b/provider/data_source_review_config.go @@ -40,7 +40,7 @@ func dataSourceReviewConfig() *schema.Resource { 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.TypeList, + Type: schema.TypeSet, Computed: true, Description: "The SQL review rules.", Elem: &schema.Resource{ @@ -72,6 +72,7 @@ func dataSourceReviewConfig() *schema.Resource { }, }, }, + Set: reviewRuleHash, }, }, } diff --git a/provider/data_source_review_config_list.go b/provider/data_source_review_config_list.go index 26abd09..a3d6500 100644 --- a/provider/data_source_review_config_list.go +++ b/provider/data_source_review_config_list.go @@ -46,7 +46,7 @@ func dataSourceReviewConfigList() *schema.Resource { 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.TypeList, + Type: schema.TypeSet, Computed: true, Description: "The SQL review rules.", Elem: &schema.Resource{ @@ -78,6 +78,7 @@ func dataSourceReviewConfigList() *schema.Resource { }, }, }, + Set: reviewRuleHash, }, }, }, @@ -105,7 +106,7 @@ func dataSourceReviewConfigListRead(ctx context.Context, d *schema.ResourceData, raw["title"] = review.Title raw["enabled"] = review.Enabled raw["resources"] = review.Resources - raw["rules"] = flattenReviewRules(review.Rules) + raw["rules"] = schema.NewSet(reviewRuleHash, flattenReviewRules(review.Rules)) reviews = append(reviews, raw) } diff --git a/provider/data_source_setting.go b/provider/data_source_setting.go index 187f464..e521f57 100644 --- a/provider/data_source_setting.go +++ b/provider/data_source_setting.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/pkg/errors" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" v1alpha1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" "github.com/bytebase/terraform-provider-bytebase/api" @@ -51,7 +51,7 @@ func getSemanticTypesSetting(computed bool) *schema.Schema { Computed: computed, Optional: true, Default: nil, - Type: schema.TypeList, + Type: schema.TypeSet, Description: "Semantic types for data masking. Require ENTERPRISE subscription.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -188,6 +188,7 @@ func getSemanticTypesSetting(computed bool) *schema.Schema { }, }, }, + Set: semanticTypeHash, } } @@ -220,7 +221,7 @@ func getClassificationSetting(computed bool) *schema.Schema { }, "levels": { Required: true, - Type: schema.TypeList, + Type: schema.TypeSet, MinItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -242,10 +243,11 @@ func getClassificationSetting(computed bool) *schema.Schema { }, }, }, + Set: levelHash, }, "classifications": { Required: true, - Type: schema.TypeList, + Type: schema.TypeSet, MinItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -273,6 +275,7 @@ func getClassificationSetting(computed bool) *schema.Schema { }, }, }, + Set: classificationHash, }, }, }, @@ -320,7 +323,7 @@ func getWorkspaceProfileSetting(computed bool) *schema.Schema { "database_change_mode": { Type: schema.TypeString, Optional: true, - Default: v1pb.DatabaseChangeMode_PIPELINE.String(), + Default: v1pb.DatabaseChangeMode_DATABASE_CHANGE_MODE_UNSPECIFIED.String(), ValidateFunc: validation.StringInSlice([]string{ v1pb.DatabaseChangeMode_EDITOR.String(), v1pb.DatabaseChangeMode_PIPELINE.String(), @@ -341,14 +344,14 @@ func getWorkspaceProfileSetting(computed bool) *schema.Schema { "announcement": { Type: schema.TypeList, Optional: true, - MinItems: 1, + MinItems: 0, MaxItems: 1, Description: "Custom announcement. Will show as a banner in the Bytebase UI. Require ENTERPRISE subscription.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "text": { Type: schema.TypeString, - Required: true, + Optional: true, Description: "The text of announcement. Leave it as empty string can clear the announcement", }, "link": { @@ -358,8 +361,9 @@ func getWorkspaceProfileSetting(computed bool) *schema.Schema { }, "level": { Type: schema.TypeString, - Required: true, + Optional: true, Description: "The alert level of announcement", + Default: v1pb.Announcement_ALERT_LEVEL_UNSPECIFIED.String(), ValidateFunc: validation.StringInSlice([]string{ v1pb.Announcement_INFO.String(), v1pb.Announcement_WARNING.String(), @@ -650,7 +654,7 @@ func setSettingMessage(ctx context.Context, d *schema.ResourceData, client api.C } if value := setting.GetValue().GetSemanticTypeSettingValue(); value != nil { settingVal := flattenSemanticTypesSetting(value) - if err := d.Set("semantic_types", settingVal); err != nil { + if err := d.Set("semantic_types", schema.NewSet(semanticTypeHash, settingVal)); err != nil { return diag.Errorf("cannot set semantic_types: %s", err.Error()) } } @@ -808,14 +812,20 @@ func flattenWorkspaceProfileSetting(setting *v1pb.WorkspaceProfileSetting) []int if v := setting.GetMaximumRoleExpiration(); v != nil { raw["maximum_role_expiration_in_seconds"] = int(v.Seconds) } + // Handle announcement field - need to be careful with empty announcements if v := setting.GetAnnouncement(); v != nil { - raw["announcement"] = []any{ - map[string]any{ - "text": v.Text, - "link": v.Link, - "level": v.Level.String(), - }, + // Check if this is truly an empty announcement (all fields at their zero/default values) + isEmpty := v.Text == "" && v.Link == "" && v.Level == v1pb.Announcement_ALERT_LEVEL_UNSPECIFIED + if !isEmpty { + raw["announcement"] = []any{ + map[string]any{ + "text": v.Text, + "link": v.Link, + "level": v.Level.String(), + }, + } } + // If announcement is empty, don't set it at all - let Terraform handle it as unset } return []interface{}{raw} @@ -859,7 +869,7 @@ func flattenClassificationSetting(setting *v1pb.DataClassificationSetting) []int rawLevel["description"] = level.Description rawLevels = append(rawLevels, rawLevel) } - raw["levels"] = rawLevels + raw["levels"] = schema.NewSet(levelHash, rawLevels) rawClassifications := []interface{}{} for _, classification := range config.GetClassification() { @@ -870,7 +880,7 @@ func flattenClassificationSetting(setting *v1pb.DataClassificationSetting) []int rawClassification["level"] = classification.LevelId rawClassifications = append(rawClassifications, rawClassification) } - raw["classifications"] = rawClassifications + raw["classifications"] = schema.NewSet(classificationHash, rawClassifications) } return []interface{}{raw} @@ -950,3 +960,21 @@ func flattenSemanticTypesSetting(setting *v1pb.SemanticTypeSetting) []interface{ return raw } + +func levelHash(rawSchema interface{}) int { + classificationLevel := convertToV1Level(rawSchema) + return internal.ToHash(classificationLevel) +} + +func classificationHash(rawSchema interface{}) int { + classificationData := convertToV1Classification(rawSchema) + return internal.ToHash(classificationData) +} + +func semanticTypeHash(rawSchema interface{}) int { + semanticType, err := convertToV1SemanticType(rawSchema) + if err != nil { + return 0 + } + return internal.ToHash(semanticType) +} diff --git a/provider/data_source_user.go b/provider/data_source_user.go index 2f7b451..f7393e3 100644 --- a/provider/data_source_user.go +++ b/provider/data_source_user.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" diff --git a/provider/data_source_user_list.go b/provider/data_source_user_list.go index 8465637..4c262ac 100644 --- a/provider/data_source_user_list.go +++ b/provider/data_source_user_list.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" diff --git a/provider/internal/mock_client.go b/provider/internal/mock_client.go index 17f909b..48b05c7 100644 --- a/provider/internal/mock_client.go +++ b/provider/internal/mock_client.go @@ -10,7 +10,7 @@ import ( "github.com/bytebase/terraform-provider-bytebase/api" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" v1alpha1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) @@ -69,73 +69,6 @@ func newMockClient(_, _, _ string) (api.Client, error) { }, nil } -// GetCaller returns the API caller. -func (*mockClient) GetCaller() *v1pb.User { - return &v1pb.User{ - Name: "users/mock@bytease.com", - Email: "mock@bytease.com", - } -} - -// CheckResourceExist check if the resource exists. -func (c *mockClient) CheckResourceExist(_ context.Context, name string) error { - prefix := strings.Split(name, "/")[0] + "/" - switch prefix { - case InstanceNamePrefix: - if _, ok := c.instanceMap[name]; ok { - return nil - } - case ProjectNamePrefix: - if _, ok := c.projectMap[name]; ok { - return nil - } - case UserNamePrefix: - if _, ok := c.userMap[name]; ok { - return nil - } - case RoleNamePrefix: - if _, ok := c.roleMap[name]; ok { - return nil - } - case GroupNamePrefix: - if _, ok := c.groupMap[name]; ok { - return nil - } - case DatabaseGroupNamePrefix: - return nil - case ReviewConfigNamePrefix: - return nil - case RiskNamePrefix: - return nil - default: - return errors.Errorf("invalid resource name %v", name) - } - return errors.Errorf("status: 404 cannot found resource %v", name) -} - -// DeleteResource delete the resource by name. -func (c *mockClient) DeleteResource(_ context.Context, name string) error { - prefix := strings.Split(name, "/")[0] + "/" - switch prefix { - case InstanceNamePrefix: - delete(c.instanceMap, name) - case ProjectNamePrefix: - delete(c.projectMap, name) - case UserNamePrefix: - delete(c.userMap, name) - case RoleNamePrefix: - delete(c.roleMap, name) - case GroupNamePrefix: - delete(c.groupMap, name) - case DatabaseGroupNamePrefix: - case ReviewConfigNamePrefix: - case RiskNamePrefix: - default: - return errors.Errorf("invalid resource name %v", name) - } - return nil -} - // ListInstance will return instances in environment. func (c *mockClient) ListInstance(_ context.Context, filter *api.InstanceFilter) ([]*v1pb.Instance, error) { instances := make([]*v1pb.Instance, 0) @@ -171,7 +104,13 @@ func (c *mockClient) CreateInstance(_ context.Context, instanceID string, instan Environment: instance.Environment, } - envID, err := GetEnvironmentID(ins.Environment) + var envID string + var err error + if ins.Environment != nil { + envID, err = GetEnvironmentID(*ins.Environment) + } else { + err = fmt.Errorf("instance environment is nil") + } if err != nil { return nil, err } @@ -620,15 +559,13 @@ func (c *mockClient) UndeleteUser(ctx context.Context, userName string) (*v1pb.U } // ListGroup list all groups. -func (c *mockClient) ListGroup(_ context.Context) (*v1pb.ListGroupsResponse, error) { +func (c *mockClient) ListGroup(_ context.Context, _ *api.GroupFilter) ([]*v1pb.Group, error) { groups := make([]*v1pb.Group, 0) for _, group := range c.groupMap { groups = append(groups, group) } - return &v1pb.ListGroupsResponse{ - Groups: groups, - }, nil + return groups, nil } // GetGroup gets the group by name. diff --git a/provider/internal/resource.go b/provider/internal/resource.go index a501a87..ce91cc8 100644 --- a/provider/internal/resource.go +++ b/provider/internal/resource.go @@ -2,59 +2,31 @@ package internal import ( "context" - "fmt" - "net/http" "strings" - "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/bytebase/terraform-provider-bytebase/api" ) -// isNotFoundError checks if the error is a 404 Not Found error. -func isNotFoundError(err error) bool { +// IsNotFoundError checks if the error is a 404 Not Found error. +func IsNotFoundError(err error) bool { if err == nil { return false } // Check if error message contains status code 404 - return strings.Contains(err.Error(), "status: 404") || - strings.Contains(err.Error(), "status: "+string(rune(http.StatusNotFound))) -} - -// ResourceRead read the resource, and will clear the state if the resource not exist. -// Once the state is cleared, the terraform can exec the creation. -func ResourceRead(read schema.ReadContextFunc) schema.ReadContextFunc { - return func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(api.Client) - fullName := d.Id() - if err := c.CheckResourceExist(ctx, fullName); err != nil { - // Check if the resource was deleted outside of Terraform - if isNotFoundError(err) { - tflog.Warn(ctx, fmt.Sprintf("Resource %s not found, removing from state", fullName)) - // Remove from state to trigger recreation on next apply - d.SetId("") - return nil - } - return diag.FromErr(err) - } - - return read(ctx, d, m) - } + return strings.Contains(err.Error(), "not_found") } -// ResourceDelete force delete the resource. -func ResourceDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(api.Client) +// ResourceDelete wrap the delete func. +func ResourceDelete(ctx context.Context, d *schema.ResourceData, delete func(ctx context.Context, name string) error) diag.Diagnostics { fullName := d.Id() // Warning or errors can be collected in a slice type var diags diag.Diagnostics - if err := c.DeleteResource(ctx, fullName); err != nil { + if err := delete(ctx, fullName); err != nil { // Check if the resource was deleted outside of Terraform - if !isNotFoundError(err) { + if !IsNotFoundError(err) { return diag.FromErr(err) } } diff --git a/provider/internal/utils.go b/provider/internal/utils.go index 1ad4e58..3728173 100644 --- a/provider/internal/utils.go +++ b/provider/internal/utils.go @@ -6,7 +6,10 @@ import ( "regexp" "strings" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/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" @@ -105,9 +108,6 @@ var EngineValidation = validation.StringInSlice([]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(), @@ -178,15 +178,6 @@ 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) @@ -258,3 +249,12 @@ func ToHashcodeInt(s string) int { // v == MinInt return 0 } + +// ToHash convert proto message to hash int. +func ToHash(m proto.Message) int { + b, err := protojson.Marshal(m) + if err != nil { + return 0 + } + return ToHashcodeInt(string(b)) +} diff --git a/provider/provider.go b/provider/provider.go index 54b7838..b905d75 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -12,8 +12,6 @@ import ( ) const ( - openAPIVersion = "v1" - envKeyForBytebaseURL = "BYTEBASE_URL" envKeyForServiceAccount = "BYTEBASE_SERVICE_ACCOUNT" envKeyForServiceKey = "BYTEBASE_SERVICE_KEY" @@ -119,7 +117,7 @@ func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{}, return nil, diags } - c, err := client.NewClient(bytebaseURL, openAPIVersion, email, key) + c, err := client.NewClient(bytebaseURL, email, key) if err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, diff --git a/provider/resource_database.go b/provider/resource_database.go index bf7e3a3..bffb2c7 100644 --- a/provider/resource_database.go +++ b/provider/resource_database.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/pkg/errors" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -81,6 +81,7 @@ func resourceDatabase() *schema.Resource { Type: schema.TypeList, Computed: true, Optional: true, + MinItems: 0, MaxItems: 1, Description: "The databases catalog.", Elem: &schema.Resource{ @@ -140,15 +141,11 @@ func resourceDatabase() *schema.Resource { }, }, }, - Set: func(i interface{}) int { - return internal.ToHashcodeInt(columnHash(i)) - }, + Set: columnHash, }, }, }, - Set: func(i interface{}) int { - return internal.ToHashcodeInt(tableHash(i)) - }, + Set: tableHash, }, }, }, @@ -173,7 +170,8 @@ func resourceDatabaseUpdate(ctx context.Context, d *schema.ResourceData, m inter updateMasks := []string{"project"} rawConfig := d.GetRawConfig() if config := rawConfig.GetAttr("environment"); !config.IsNull() { - database.Environment = d.Get("environment").(string) + env := d.Get("environment").(string) + database.Environment = &env updateMasks = append(updateMasks, "environment") } if config := rawConfig.GetAttr("labels"); !config.IsNull() { @@ -261,8 +259,10 @@ func setDatabase( if err := d.Set("project", database.Project); err != nil { return diag.Errorf("cannot set project for database: %s", err.Error()) } - if err := d.Set("environment", database.EffectiveEnvironment); err != nil { - return diag.Errorf("cannot set environment for database: %s", err.Error()) + if v := database.EffectiveEnvironment; v != nil { + if err := d.Set("environment", *v); err != nil { + return diag.Errorf("cannot set environment for database: %s", err.Error()) + } } if err := d.Set("state", database.State.String()); err != nil { return diag.Errorf("cannot set state for database: %s", err.Error()) @@ -306,14 +306,10 @@ func flattenDatabaseCatalog(catalog *v1pb.DatabaseCatalog) []interface{} { rawColumn["labels"] = column.Labels columnList = append(columnList, rawColumn) } - rawTable["columns"] = schema.NewSet(func(i interface{}) int { - return internal.ToHashcodeInt(columnHash(i)) - }, columnList) + rawTable["columns"] = schema.NewSet(columnHash, columnList) tableList = append(tableList, rawTable) } - rawSchema["tables"] = schema.NewSet(func(i interface{}) int { - return internal.ToHashcodeInt(tableHash(i)) - }, tableList) + rawSchema["tables"] = schema.NewSet(tableHash, tableList) schemaList = append(schemaList, rawSchema) } @@ -323,6 +319,79 @@ func flattenDatabaseCatalog(catalog *v1pb.DatabaseCatalog) []interface{} { return []interface{}{rawCatalog} } +func convertToV1ColumnCatalog(raw interface{}) *v1pb.ColumnCatalog { + rawColumn := raw.(map[string]interface{}) + labels := map[string]string{} + + // Handle labels field which can be either map[string]interface{} or map[string]string + if rawLabels, ok := rawColumn["labels"]; ok && rawLabels != nil { + switch v := rawLabels.(type) { + case map[string]string: + labels = v + case map[string]interface{}: + for key, val := range v { + if strVal, ok := val.(string); ok { + labels[key] = strVal + } + } + } + } + + return &v1pb.ColumnCatalog{ + Name: rawColumn["name"].(string), + SemanticType: rawColumn["semantic_type"].(string), + Classification: rawColumn["classification"].(string), + Labels: labels, + } +} + +func convertToV1TableCatalog(raw interface{}) (*v1pb.TableCatalog, error) { + rawTable := raw.(map[string]interface{}) + table := &v1pb.TableCatalog{ + Name: rawTable["name"].(string), + Classification: rawTable["classification"].(string), + } + + columnList := []*v1pb.ColumnCatalog{} + rawColumnList, ok := rawTable["columns"].(*schema.Set) + if !ok { + return nil, errors.Errorf("invalid columns") + } + for _, raw := range rawColumnList.List() { + column := convertToV1ColumnCatalog(raw) + columnList = append(columnList, column) + } + + table.Kind = &v1pb.TableCatalog_Columns_{ + Columns: &v1pb.TableCatalog_Columns{ + Columns: columnList, + }, + } + + return table, nil +} + +func convertToV1SchemaCatalog(raw interface{}) (*v1pb.SchemaCatalog, error) { + rawSchema := raw.(map[string]interface{}) + schemaCatalog := &v1pb.SchemaCatalog{ + Name: rawSchema["name"].(string), + } + + rawTableList, ok := rawSchema["tables"].(*schema.Set) + if !ok { + return nil, errors.Errorf("invalid tables") + } + for _, raw := range rawTableList.List() { + table, err := convertToV1TableCatalog(raw) + if err != nil { + return nil, err + } + schemaCatalog.Tables = append(schemaCatalog.Tables, table) + } + + return schemaCatalog, nil +} + func convertToV1DatabaseCatalog(d *schema.ResourceData, databaseName string) (*v1pb.DatabaseCatalog, error) { catalogs, ok := d.Get("catalog").([]interface{}) if !ok || len(catalogs) != 1 { @@ -341,50 +410,9 @@ func convertToV1DatabaseCatalog(d *schema.ResourceData, databaseName string) (*v } for _, raw := range rawSchemaList.List() { - rawSchema := raw.(map[string]interface{}) - schemaCatalog := &v1pb.SchemaCatalog{ - Name: rawSchema["name"].(string), - } - - rawTableList, ok := rawSchema["tables"].(*schema.Set) - if !ok { - return nil, errors.Errorf("invalid tables") - } - for _, table := range rawTableList.List() { - rawTable := table.(map[string]interface{}) - table := &v1pb.TableCatalog{ - Name: rawTable["name"].(string), - Classification: rawTable["classification"].(string), - } - - columnList := []*v1pb.ColumnCatalog{} - rawColumnList, ok := rawTable["columns"].(*schema.Set) - if !ok { - return nil, errors.Errorf("invalid columns") - } - for _, column := range rawColumnList.List() { - rawColumn := column.(map[string]interface{}) - labels := map[string]string{} - for key, val := range rawColumn["labels"].(map[string]interface{}) { - labels[key] = val.(string) - } - - column := &v1pb.ColumnCatalog{ - Name: rawColumn["name"].(string), - SemanticType: rawColumn["semantic_type"].(string), - Classification: rawColumn["classification"].(string), - Labels: labels, - } - columnList = append(columnList, column) - } - - table.Kind = &v1pb.TableCatalog_Columns_{ - Columns: &v1pb.TableCatalog_Columns{ - Columns: columnList, - }, - } - - schemaCatalog.Tables = append(schemaCatalog.Tables, table) + schemaCatalog, err := convertToV1SchemaCatalog(raw) + if err != nil { + return nil, err } catalog.Schemas = append(catalog.Schemas, schemaCatalog) diff --git a/provider/resource_database_group.go b/provider/resource_database_group.go index 1b7a488..c80fba8 100644 --- a/provider/resource_database_group.go +++ b/provider/resource_database_group.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/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" @@ -18,8 +18,8 @@ import ( func resourceDatabaseGroup() *schema.Resource { return &schema.Resource{ Description: "The database group resource.", - ReadContext: internal.ResourceRead(resourceDatabaseGroupRead), - DeleteContext: internal.ResourceDelete, + ReadContext: resourceDatabaseGroupRead, + DeleteContext: resourceDatabaseGroupDelete, CreateContext: resourceDatabaseGroupCreate, UpdateContext: resourceDatabaseGroupUpdate, Importer: &schema.ResourceImporter{ @@ -71,6 +71,13 @@ func resourceDatabaseGroupRead(ctx context.Context, d *schema.ResourceData, m in group, err := c.GetDatabaseGroup(ctx, fullName, v1pb.DatabaseGroupView_DATABASE_GROUP_VIEW_FULL) if err != nil { + // Check if the resource was deleted outside of Terraform + if internal.IsNotFoundError(err) { + tflog.Warn(ctx, fmt.Sprintf("Resource %s not found, removing from state", fullName)) + // Remove from state to trigger recreation on next apply + d.SetId("") + return nil + } return diag.FromErr(err) } @@ -213,3 +220,8 @@ func resourceDatabaseGroupUpdate(ctx context.Context, d *schema.ResourceData, m return diags } + +func resourceDatabaseGroupDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + return internal.ResourceDelete(ctx, d, c.DeleteDatabaseGroup) +} diff --git a/provider/resource_environment.go b/provider/resource_environment.go index 9bc1461..c95b549 100644 --- a/provider/resource_environment.go +++ b/provider/resource_environment.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/pkg/errors" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" diff --git a/provider/resource_group.go b/provider/resource_group.go index 3ea6b17..57ecb39 100644 --- a/provider/resource_group.go +++ b/provider/resource_group.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/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" @@ -18,8 +18,8 @@ import ( func resourceGroup() *schema.Resource { return &schema.Resource{ Description: "The group resource. Workspace domain is required for creating groups.", - ReadContext: internal.ResourceRead(resourceGroupRead), - DeleteContext: internal.ResourceDelete, + ReadContext: resourceGroupRead, + DeleteContext: resourceGroupDelete, CreateContext: resourceGroupCreate, UpdateContext: resourceGroupUpdate, Importer: &schema.ResourceImporter{ @@ -67,7 +67,7 @@ func resourceGroup() *schema.Resource { ValidateDiagFunc: internal.ResourceNameValidation( fmt.Sprintf("^%s", internal.UserNamePrefix), ), - Description: "The member in users/{email} format.", + Description: "The member in users/{email} format. Only allow add end users to the group.", }, "role": { Type: schema.TypeString, @@ -92,6 +92,13 @@ func resourceGroupRead(ctx context.Context, d *schema.ResourceData, m interface{ fullName := d.Id() group, err := c.GetGroup(ctx, fullName) if err != nil { + // Check if the resource was deleted outside of Terraform + if internal.IsNotFoundError(err) { + tflog.Warn(ctx, fmt.Sprintf("Resource %s not found, removing from state", fullName)) + // Remove from state to trigger recreation on next apply + d.SetId("") + return nil + } return diag.FromErr(err) } @@ -212,6 +219,17 @@ func resourceGroupUpdate(ctx context.Context, d *schema.ResourceData, m interfac return diags } +func convertToV1Member(rawSchema interface{}) *v1pb.GroupMember { + rawMember := rawSchema.(map[string]interface{}) + + member := rawMember["member"].(string) + role := v1pb.GroupMember_Role(v1pb.GroupMember_Role_value[rawMember["role"].(string)]) + return &v1pb.GroupMember{ + Member: member, + Role: role, + } +} + func convertToMemberList(d *schema.ResourceData) ([]*v1pb.GroupMember, error) { memberSet, ok := d.Get("members").(*schema.Set) if !ok { @@ -221,16 +239,9 @@ func convertToMemberList(d *schema.ResourceData) ([]*v1pb.GroupMember, error) { memberList := []*v1pb.GroupMember{} existOwner := false for _, m := range memberSet.List() { - rawMember := m.(map[string]interface{}) - - member := rawMember["member"].(string) - role := v1pb.GroupMember_Role(v1pb.GroupMember_Role_value[rawMember["role"].(string)]) - memberList = append(memberList, &v1pb.GroupMember{ - Member: member, - Role: role, - }) - - if role == v1pb.GroupMember_OWNER { + member := convertToV1Member(m) + memberList = append(memberList, member) + if member.Role == v1pb.GroupMember_OWNER { existOwner = true } } @@ -241,3 +252,8 @@ func convertToMemberList(d *schema.ResourceData) ([]*v1pb.GroupMember, error) { return memberList, nil } + +func resourceGroupDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + return internal.ResourceDelete(ctx, d, c.DeleteGroup) +} diff --git a/provider/resource_iam_policy.go b/provider/resource_iam_policy.go index e27667d..fe8df84 100644 --- a/provider/resource_iam_policy.go +++ b/provider/resource_iam_policy.go @@ -11,7 +11,7 @@ import ( "github.com/pkg/errors" "google.golang.org/genproto/googleapis/type/expr" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -81,6 +81,84 @@ func resourceIAMPolicyDelete(_ context.Context, d *schema.ResourceData, _ interf return diags } +func convertToV1Binding(rawSchema interface{}) (*v1pb.Binding, error) { + rawBinding := rawSchema.(map[string]interface{}) + + role := rawBinding["role"].(string) + if !strings.HasPrefix(role, internal.RoleNamePrefix) { + return nil, errors.Errorf("invalid role format, role must in roles/{id} format") + } + + binding := &v1pb.Binding{ + Role: role, + } + + members, ok := rawBinding["members"].(*schema.Set) + if !ok { + return nil, errors.Errorf("invalid members") + } + if members.Len() == 0 { + return nil, errors.Errorf("empty members") + } + for _, member := range members.List() { + if err := internal.ValidateMemberBinding(member.(string)); err != nil { + return nil, errors.Wrapf(err, "invalid member: %v", member) + } + binding.Members = append(binding.Members, member.(string)) + } + + if condition, ok := rawBinding["condition"].(*schema.Set); ok { + if condition.Len() > 1 { + return nil, errors.Errorf("should only set one condition") + } + if condition.Len() == 1 && condition.List()[0] != nil { + conditionExpr, err := convertToV1Condition(condition.List()[0]) + if err != nil { + return nil, err + } + binding.Condition = conditionExpr + } + } else { + binding.Condition = &expr.Expr{ + Expression: "", + } + } + return binding, nil +} + +func convertToV1Condition(rawSchema interface{}) (*expr.Expr, error) { + rawCondition := rawSchema.(map[string]interface{}) + expressions := []string{} + + if database, ok := rawCondition["database"].(string); ok && database != "" { + expressions = append(expressions, fmt.Sprintf(`resource.database == "%s"`, database)) + } + if schema, ok := rawCondition["schema"].(string); ok { + expressions = append(expressions, fmt.Sprintf(`resource.schema == "%s"`, schema)) + } + if tables, ok := rawCondition["tables"].(*schema.Set); ok && tables.Len() > 0 { + tableList := []string{} + for _, table := range tables.List() { + tableList = append(tableList, fmt.Sprintf(`"%s"`, table.(string))) + } + expressions = append(expressions, fmt.Sprintf(`resource.table in [%s]`, strings.Join(tableList, ","))) + } + if rowLimit, ok := rawCondition["row_limit"].(int); ok && rowLimit > 0 { + expressions = append(expressions, fmt.Sprintf(`request.row_limit <= %d`, rowLimit)) + } + if expire, ok := rawCondition["expire_timestamp"].(string); ok && expire != "" { + formattedTime, err := time.Parse(time.RFC3339, expire) + if err != nil { + return nil, errors.Wrapf(err, "invalid time: %v", expire) + } + expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339))) + } + + return &expr.Expr{ + Expression: strings.Join(expressions, " && "), + }, nil +} + func convertToIAMPolicy(d *schema.ResourceData) (*v1pb.IamPolicy, error) { rawList, ok := d.Get("iam_policy").([]interface{}) if !ok || len(rawList) != 1 { @@ -95,66 +173,10 @@ func convertToIAMPolicy(d *schema.ResourceData) (*v1pb.IamPolicy, error) { policy := &v1pb.IamPolicy{} - for _, binding := range bindingList.List() { - rawBinding := binding.(map[string]interface{}) - - role := rawBinding["role"].(string) - if !strings.HasPrefix(role, internal.RoleNamePrefix) { - return nil, errors.Errorf("invalid role format, role must in roles/{id} format") - } - - binding := &v1pb.Binding{ - Role: role, - } - - members, ok := rawBinding["members"].(*schema.Set) - if !ok { - return nil, errors.Errorf("invalid members") - } - if members.Len() == 0 { - return nil, errors.Errorf("empty members") - } - for _, member := range members.List() { - if err := internal.ValidateMemberBinding(member.(string)); err != nil { - return nil, errors.Wrapf(err, "invalid member: %v", member) - } - binding.Members = append(binding.Members, member.(string)) - } - - expressions := []string{} - if condition, ok := rawBinding["condition"].(*schema.Set); ok { - if condition.Len() > 1 { - return nil, errors.Errorf("should only set one condition") - } - if condition.Len() == 1 && condition.List()[0] != nil { - rawCondition := condition.List()[0].(map[string]interface{}) - if database, ok := rawCondition["database"].(string); ok && database != "" { - expressions = append(expressions, fmt.Sprintf(`resource.database == "%s"`, database)) - } - if schema, ok := rawCondition["schema"].(string); ok { - expressions = append(expressions, fmt.Sprintf(`resource.schema == "%s"`, schema)) - } - if tables, ok := rawCondition["tables"].(*schema.Set); ok && tables.Len() > 0 { - tableList := []string{} - for _, table := range tables.List() { - tableList = append(tableList, fmt.Sprintf(`"%s"`, table.(string))) - } - expressions = append(expressions, fmt.Sprintf(`resource.table in [%s]`, strings.Join(tableList, ","))) - } - if rowLimit, ok := rawCondition["row_limit"].(int); ok && rowLimit > 0 { - expressions = append(expressions, fmt.Sprintf(`request.row_limit <= %d`, rowLimit)) - } - if expire, ok := rawCondition["expire_timestamp"].(string); ok && expire != "" { - formattedTime, err := time.Parse(time.RFC3339, expire) - if err != nil { - return nil, errors.Wrapf(err, "invalid time: %v", expire) - } - expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339))) - } - } - } - binding.Condition = &expr.Expr{ - Expression: strings.Join(expressions, " && "), + for _, raw := range bindingList.List() { + binding, err := convertToV1Binding(raw) + if err != nil { + return nil, err } policy.Bindings = append(policy.Bindings, binding) } diff --git a/provider/resource_instance.go b/provider/resource_instance.go index 5b26f5c..26aca6c 100644 --- a/provider/resource_instance.go +++ b/provider/resource_instance.go @@ -1,7 +1,6 @@ package provider import ( - "bytes" "context" "fmt" @@ -12,7 +11,7 @@ import ( "github.com/pkg/errors" "google.golang.org/protobuf/types/known/durationpb" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -22,9 +21,9 @@ func resourceInstance() *schema.Resource { return &schema.Resource{ Description: "The instance resource.", CreateWithoutTimeout: resourceInstanceCreate, - ReadWithoutTimeout: internal.ResourceRead(resourceInstanceRead), + ReadWithoutTimeout: resourceInstanceRead, UpdateContext: resourceInstanceUpdate, - DeleteContext: internal.ResourceDelete, + DeleteContext: resourceInstanceDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -37,7 +36,7 @@ func resourceInstance() *schema.Resource { }, "environment": { Type: schema.TypeString, - Required: true, + Optional: true, ValidateDiagFunc: internal.ResourceNameValidation( fmt.Sprintf("^%s%s$", internal.EnvironmentNamePrefix, internal.ResourceIDPattern), ), @@ -82,15 +81,15 @@ func resourceInstance() *schema.Resource { Type: schema.TypeInt, Optional: true, Computed: true, - Description: "How often the instance is synced in seconds. Default 0, means never sync.", + Description: "How often the instance is synced in seconds. Default 0, means never sync. Require instance license to enable this feature.", }, "maximum_connections": { Type: schema.TypeInt, Optional: true, Computed: true, - Description: "The maximum number of connections.", + Description: "The maximum number of connections. Require instance license to enable this feature.", }, - "sync_databases": getSyncDatabasesSchema(true), + "sync_databases": getSyncDatabasesSchema(false), "data_sources": { Type: schema.TypeSet, Required: true, @@ -110,7 +109,7 @@ func resourceInstance() *schema.Resource { v1pb.DataSourceType_ADMIN.String(), v1pb.DataSourceType_READ_ONLY.String(), }, false), - Description: "The data source type. Should be ADMIN or READ_ONLY.", + Description: "The data source type. Should be ADMIN or READ_ONLY. The READ_ONLY data source requires the instance license.", }, "username": { Type: schema.TypeString, @@ -130,13 +129,15 @@ func resourceInstance() *schema.Resource { Type: schema.TypeList, Optional: true, MaxItems: 1, - Description: "The external secret to get the database password. Learn more: https://www.bytebase.com/docs/get-started/instance/#use-external-secret-manager", + MinItems: 0, + Description: "The external secret to get the database password. Require instance license to enable this feature. Learn more: https://www.bytebase.com/docs/get-started/instance/#use-external-secret-manager", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "vault": { Type: schema.TypeList, Optional: true, MaxItems: 1, + MinItems: 0, Description: "The Valut to get the database password. Reference doc https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -155,6 +156,7 @@ func resourceInstance() *schema.Resource { Type: schema.TypeList, Optional: true, MaxItems: 1, + MinItems: 0, Description: "The Vault app role to get the password.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -204,6 +206,7 @@ func resourceInstance() *schema.Resource { Type: schema.TypeList, Optional: true, MaxItems: 1, + MinItems: 0, Description: "The AWS Secrets Manager to get the database password. Reference doc https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -224,6 +227,7 @@ func resourceInstance() *schema.Resource { Type: schema.TypeList, Optional: true, MaxItems: 1, + MinItems: 0, Description: "The GCP Secret Manager to get the database password. Reference doc https://cloud.google.com/secret-manager/docs", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -344,13 +348,16 @@ func resourceInstanceCreate(ctx context.Context, d *schema.ResourceData, m inter Title: d.Get("title").(string), ExternalLink: d.Get("external_link").(string), DataSources: dataSourceList, - Environment: d.Get("environment").(string), Activation: d.Get("activation").(bool), State: v1pb.State_ACTIVE, MaximumConnections: int32(d.Get("maximum_connections").(int)), Engine: v1pb.Engine(v1pb.Engine_value[d.Get("engine").(string)]), SyncDatabases: getSyncDatabases(d), } + environment := d.Get("environment").(string) + if environment != "" { + instance.Environment = &environment + } rawConfig := d.GetRawConfig() if config := rawConfig.GetAttr("sync_interval"); !config.IsNull() { instance.SyncInterval = &durationpb.Duration{ @@ -468,6 +475,13 @@ func resourceInstanceRead(ctx context.Context, d *schema.ResourceData, m interfa instance, err := c.GetInstance(ctx, instanceName) if err != nil { + // Check if the resource was deleted outside of Terraform + if internal.IsNotFoundError(err) { + tflog.Warn(ctx, fmt.Sprintf("Resource %s not found, removing from state", instanceName)) + // Remove from state to trigger recreation on next apply + d.SetId("") + return nil + } return diag.FromErr(err) } @@ -500,6 +514,7 @@ func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, m inter c := m.(api.Client) instanceName := d.Id() + environment := d.Get("environment").(string) existedInstance, err := c.GetInstance(ctx, instanceName) if err != nil { @@ -559,7 +574,7 @@ func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, m inter Name: instanceName, Title: d.Get("title").(string), ExternalLink: d.Get("external_link").(string), - Environment: d.Get("environment").(string), + Environment: &environment, Activation: d.Get("activation").(bool), DataSources: dataSourceList, State: v1pb.State_ACTIVE, @@ -611,6 +626,11 @@ func setInstanceMessage( if err := d.Set("name", instance.Name); err != nil { return diag.Errorf("cannot set name for instance: %s", err.Error()) } + if v := instance.Environment; v != nil { + if err := d.Set("environment", *v); err != nil { + return diag.Errorf("cannot set environment for instance: %s", err.Error()) + } + } if err := d.Set("environment", instance.Environment); err != nil { return diag.Errorf("cannot set environment for instance: %s", err.Error()) } @@ -749,44 +769,95 @@ func flattenDataSourceList(d *schema.ResourceData, dataSourceList []*v1pb.DataSo } func dataSourceHash(rawDataSource interface{}) int { - var buf bytes.Buffer - raw := rawDataSource.(map[string]interface{}) - - if v, ok := raw["id"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) + ds, err := convertToV1DataSource(rawDataSource) + if err != nil { + return 0 } - if v, ok := raw["username"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) + return internal.ToHash(ds) +} + +func convertToV1DataSource(raw interface{}) (*v1pb.DataSource, error) { + obj := raw.(map[string]interface{}) + dataSource := &v1pb.DataSource{ + Id: obj["id"].(string), + Type: v1pb.DataSourceType(v1pb.DataSourceType_value[obj["type"].(string)]), + } + + if v, ok := obj["username"].(string); ok { + dataSource.Username = v + } + if v, ok := obj["password"].(string); ok && v != "" { + dataSource.Password = v + } + if v, ok := obj["external_secret"].([]interface{}); ok && len(v) == 1 { + externalSecret := &v1pb.DataSourceExternalSecret{} + rawExternalSecret := v[0].(map[string]interface{}) + if v, ok := rawExternalSecret["vault"].([]interface{}); ok && len(v) == 1 { + rawVault := v[0].(map[string]interface{}) + externalSecret.SecretType = v1pb.DataSourceExternalSecret_VAULT_KV_V2 + externalSecret.Url = rawVault["url"].(string) + externalSecret.EngineName = rawVault["engine_name"].(string) + externalSecret.SecretName = rawVault["secret_name"].(string) + externalSecret.PasswordKeyName = rawVault["password_key_name"].(string) + + if token, ok := rawVault["token"].(string); ok && token != "" { + externalSecret.AuthType = v1pb.DataSourceExternalSecret_TOKEN + externalSecret.AuthOption = &v1pb.DataSourceExternalSecret_Token{ + Token: token, + } + } else if v, ok := rawVault["app_role"].([]interface{}); ok && len(v) == 1 { + rawAppRole := v[0].(map[string]interface{}) + externalSecret.AuthType = v1pb.DataSourceExternalSecret_VAULT_APP_ROLE + externalSecret.AuthOption = &v1pb.DataSourceExternalSecret_AppRole{ + AppRole: &v1pb.DataSourceExternalSecret_AppRoleAuthOption{ + RoleId: rawAppRole["role_id"].(string), + SecretId: rawAppRole["secret"].(string), + Type: v1pb.DataSourceExternalSecret_AppRoleAuthOption_SecretType(v1pb.DataSourceExternalSecret_AppRoleAuthOption_SecretType_value[rawAppRole["secret_type"].(string)]), + }, + } + } else { + return nil, errors.Errorf("require token or app_role for Vault") + } + } else if v, ok := rawExternalSecret["aws_secrets_manager"].([]interface{}); ok && len(v) == 1 { + rawAWS := v[0].(map[string]interface{}) + externalSecret.SecretType = v1pb.DataSourceExternalSecret_AWS_SECRETS_MANAGER + externalSecret.SecretName = rawAWS["secret_name"].(string) + externalSecret.PasswordKeyName = rawAWS["password_key_name"].(string) + } else if v, ok := rawExternalSecret["gcp_secret_manager"].([]interface{}); ok && len(v) == 1 { + rawGCP := v[0].(map[string]interface{}) + externalSecret.SecretType = v1pb.DataSourceExternalSecret_GCP_SECRET_MANAGER + externalSecret.SecretName = rawGCP["secret_name"].(string) + } else { + return nil, errors.Errorf("must set one of vault, aws_secrets_manager or gcp_secret_manager") + } + dataSource.ExternalSecret = externalSecret } - if v, ok := raw["password"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) + if dataSource.Password != "" && dataSource.ExternalSecret != nil { + return nil, errors.Errorf("cannot set both password and external_secret") } - if v, ok := raw["host"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) + + if v, ok := obj["use_ssl"].(bool); ok { + dataSource.UseSsl = v } - if v, ok := raw["port"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) + if v, ok := obj["ssl_ca"].(string); ok { + dataSource.SslCa = v } - if v, ok := raw["database"].(string); ok { - _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) + if v, ok := obj["ssl_cert"].(string); ok { + dataSource.SslCert = v } - - // Include use_ssl in hash to detect SSL enablement changes - if v, ok := raw["use_ssl"].(bool); ok { - _, _ = buf.WriteString(fmt.Sprintf("ssl_%v-", v)) + if v, ok := obj["ssl_key"].(string); ok { + dataSource.SslKey = v } - // Include whether SSL certificates are present (not the values themselves) - if v, ok := raw["ssl_ca"].(string); ok && v != "" { - _, _ = buf.WriteString(fmt.Sprintf("ca_present_%s-", v)) + if v, ok := obj["host"].(string); ok { + dataSource.Host = v } - if v, ok := raw["ssl_cert"].(string); ok && v != "" { - _, _ = buf.WriteString(fmt.Sprintf("cert_present_%s-", v)) + if v, ok := obj["port"].(string); ok { + dataSource.Port = v } - if v, ok := raw["ssl_key"].(string); ok && v != "" { - _, _ = buf.WriteString(fmt.Sprintf("key_present_%s-", v)) + if v, ok := obj["database"].(string); ok { + dataSource.Database = v } - - return internal.ToHashcodeInt(buf.String()) + return dataSource, nil } func convertDataSourceCreateList(d *schema.ResourceData, validate bool) ([]*v1pb.DataSource, error) { @@ -798,90 +869,14 @@ func convertDataSourceCreateList(d *schema.ResourceData, validate bool) ([]*v1pb dataSourceTypeMap := map[v1pb.DataSourceType]bool{} for _, raw := range dataSourceSet.List() { - obj := raw.(map[string]interface{}) - dataSource := &v1pb.DataSource{ - Id: obj["id"].(string), - Type: v1pb.DataSourceType(v1pb.DataSourceType_value[obj["type"].(string)]), + dataSource, err := convertToV1DataSource(raw) + if err != nil { + return nil, err } if dataSourceTypeMap[dataSource.Type] && dataSource.Type == v1pb.DataSourceType_ADMIN { return nil, errors.Errorf("duplicate data source type ADMIN") } dataSourceTypeMap[dataSource.Type] = true - - if v, ok := obj["username"].(string); ok { - dataSource.Username = v - } - if v, ok := obj["password"].(string); ok && v != "" { - dataSource.Password = v - } - if v, ok := obj["external_secret"].([]interface{}); ok && len(v) == 1 { - externalSecret := &v1pb.DataSourceExternalSecret{} - rawExternalSecret := v[0].(map[string]interface{}) - if v, ok := rawExternalSecret["vault"].([]interface{}); ok && len(v) == 1 { - rawVault := v[0].(map[string]interface{}) - externalSecret.SecretType = v1pb.DataSourceExternalSecret_VAULT_KV_V2 - externalSecret.Url = rawVault["url"].(string) - externalSecret.EngineName = rawVault["engine_name"].(string) - externalSecret.SecretName = rawVault["secret_name"].(string) - externalSecret.PasswordKeyName = rawVault["password_key_name"].(string) - - if token, ok := rawVault["token"].(string); ok && token != "" { - externalSecret.AuthType = v1pb.DataSourceExternalSecret_TOKEN - externalSecret.AuthOption = &v1pb.DataSourceExternalSecret_Token{ - Token: token, - } - } else if v, ok := rawVault["app_role"].([]interface{}); ok && len(v) == 1 { - rawAppRole := v[0].(map[string]interface{}) - externalSecret.AuthType = v1pb.DataSourceExternalSecret_VAULT_APP_ROLE - externalSecret.AuthOption = &v1pb.DataSourceExternalSecret_AppRole{ - AppRole: &v1pb.DataSourceExternalSecret_AppRoleAuthOption{ - RoleId: rawAppRole["role_id"].(string), - SecretId: rawAppRole["secret"].(string), - Type: v1pb.DataSourceExternalSecret_AppRoleAuthOption_SecretType(v1pb.DataSourceExternalSecret_AppRoleAuthOption_SecretType_value[rawAppRole["secret_type"].(string)]), - }, - } - } else { - return nil, errors.Errorf("require token or app_role for Vault") - } - } else if v, ok := rawExternalSecret["aws_secrets_manager"].([]interface{}); ok && len(v) == 1 { - rawAWS := v[0].(map[string]interface{}) - externalSecret.SecretType = v1pb.DataSourceExternalSecret_AWS_SECRETS_MANAGER - externalSecret.SecretName = rawAWS["secret_name"].(string) - externalSecret.PasswordKeyName = rawAWS["password_key_name"].(string) - } else if v, ok := rawExternalSecret["gcp_secret_manager"].([]interface{}); ok && len(v) == 1 { - rawGCP := v[0].(map[string]interface{}) - externalSecret.SecretType = v1pb.DataSourceExternalSecret_GCP_SECRET_MANAGER - externalSecret.SecretName = rawGCP["secret_name"].(string) - } else { - return nil, errors.Errorf("must set one of vault, aws_secrets_manager or gcp_secret_manager") - } - dataSource.ExternalSecret = externalSecret - } - if dataSource.Password != "" && dataSource.ExternalSecret != nil { - return nil, errors.Errorf("cannot set both password and external_secret") - } - - if v, ok := obj["use_ssl"].(bool); ok { - dataSource.UseSsl = v - } - if v, ok := obj["ssl_ca"].(string); ok { - dataSource.SslCa = v - } - if v, ok := obj["ssl_cert"].(string); ok { - dataSource.SslCert = v - } - if v, ok := obj["ssl_key"].(string); ok { - dataSource.SslKey = v - } - if v, ok := obj["host"].(string); ok { - dataSource.Host = v - } - if v, ok := obj["port"].(string); ok { - dataSource.Port = v - } - if v, ok := obj["database"].(string); ok { - dataSource.Database = v - } dataSourceList = append(dataSourceList, dataSource) } @@ -891,3 +886,8 @@ func convertDataSourceCreateList(d *schema.ResourceData, validate bool) ([]*v1pb return dataSourceList, nil } + +func resourceInstanceDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + return internal.ResourceDelete(ctx, d, c.DeleteInstance) +} diff --git a/provider/resource_instance_test.go b/provider/resource_instance_test.go index b872f9e..20d4ce3 100644 --- a/provider/resource_instance_test.go +++ b/provider/resource_instance_test.go @@ -153,7 +153,7 @@ func testAccCheckInstanceDestroy(s *terraform.State) error { continue } - if err := c.DeleteResource(context.Background(), rs.Primary.ID); err != nil { + if err := c.DeleteInstance(context.Background(), rs.Primary.ID); err != nil { return err } } diff --git a/provider/resource_policy.go b/provider/resource_policy.go index 9ca4e12..4d4466c 100644 --- a/provider/resource_policy.go +++ b/provider/resource_policy.go @@ -12,7 +12,7 @@ import ( "github.com/pkg/errors" "google.golang.org/genproto/googleapis/type/expr" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -128,6 +128,7 @@ func resourcePolicyCreate(ctx context.Context, d *schema.ResourceData, m interfa Enforce: d.Get("enforce").(bool), Type: policyType, } + updateMasks := []string{} switch policyType { case v1pb.PolicyType_MASKING_EXCEPTION: @@ -138,6 +139,7 @@ func resourcePolicyCreate(ctx context.Context, d *schema.ResourceData, m interfa patch.Policy = &v1pb.Policy_MaskingExceptionPolicy{ MaskingExceptionPolicy: maskingExceptionPolicy, } + updateMasks = append(updateMasks, "masking_exception_policy") case v1pb.PolicyType_MASKING_RULE: maskingRulePolicy, err := convertToMaskingRulePolicy(d) if err != nil { @@ -146,6 +148,7 @@ func resourcePolicyCreate(ctx context.Context, d *schema.ResourceData, m interfa patch.Policy = &v1pb.Policy_MaskingRulePolicy{ MaskingRulePolicy: maskingRulePolicy, } + updateMasks = append(updateMasks, "masking_rule_policy") case v1pb.PolicyType_DISABLE_COPY_DATA: if !strings.HasPrefix(policyName, internal.EnvironmentNamePrefix) && !strings.HasPrefix(policyName, internal.ProjectNamePrefix) { return diag.Errorf("policy %v only support environment or project resource", policyName) @@ -157,6 +160,7 @@ func resourcePolicyCreate(ctx context.Context, d *schema.ResourceData, m interfa patch.Policy = &v1pb.Policy_DisableCopyDataPolicy{ DisableCopyDataPolicy: disableCopyDataPolicy, } + updateMasks = append(updateMasks, "disable_copy_data_policy") case v1pb.PolicyType_DATA_SOURCE_QUERY: if !strings.HasPrefix(policyName, internal.EnvironmentNamePrefix) && !strings.HasPrefix(policyName, internal.ProjectNamePrefix) { return diag.Errorf("policy %v only support environment or project resource", policyName) @@ -168,6 +172,7 @@ func resourcePolicyCreate(ctx context.Context, d *schema.ResourceData, m interfa patch.Policy = &v1pb.Policy_DataSourceQueryPolicy{ DataSourceQueryPolicy: dataSourceQueryPolicy, } + updateMasks = append(updateMasks, "data_source_query_policy") case v1pb.PolicyType_ROLLOUT_POLICY: if !strings.HasPrefix(policyName, internal.EnvironmentNamePrefix) { return diag.Errorf("policy %v only support environment resource", policyName) @@ -179,11 +184,11 @@ func resourcePolicyCreate(ctx context.Context, d *schema.ResourceData, m interfa patch.Policy = &v1pb.Policy_RolloutPolicy{ RolloutPolicy: rolloutPolicy, } + updateMasks = append(updateMasks, "rollout_policy") default: return diag.Errorf("unsupport policy type: %v", policyName) } - updateMasks := []string{"payload"} rawConfig := d.GetRawConfig() if config := rawConfig.GetAttr("inherit_from_parent"); !config.IsNull() { updateMasks = append(updateMasks, "inherit_from_parent") @@ -242,7 +247,7 @@ func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interfa } if d.HasChange("masking_exception_policy") { - updateMasks = append(updateMasks, "payload") + updateMasks = append(updateMasks, "masking_exception_policy") maskingExceptionPolicy, err := convertToMaskingExceptionPolicy(d) if err != nil { return diag.FromErr(err) @@ -252,7 +257,7 @@ func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interfa } } if d.HasChange("global_masking_policy") { - updateMasks = append(updateMasks, "payload") + updateMasks = append(updateMasks, "masking_rule_policy") maskingRulePolicy, err := convertToMaskingRulePolicy(d) if err != nil { return diag.FromErr(err) @@ -262,7 +267,7 @@ func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interfa } } if d.HasChange("disable_copy_data_policy") { - updateMasks = append(updateMasks, "payload") + updateMasks = append(updateMasks, "disable_copy_data_policy") disableCopyDataPolicy, err := convertToDisableCopyDataPolicy(d) if err != nil { return diag.FromErr(err) @@ -272,7 +277,7 @@ func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interfa } } if d.HasChange("data_source_query_policy") { - updateMasks = append(updateMasks, "payload") + updateMasks = append(updateMasks, "data_source_query_policy") dataSourceQueryPolicy, err := convertToDataSourceQueryPolicy(d) if err != nil { return diag.FromErr(err) @@ -281,6 +286,16 @@ func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interfa DataSourceQueryPolicy: dataSourceQueryPolicy, } } + if d.HasChange("rollout_policy") { + updateMasks = append(updateMasks, "rollout_policy") + rolloutPolicy, err := convertToRolloutPolicy(d) + if err != nil { + return diag.FromErr(err) + } + patch.Policy = &v1pb.Policy_RolloutPolicy{ + RolloutPolicy: rolloutPolicy, + } + } var diags diag.Diagnostics if len(updateMasks) > 0 { @@ -332,6 +347,59 @@ func convertToMaskingRulePolicy(d *schema.ResourceData) (*v1pb.MaskingRulePolicy return policy, nil } +func convertToV1Exception(rawSchema interface{}) (*v1pb.MaskingExceptionPolicy_MaskingException, error) { + rawException := rawSchema.(map[string]interface{}) + + expressions := []string{} + databaseFullName := rawException["database"].(string) + if databaseFullName != "" { + instanceID, databaseName, err := internal.GetInstanceDatabaseID(databaseFullName) + if err != nil { + return nil, errors.Wrapf(err, "invalid database full name: %v", databaseFullName) + } + expressions = append( + expressions, + 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 != "" { + formattedTime, err := time.Parse(time.RFC3339, expire) + if err != nil { + return nil, errors.Wrapf(err, "invalid time: %v", expire) + } + expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339))) + } + member := rawException["member"].(string) + if member == "allUsers" { + return nil, errors.Errorf("not support allUsers in masking_exception_policy") + } + if err := internal.ValidateMemberBinding(member); err != nil { + return nil, err + } + return &v1pb.MaskingExceptionPolicy_MaskingException{ + Member: member, + Action: v1pb.MaskingExceptionPolicy_MaskingException_Action( + v1pb.MaskingExceptionPolicy_MaskingException_Action_value[rawException["action"].(string)], + ), + Condition: &expr.Expr{ + Description: rawException["reason"].(string), + Expression: strings.Join(expressions, " && "), + }, + }, nil +} + func convertToMaskingExceptionPolicy(d *schema.ResourceData) (*v1pb.MaskingExceptionPolicy, error) { rawList, ok := d.Get("masking_exception_policy").([]interface{}) if !ok || len(rawList) != 1 { @@ -346,57 +414,12 @@ func convertToMaskingExceptionPolicy(d *schema.ResourceData) (*v1pb.MaskingExcep policy := &v1pb.MaskingExceptionPolicy{} - for _, exception := range exceptionList.List() { - rawException := exception.(map[string]interface{}) - - expressions := []string{} - databaseFullName := rawException["database"].(string) - if databaseFullName != "" { - instanceID, databaseName, err := internal.GetInstanceDatabaseID(databaseFullName) - if err != nil { - return nil, errors.Wrapf(err, "invalid database full name: %v", databaseFullName) - } - expressions = append( - expressions, - 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 != "" { - formattedTime, err := time.Parse(time.RFC3339, expire) - if err != nil { - return nil, errors.Wrapf(err, "invalid time: %v", expire) - } - expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339))) - } - member := rawException["member"].(string) - if member == "allUsers" { - return nil, errors.Errorf("not support allUsers in masking_exception_policy") - } - if err := internal.ValidateMemberBinding(member); err != nil { + for _, raw := range exceptionList.List() { + exception, err := convertToV1Exception(raw) + if err != nil { return nil, err } - policy.MaskingExceptions = append(policy.MaskingExceptions, &v1pb.MaskingExceptionPolicy_MaskingException{ - Member: member, - Action: v1pb.MaskingExceptionPolicy_MaskingException_Action( - v1pb.MaskingExceptionPolicy_MaskingException_Action_value[rawException["action"].(string)], - ), - Condition: &expr.Expr{ - Description: rawException["reason"].(string), - Expression: strings.Join(expressions, " && "), - }, - }) + policy.MaskingExceptions = append(policy.MaskingExceptions, exception) } return policy, nil } diff --git a/provider/resource_policy_test.go b/provider/resource_policy_test.go index 104c812..cd1eadd 100644 --- a/provider/resource_policy_test.go +++ b/provider/resource_policy_test.go @@ -5,7 +5,7 @@ import ( "fmt" "testing" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/pkg/errors" diff --git a/provider/resource_project.go b/provider/resource_project.go index 6c23ca8..36395f1 100644 --- a/provider/resource_project.go +++ b/provider/resource_project.go @@ -14,7 +14,7 @@ import ( "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" ) // defaultProj is the default project name. @@ -26,7 +26,7 @@ func resourceProjct() *schema.Resource { CreateWithoutTimeout: resourceProjectCreate, ReadWithoutTimeout: resourceProjectRead, UpdateWithoutTimeout: resourceProjectUpdate, - DeleteContext: internal.ResourceDelete, + DeleteContext: resourceProjectDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -373,6 +373,11 @@ func resourceProjectRead(ctx context.Context, d *schema.ResourceData, m interfac return resp } +func resourceProjectDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + return internal.ResourceDelete(ctx, d, c.DeleteProject) +} + const batchSize = 100 func updateDatabasesInProject(ctx context.Context, d *schema.ResourceData, client api.Client, projectName string) diag.Diagnostics { @@ -426,10 +431,7 @@ func updateDatabasesInProject(ctx context.Context, d *schema.ResourceData, clien }) for i := 0; i < len(batchTransferDatabases); i += batchSize { - end := i + batchSize - if end > len(batchTransferDatabases) { - end = len(batchTransferDatabases) - } + end := min(i+batchSize, len(batchTransferDatabases)) batch := batchTransferDatabases[i:end] startTime := time.Now() @@ -480,6 +482,31 @@ func updateDatabasesInProject(ctx context.Context, d *schema.ResourceData, clien return nil } +func convertToV1Webhook(rawSchema interface{}) *v1pb.Webhook { + rawWebhook := rawSchema.(map[string]interface{}) + webhook := &v1pb.Webhook{ + Title: rawWebhook["title"].(string), + Url: rawWebhook["url"].(string), + Type: v1pb.Webhook_Type(v1pb.Webhook_Type_value[rawWebhook["type"].(string)]), + } + if dm, ok := rawWebhook["direct_message"].(bool); ok { + webhook.DirectMessage = dm + } + if rawTypes, ok := rawWebhook["notification_types"]; ok { + switch v := rawTypes.(type) { + case *schema.Set: + for _, n := range v.List() { + webhook.NotificationTypes = append(webhook.NotificationTypes, v1pb.Activity_Type(v1pb.Activity_Type_value[n.(string)])) + } + case []string: + for _, n := range v { + webhook.NotificationTypes = append(webhook.NotificationTypes, v1pb.Activity_Type(v1pb.Activity_Type_value[n])) + } + } + } + return webhook +} + func updateWebhooksInProject(ctx context.Context, d *schema.ResourceData, client api.Client, projectName string) diag.Diagnostics { rawConfig := d.GetRawConfig() if config := rawConfig.GetAttr("webhooks"); config.IsNull() { @@ -496,20 +523,9 @@ func updateWebhooksInProject(ctx context.Context, d *schema.ResourceData, client existedWebhookMap[webhook.Name] = webhook } - for _, w := range d.Get("webhooks").([]interface{}) { - rawWebhook := w.(map[string]interface{}) - webhook := &v1pb.Webhook{ - Name: rawWebhook["name"].(string), - Title: rawWebhook["title"].(string), - Url: rawWebhook["url"].(string), - DirectMessage: rawWebhook["direct_message"].(bool), - Type: v1pb.Webhook_Type(v1pb.Webhook_Type_value[rawWebhook["type"].(string)]), - } - notificationTypes := rawWebhook["notification_types"].(*schema.Set) - for _, n := range notificationTypes.List() { - webhook.NotificationTypes = append(webhook.NotificationTypes, v1pb.Activity_Type(v1pb.Activity_Type_value[n.(string)])) - } - + rawWebhooks := d.Get("webhooks").(*schema.Set) + for _, w := range rawWebhooks.List() { + webhook := convertToV1Webhook(w) if v, ok := existedWebhookMap[webhook.Name]; ok && v.Type == webhook.Type { // Not support change the webhook type. if _, err := client.UpdateProjectWebhook(ctx, webhook, []string{ diff --git a/provider/resource_project_test.go b/provider/resource_project_test.go index eec2108..04e931a 100644 --- a/provider/resource_project_test.go +++ b/provider/resource_project_test.go @@ -59,7 +59,7 @@ func testAccCheckProjectDestroy(s *terraform.State) error { continue } - if err := c.DeleteResource(context.Background(), rs.Primary.ID); err != nil { + if err := c.DeleteProject(context.Background(), rs.Primary.ID); err != nil { return err } } diff --git a/provider/resource_review_config.go b/provider/resource_review_config.go index 28973fd..7d1ff8a 100644 --- a/provider/resource_review_config.go +++ b/provider/resource_review_config.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/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" @@ -53,7 +53,7 @@ func resourceReviewConfig() *schema.Resource { 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.TypeList, + Type: schema.TypeSet, Required: true, MinItems: 1, Description: "The SQL review rules.", @@ -94,6 +94,7 @@ func resourceReviewConfig() *schema.Resource { }, }, }, + Set: reviewRuleHash, }, }, } @@ -105,6 +106,13 @@ func resourceReviewConfigRead(ctx context.Context, d *schema.ResourceData, m int fullName := d.Id() review, err := c.GetReviewConfig(ctx, fullName) if err != nil { + // Check if the resource was deleted outside of Terraform + if internal.IsNotFoundError(err) { + tflog.Warn(ctx, fmt.Sprintf("Resource %s not found, removing from state", fullName)) + // Remove from state to trigger recreation on next apply + d.SetId("") + return nil + } return diag.FromErr(err) } @@ -116,7 +124,7 @@ func resourceReviewConfigDelete(ctx context.Context, d *schema.ResourceData, m i resources := getReviewConfigRelatedResources(d) removeReviewConfigTag(ctx, c, resources) - return internal.ResourceDelete(ctx, d, m) + return internal.ResourceDelete(ctx, d, c.DeleteReviewConfig) } func resourceReviewConfigUpsert(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { @@ -268,7 +276,8 @@ func setReviewConfig(d *schema.ResourceData, review *v1pb.ReviewConfig) diag.Dia 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", flattenReviewRules(review.Rules)); err != nil { + rules := flattenReviewRules(review.Rules) + if err := d.Set("rules", schema.NewSet(reviewRuleHash, rules)); err != nil { return diag.Errorf("cannot set rules for review: %s", err.Error()) } @@ -289,27 +298,36 @@ func flattenReviewRules(rules []*v1pb.SQLReviewRule) []interface{} { return ruleList } +func convertToV1Rule(rawSchema interface{}) *v1pb.SQLReviewRule { + rawRule := rawSchema.(map[string]interface{}) + payload := rawRule["payload"].(string) + if payload == "" { + payload = "{}" + } + return &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)]), + } +} + func convertToV1RuleList(d *schema.ResourceData) ([]*v1pb.SQLReviewRule, error) { - ruleRawList, ok := d.Get("rules").([]interface{}) - if !ok || len(ruleRawList) == 0 { + ruleRawList, ok := d.Get("rules").(*schema.Set) + if !ok || ruleRawList.Len() == 0 { return nil, errors.Errorf("rules is required") } ruleList := []*v1pb.SQLReviewRule{} - for _, r := range ruleRawList { - 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)]), - }) + for _, r := range ruleRawList.List() { + ruleList = append(ruleList, convertToV1Rule(r)) } return ruleList, nil } + +func reviewRuleHash(rawSchema interface{}) int { + rule := convertToV1Rule(rawSchema) + return internal.ToHash(rule) +} diff --git a/provider/resource_risk.go b/provider/resource_risk.go index a42dfb0..edd5526 100644 --- a/provider/resource_risk.go +++ b/provider/resource_risk.go @@ -4,12 +4,13 @@ import ( "context" "fmt" + "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" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -18,8 +19,8 @@ import ( func resourceRisk() *schema.Resource { return &schema.Resource{ Description: "The risk resource. Require ENTERPRISE subscription. Check the docs https://www.bytebase.com/docs/administration/risk-center?source=terraform for more information.", - ReadContext: internal.ResourceRead(resourceRiskRead), - DeleteContext: internal.ResourceDelete, + ReadContext: resourceRiskRead, + DeleteContext: resourceRiskDelete, CreateContext: resourceRiskCreate, UpdateContext: resourceRiskUpdate, Importer: &schema.ResourceImporter{ @@ -78,6 +79,13 @@ func resourceRiskRead(ctx context.Context, d *schema.ResourceData, m interface{} fullName := d.Id() risk, err := c.GetRisk(ctx, fullName) if err != nil { + // Check if the resource was deleted outside of Terraform + if internal.IsNotFoundError(err) { + tflog.Warn(ctx, fmt.Sprintf("Resource %s not found, removing from state", fullName)) + // Remove from state to trigger recreation on next apply + d.SetId("") + return nil + } return diag.FromErr(err) } @@ -180,3 +188,8 @@ func resourceRiskUpdate(ctx context.Context, d *schema.ResourceData, m interface return diags } + +func resourceRiskDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + return internal.ResourceDelete(ctx, d, c.DeleteRisk) +} diff --git a/provider/resource_role.go b/provider/resource_role.go index 2b5ed78..7fab27c 100644 --- a/provider/resource_role.go +++ b/provider/resource_role.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/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" @@ -18,8 +18,8 @@ import ( func resourceRole() *schema.Resource { return &schema.Resource{ Description: "The role resource. Require ENTERPRISE subscription. Check the docs https://www.bytebase.com/docs/administration/custom-roles/?source=terraform for more information.", - ReadContext: internal.ResourceRead(resourceRoleRead), - DeleteContext: internal.ResourceDelete, + ReadContext: resourceRoleRead, + DeleteContext: resourceRoleDelete, CreateContext: resourceRoleCreate, UpdateContext: resourceRoleUpdate, Importer: &schema.ResourceImporter{ @@ -73,6 +73,13 @@ func resourceRoleRead(ctx context.Context, d *schema.ResourceData, m interface{} fullName := d.Id() role, err := c.GetRole(ctx, fullName) if err != nil { + // Check if the resource was deleted outside of Terraform + if internal.IsNotFoundError(err) { + tflog.Warn(ctx, fmt.Sprintf("Resource %s not found, removing from state", fullName)) + // Remove from state to trigger recreation on next apply + d.SetId("") + return nil + } return diag.FromErr(err) } @@ -247,3 +254,8 @@ func resourceRoleUpdate(ctx context.Context, d *schema.ResourceData, m interface return diags } + +func resourceRoleDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + return internal.ResourceDelete(ctx, d, c.DeleteRole) +} diff --git a/provider/resource_setting.go b/provider/resource_setting.go index 66f09f1..4ae10a0 100644 --- a/provider/resource_setting.go +++ b/provider/resource_setting.go @@ -5,13 +5,14 @@ import ( "fmt" "strings" + "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/pkg/errors" "google.golang.org/genproto/googleapis/type/expr" "google.golang.org/protobuf/types/known/durationpb" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "github.com/bytebase/terraform-provider-bytebase/api" "github.com/bytebase/terraform-provider-bytebase/provider/internal" @@ -284,6 +285,29 @@ func convertToV1WorkspaceProfileSetting(d *schema.ResourceData) (*v1pb.Workspace return workspacePrfile, updateMasks, nil } +func convertToV1Level(rawSchema interface{}) *v1pb.DataClassificationSetting_DataClassificationConfig_Level { + rawLevel := rawSchema.(map[string]interface{}) + return &v1pb.DataClassificationSetting_DataClassificationConfig_Level{ + Id: rawLevel["id"].(string), + Title: rawLevel["title"].(string), + Description: rawLevel["description"].(string), + } +} + +func convertToV1Classification(rawSchema interface{}) *v1pb.DataClassificationSetting_DataClassificationConfig_DataClassification { + rawClassification := rawSchema.(map[string]interface{}) + classificationData := &v1pb.DataClassificationSetting_DataClassificationConfig_DataClassification{ + Id: rawClassification["id"].(string), + Title: rawClassification["title"].(string), + Description: rawClassification["description"].(string), + } + levelID, ok := rawClassification["level"].(string) + if ok { + classificationData.LevelId = &levelID + } + return classificationData +} + func convertToV1ClassificationSetting(d *schema.ResourceData) (*v1pb.DataClassificationSetting, error) { rawList, ok := d.Get("classification").([]interface{}) if !ok || len(rawList) != 1 { @@ -303,17 +327,12 @@ func convertToV1ClassificationSetting(d *schema.ResourceData) (*v1pb.DataClassif return nil, errors.Errorf("id is required for classification config") } - rawLevels := raw["levels"].([]interface{}) + rawLevels := raw["levels"].(*schema.Set) if !ok { return nil, errors.Errorf("levels is required for classification config") } - for _, level := range rawLevels { - rawLevel := level.(map[string]interface{}) - classificationLevel := &v1pb.DataClassificationSetting_DataClassificationConfig_Level{ - Id: rawLevel["id"].(string), - Title: rawLevel["title"].(string), - Description: rawLevel["description"].(string), - } + for _, level := range rawLevels.List() { + classificationLevel := convertToV1Level(level) if classificationLevel.Id == "" { return nil, errors.Errorf("classification level id is required") } @@ -323,27 +342,18 @@ func convertToV1ClassificationSetting(d *schema.ResourceData) (*v1pb.DataClassif dataClassificationConfig.Levels = append(dataClassificationConfig.Levels, classificationLevel) } - rawClassificationss := raw["classifications"].([]interface{}) + rawClassificationss := raw["classifications"].(*schema.Set) if !ok { return nil, errors.Errorf("classifications is required for classification config") } - for _, classification := range rawClassificationss { - rawClassification := classification.(map[string]interface{}) - classificationData := &v1pb.DataClassificationSetting_DataClassificationConfig_DataClassification{ - Id: rawClassification["id"].(string), - Title: rawClassification["title"].(string), - Description: rawClassification["description"].(string), - } + for _, classification := range rawClassificationss.List() { + classificationData := convertToV1Classification(classification) if classificationData.Id == "" { return nil, errors.Errorf("classification id is required") } if classificationData.Title == "" { return nil, errors.Errorf("classification title is required") } - levelID, ok := rawClassification["level"].(string) - if ok { - classificationData.LevelId = &levelID - } dataClassificationConfig.Classification[classificationData.Id] = classificationData } @@ -457,87 +467,109 @@ func convertToV1EnvironmentSetting(d *schema.ResourceData) (*v1pb.EnvironmentSet return environmentSetting, nil } -func convertToV1SemanticTypeSetting(d *schema.ResourceData) (*v1pb.SemanticTypeSetting, error) { - set, ok := d.Get("semantic_types").([]interface{}) +func getNumberValueFromSchema(rawSchema map[string]interface{}, field string) int32 { + raw, ok := rawSchema[field] if !ok { - return nil, errors.Errorf("invalid semantic_types") + return 0 } + switch v := raw.(type) { + case int: + return int32(v) + case int32: + return v + default: + return 0 + } +} - setting := &v1pb.SemanticTypeSetting{} - for _, s := range set { - rawSemanticType := s.(map[string]interface{}) - semanticType := &v1pb.SemanticTypeSetting_SemanticType{ - Id: rawSemanticType["id"].(string), - Title: rawSemanticType["title"].(string), - Description: rawSemanticType["description"].(string), - } - if semanticType.Id == "" || semanticType.Title == "" { - return nil, errors.Errorf("semantic type id and title is required") - } - - algorithmList, ok := rawSemanticType["algorithm"].([]interface{}) - if !ok || len(algorithmList) != 1 { - setting.Types = append(setting.Types, semanticType) - continue - } - - rawAlgorithm := algorithmList[0].(map[string]interface{}) - if fullMasks, ok := rawAlgorithm["full_mask"].([]interface{}); ok && len(fullMasks) == 1 { - fullMask := fullMasks[0].(map[string]interface{}) - semanticType.Algorithm = &v1pb.Algorithm{ - Mask: &v1pb.Algorithm_FullMask_{ - FullMask: &v1pb.Algorithm_FullMask{ - Substitution: fullMask["substitution"].(string), - }, - }, - } - } else if rangeMasks, ok := rawAlgorithm["range_mask"].([]interface{}); ok && len(rangeMasks) == 1 { - rawRangeMask := rangeMasks[0].(map[string]interface{}) - rangeMask := &v1pb.Algorithm_RangeMask{} - for _, raw := range rawRangeMask["slices"].([]interface{}) { - rawSlice := raw.(map[string]interface{}) - rangeMask.Slices = append(rangeMask.Slices, &v1pb.Algorithm_RangeMask_Slice{ - Start: int32(rawSlice["start"].(int)), - End: int32(rawSlice["end"].(int)), - Substitution: rawSlice["substitution"].(string), - }) - } - semanticType.Algorithm = &v1pb.Algorithm{ - Mask: &v1pb.Algorithm_RangeMask_{ - RangeMask: rangeMask, +func convertToV1SemanticType(rawSchema interface{}) (*v1pb.SemanticTypeSetting_SemanticType, error) { + rawSemanticType := rawSchema.(map[string]interface{}) + semanticType := &v1pb.SemanticTypeSetting_SemanticType{ + Id: rawSemanticType["id"].(string), + Title: rawSemanticType["title"].(string), + Description: rawSemanticType["description"].(string), + } + if semanticType.Id == "" || semanticType.Title == "" { + return nil, errors.Errorf("semantic type id and title is required") + } + + algorithmList, ok := rawSemanticType["algorithm"].([]interface{}) + if !ok || len(algorithmList) != 1 { + return semanticType, nil + } + + rawAlgorithm := algorithmList[0].(map[string]interface{}) + if fullMasks, ok := rawAlgorithm["full_mask"].([]interface{}); ok && len(fullMasks) == 1 { + fullMask := fullMasks[0].(map[string]interface{}) + semanticType.Algorithm = &v1pb.Algorithm{ + Mask: &v1pb.Algorithm_FullMask_{ + FullMask: &v1pb.Algorithm_FullMask{ + Substitution: fullMask["substitution"].(string), }, - } - } else if md5Masks, ok := rawAlgorithm["md5_mask"].([]interface{}); ok && len(md5Masks) == 1 { - md5Mask := md5Masks[0].(map[string]interface{}) - semanticType.Algorithm = &v1pb.Algorithm{ - Mask: &v1pb.Algorithm_Md5Mask{ - Md5Mask: &v1pb.Algorithm_MD5Mask{ - Salt: md5Mask["salt"].(string), - }, + }, + } + } else if rangeMasks, ok := rawAlgorithm["range_mask"].([]interface{}); ok && len(rangeMasks) == 1 { + rawRangeMask := rangeMasks[0].(map[string]interface{}) + rangeMask := &v1pb.Algorithm_RangeMask{} + for _, raw := range rawRangeMask["slices"].([]interface{}) { + rawSlice := raw.(map[string]interface{}) + rangeMask.Slices = append(rangeMask.Slices, &v1pb.Algorithm_RangeMask_Slice{ + Start: getNumberValueFromSchema(rawSlice, "start"), + End: getNumberValueFromSchema(rawSlice, "end"), + Substitution: rawSlice["substitution"].(string), + }) + } + semanticType.Algorithm = &v1pb.Algorithm{ + Mask: &v1pb.Algorithm_RangeMask_{ + RangeMask: rangeMask, + }, + } + } else if md5Masks, ok := rawAlgorithm["md5_mask"].([]interface{}); ok && len(md5Masks) == 1 { + md5Mask := md5Masks[0].(map[string]interface{}) + semanticType.Algorithm = &v1pb.Algorithm{ + Mask: &v1pb.Algorithm_Md5Mask{ + Md5Mask: &v1pb.Algorithm_MD5Mask{ + Salt: md5Mask["salt"].(string), }, - } - } else if innerOuterMasks, ok := rawAlgorithm["inner_outer_mask"].([]interface{}); ok && len(innerOuterMasks) == 1 { - innerOuterMask := innerOuterMasks[0].(map[string]interface{}) - t := v1pb.Algorithm_InnerOuterMask_MaskType( - v1pb.Algorithm_InnerOuterMask_MaskType_value[innerOuterMask["type"].(string)], - ) - if t == v1pb.Algorithm_InnerOuterMask_MASK_TYPE_UNSPECIFIED { - return nil, errors.Errorf("invalid inner_outer_mask type: %s", innerOuterMask["type"].(string)) - } - semanticType.Algorithm = &v1pb.Algorithm{ - Mask: &v1pb.Algorithm_InnerOuterMask_{ - InnerOuterMask: &v1pb.Algorithm_InnerOuterMask{ - PrefixLen: int32(innerOuterMask["prefix_len"].(int)), - SuffixLen: int32(innerOuterMask["suffix_len"].(int)), - Substitution: innerOuterMask["substitution"].(string), - Type: v1pb.Algorithm_InnerOuterMask_MaskType( - v1pb.Algorithm_InnerOuterMask_MaskType_value[innerOuterMask["type"].(string)], - ), - }, + }, + } + } else if innerOuterMasks, ok := rawAlgorithm["inner_outer_mask"].([]interface{}); ok && len(innerOuterMasks) == 1 { + innerOuterMask := innerOuterMasks[0].(map[string]interface{}) + t := v1pb.Algorithm_InnerOuterMask_MaskType( + v1pb.Algorithm_InnerOuterMask_MaskType_value[innerOuterMask["type"].(string)], + ) + if t == v1pb.Algorithm_InnerOuterMask_MASK_TYPE_UNSPECIFIED { + return nil, errors.Errorf("invalid inner_outer_mask type: %s", innerOuterMask["type"].(string)) + } + semanticType.Algorithm = &v1pb.Algorithm{ + Mask: &v1pb.Algorithm_InnerOuterMask_{ + InnerOuterMask: &v1pb.Algorithm_InnerOuterMask{ + PrefixLen: getNumberValueFromSchema(innerOuterMask, "prefix_len"), + SuffixLen: getNumberValueFromSchema(innerOuterMask, "suffix_len"), + Substitution: innerOuterMask["substitution"].(string), + Type: v1pb.Algorithm_InnerOuterMask_MaskType( + v1pb.Algorithm_InnerOuterMask_MaskType_value[innerOuterMask["type"].(string)], + ), }, - } + }, } + } + + return semanticType, nil +} + +func convertToV1SemanticTypeSetting(d *schema.ResourceData) (*v1pb.SemanticTypeSetting, error) { + set, ok := d.Get("semantic_types").(*schema.Set) + if !ok { + return nil, errors.Errorf("invalid semantic_types") + } + setting := &v1pb.SemanticTypeSetting{} + for _, raw := range set.List() { + semanticType, err := convertToV1SemanticType(raw) + if err != nil { + return nil, err + } setting.Types = append(setting.Types, semanticType) } @@ -550,6 +582,13 @@ func resourceSettingRead(ctx context.Context, d *schema.ResourceData, m interfac settingName := d.Id() setting, err := c.GetSetting(ctx, settingName) if err != nil { + // Check if the resource was deleted outside of Terraform + if internal.IsNotFoundError(err) { + tflog.Warn(ctx, fmt.Sprintf("Resource %s not found, removing from state", settingName)) + // Remove from state to trigger recreation on next apply + d.SetId("") + return nil + } return diag.FromErr(err) } diff --git a/provider/resource_user.go b/provider/resource_user.go index 135a63e..ec55e24 100644 --- a/provider/resource_user.go +++ b/provider/resource_user.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - v1pb "github.com/bytebase/bytebase/backend/generated-go/v1" + v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/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" @@ -17,8 +17,8 @@ import ( func resourceUser() *schema.Resource { return &schema.Resource{ Description: "The user resource.", - ReadContext: internal.ResourceRead(resourceUserRead), - DeleteContext: internal.ResourceDelete, + ReadContext: resourceUserRead, + DeleteContext: resourceUserDelete, CreateContext: resourceUserCreate, UpdateContext: resourceUserUpdate, Importer: &schema.ResourceImporter{ @@ -131,6 +131,13 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, m interface{} fullName := d.Id() user, err := c.GetUser(ctx, fullName) if err != nil { + // Check if the resource was deleted outside of Terraform + if internal.IsNotFoundError(err) { + tflog.Warn(ctx, fmt.Sprintf("Resource %s not found, removing from state", fullName)) + // Remove from state to trigger recreation on next apply + d.SetId("") + return nil + } return diag.FromErr(err) } @@ -308,3 +315,8 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, m interface return diags } + +func resourceUserDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + return internal.ResourceDelete(ctx, d, c.DeleteUser) +} From d4bdf41432ff77058ce0ac54eecf86aeedc494dc Mon Sep 17 00:00:00 2001 From: ecmadao Date: Mon, 25 Aug 2025 11:47:32 +0800 Subject: [PATCH 2/5] fix: lint --- client/auth.go | 2 +- client/cel.go | 3 +-- client/database.go | 13 ++++++------ client/database_group.go | 18 ++++++++-------- client/group.go | 11 +++++----- client/instance.go | 15 ++++++------- client/policy.go | 12 +++++------ client/project.go | 36 ++++++++++++++++---------------- client/review_config.go | 10 ++++----- client/risk.go | 12 +++++------ client/role.go | 12 +++++------ client/setting.go | 8 +++---- client/user.go | 17 ++++++++------- client/workspace.go | 6 +++--- provider/internal/mock_client.go | 2 +- provider/internal/resource.go | 5 ++++- 16 files changed, 94 insertions(+), 88 deletions(-) diff --git a/client/auth.go b/client/auth.go index 1ee1be2..601e9f6 100644 --- a/client/auth.go +++ b/client/auth.go @@ -10,7 +10,7 @@ import ( // Note: The login method has been moved to client.go and now uses Connect RPC. // This file is kept for backward compatibility but the implementation // has been migrated to use the AuthServiceClient from Connect RPC. -// authInterceptor implements connect.Interceptor to add authentication headers +// authInterceptor implements connect.Interceptor to add authentication headers. type authInterceptor struct { token string } diff --git a/client/cel.go b/client/cel.go index 2270e89..5149931 100644 --- a/client/cel.go +++ b/client/cel.go @@ -2,7 +2,6 @@ package client import ( "context" - "fmt" v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "connectrpc.com/connect" @@ -13,7 +12,7 @@ import ( // ParseExpression parse the expression string using Connect RPC. func (c *client) ParseExpression(ctx context.Context, expression string) (*v1alpha1.Expr, error) { if c.celClient == nil { - return nil, fmt.Errorf("cel service client not initialized") + return nil, errors.New("cel service client not initialized") } req := connect.NewRequest(&v1pb.BatchParseRequest{ diff --git a/client/database.go b/client/database.go index 53ccd91..2ac5070 100644 --- a/client/database.go +++ b/client/database.go @@ -2,6 +2,7 @@ package client import ( "context" + "errors" "fmt" "strings" "time" @@ -17,7 +18,7 @@ import ( // GetDatabase gets the database by the database full name using Connect RPC. func (c *client) GetDatabase(ctx context.Context, databaseName string) (*v1pb.Database, error) { if c.databaseClient == nil { - return nil, fmt.Errorf("database service client not initialized") + return nil, errors.New("database service client not initialized") } req := connect.NewRequest(&v1pb.GetDatabaseRequest{ @@ -76,7 +77,7 @@ func buildDatabaseFilter(filter *api.DatabaseFilter) string { // ListDatabase list all databases using Connect RPC. func (c *client) ListDatabase(ctx context.Context, parent string, filter *api.DatabaseFilter, listAll bool) ([]*v1pb.Database, error) { if c.databaseClient == nil { - return nil, fmt.Errorf("database service client not initialized") + return nil, errors.New("database service client not initialized") } res := []*v1pb.Database{} @@ -123,7 +124,7 @@ func (c *client) ListDatabase(ctx context.Context, parent string, filter *api.Da // UpdateDatabase patches the database using Connect RPC. func (c *client) UpdateDatabase(ctx context.Context, patch *v1pb.Database, updateMasks []string) (*v1pb.Database, error) { if c.databaseClient == nil { - return nil, fmt.Errorf("database service client not initialized") + return nil, errors.New("database service client not initialized") } req := connect.NewRequest(&v1pb.UpdateDatabaseRequest{ @@ -142,7 +143,7 @@ func (c *client) UpdateDatabase(ctx context.Context, patch *v1pb.Database, updat // BatchUpdateDatabases batch updates databases using Connect RPC. func (c *client) BatchUpdateDatabases(ctx context.Context, request *v1pb.BatchUpdateDatabasesRequest) (*v1pb.BatchUpdateDatabasesResponse, error) { if c.databaseClient == nil { - return nil, fmt.Errorf("database service client not initialized") + return nil, errors.New("database service client not initialized") } req := connect.NewRequest(request) @@ -158,7 +159,7 @@ func (c *client) BatchUpdateDatabases(ctx context.Context, request *v1pb.BatchUp // GetDatabaseCatalog gets the database catalog by the database full name using Connect RPC. func (c *client) GetDatabaseCatalog(ctx context.Context, databaseName string) (*v1pb.DatabaseCatalog, error) { if c.databaseCatalogClient == nil { - return nil, fmt.Errorf("database catalog service client not initialized") + return nil, errors.New("database catalog service client not initialized") } req := connect.NewRequest(&v1pb.GetDatabaseCatalogRequest{ @@ -176,7 +177,7 @@ func (c *client) GetDatabaseCatalog(ctx context.Context, databaseName string) (* // UpdateDatabaseCatalog patches the database catalog using Connect RPC. func (c *client) UpdateDatabaseCatalog(ctx context.Context, patch *v1pb.DatabaseCatalog) (*v1pb.DatabaseCatalog, error) { if c.databaseCatalogClient == nil { - return nil, fmt.Errorf("database catalog service client not initialized") + return nil, errors.New("database catalog service client not initialized") } req := connect.NewRequest(&v1pb.UpdateDatabaseCatalogRequest{ diff --git a/client/database_group.go b/client/database_group.go index 46c8ce5..30d1da9 100644 --- a/client/database_group.go +++ b/client/database_group.go @@ -2,7 +2,7 @@ package client import ( "context" - "fmt" + "errors" v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "connectrpc.com/connect" @@ -12,7 +12,7 @@ import ( // ListDatabaseGroup list all database groups in a project using Connect RPC. func (c *client) ListDatabaseGroup(ctx context.Context, project string) (*v1pb.ListDatabaseGroupsResponse, error) { if c.databaseGroupClient == nil { - return nil, fmt.Errorf("database group service client not initialized") + return nil, errors.New("database group service client not initialized") } req := connect.NewRequest(&v1pb.ListDatabaseGroupsRequest{ @@ -30,13 +30,13 @@ func (c *client) ListDatabaseGroup(ctx context.Context, project string) (*v1pb.L // CreateDatabaseGroup creates the database group using Connect RPC. func (c *client) CreateDatabaseGroup(ctx context.Context, project, groupID string, group *v1pb.DatabaseGroup) (*v1pb.DatabaseGroup, error) { if c.databaseGroupClient == nil { - return nil, fmt.Errorf("database group service client not initialized") + return nil, errors.New("database group service client not initialized") } req := connect.NewRequest(&v1pb.CreateDatabaseGroupRequest{ - Parent: project, - DatabaseGroupId: groupID, - DatabaseGroup: group, + Parent: project, + DatabaseGroupId: groupID, + DatabaseGroup: group, }) resp, err := c.databaseGroupClient.CreateDatabaseGroup(ctx, req) @@ -50,7 +50,7 @@ func (c *client) CreateDatabaseGroup(ctx context.Context, project, groupID strin // GetDatabaseGroup gets the database group by name using Connect RPC. func (c *client) GetDatabaseGroup(ctx context.Context, name string, view v1pb.DatabaseGroupView) (*v1pb.DatabaseGroup, error) { if c.databaseGroupClient == nil { - return nil, fmt.Errorf("database group service client not initialized") + return nil, errors.New("database group service client not initialized") } req := connect.NewRequest(&v1pb.GetDatabaseGroupRequest{ @@ -69,7 +69,7 @@ func (c *client) GetDatabaseGroup(ctx context.Context, name string, view v1pb.Da // UpdateDatabaseGroup updates the database group using Connect RPC. func (c *client) UpdateDatabaseGroup(ctx context.Context, patch *v1pb.DatabaseGroup, updateMasks []string) (*v1pb.DatabaseGroup, error) { if c.databaseGroupClient == nil { - return nil, fmt.Errorf("database group service client not initialized") + return nil, errors.New("database group service client not initialized") } req := connect.NewRequest(&v1pb.UpdateDatabaseGroupRequest{ @@ -88,7 +88,7 @@ func (c *client) UpdateDatabaseGroup(ctx context.Context, patch *v1pb.DatabaseGr // DeleteDatabaseGroup deletes the database group. func (c *client) DeleteDatabaseGroup(ctx context.Context, databaseGroupName string) error { if c.databaseGroupClient == nil { - return fmt.Errorf("database group service client not initialized") + return errors.New("database group service client not initialized") } req := connect.NewRequest(&v1pb.DeleteDatabaseGroupRequest{ diff --git a/client/group.go b/client/group.go index fa3753a..ae3e453 100644 --- a/client/group.go +++ b/client/group.go @@ -2,6 +2,7 @@ package client import ( "context" + "errors" "fmt" "strings" "time" @@ -30,7 +31,7 @@ func buildGroupFilter(filter *api.GroupFilter) string { // ListGroup list all groups using Connect RPC. func (c *client) ListGroup(ctx context.Context, filter *api.GroupFilter) ([]*v1pb.Group, error) { if c.groupClient == nil { - return nil, fmt.Errorf("group service client not initialized") + return nil, errors.New("group service client not initialized") } res := []*v1pb.Group{} @@ -75,7 +76,7 @@ func (c *client) ListGroup(ctx context.Context, filter *api.GroupFilter) ([]*v1p // CreateGroup creates the group using Connect RPC. func (c *client) CreateGroup(ctx context.Context, email string, group *v1pb.Group) (*v1pb.Group, error) { if c.groupClient == nil { - return nil, fmt.Errorf("group service client not initialized") + return nil, errors.New("group service client not initialized") } req := connect.NewRequest(&v1pb.CreateGroupRequest{ @@ -94,7 +95,7 @@ func (c *client) CreateGroup(ctx context.Context, email string, group *v1pb.Grou // GetGroup gets the group by name using Connect RPC. func (c *client) GetGroup(ctx context.Context, name string) (*v1pb.Group, error) { if c.groupClient == nil { - return nil, fmt.Errorf("group service client not initialized") + return nil, errors.New("group service client not initialized") } req := connect.NewRequest(&v1pb.GetGroupRequest{ @@ -112,7 +113,7 @@ func (c *client) GetGroup(ctx context.Context, name string) (*v1pb.Group, error) // UpdateGroup updates the group using Connect RPC. func (c *client) UpdateGroup(ctx context.Context, patch *v1pb.Group, updateMasks []string) (*v1pb.Group, error) { if c.groupClient == nil { - return nil, fmt.Errorf("group service client not initialized") + return nil, errors.New("group service client not initialized") } req := connect.NewRequest(&v1pb.UpdateGroupRequest{ @@ -132,7 +133,7 @@ func (c *client) UpdateGroup(ctx context.Context, patch *v1pb.Group, updateMasks // DeleteGroup deletes the group. func (c *client) DeleteGroup(ctx context.Context, name string) error { if c.groupClient == nil { - return fmt.Errorf("group service client not initialized") + return errors.New("group service client not initialized") } req := connect.NewRequest(&v1pb.DeleteGroupRequest{ diff --git a/client/instance.go b/client/instance.go index 062e6f7..4c85559 100644 --- a/client/instance.go +++ b/client/instance.go @@ -2,6 +2,7 @@ package client import ( "context" + "errors" "fmt" "strings" "time" @@ -49,7 +50,7 @@ func buildInstanceFilter(filter *api.InstanceFilter) string { // ListInstance will return instances using Connect RPC. func (c *client) ListInstance(ctx context.Context, filter *api.InstanceFilter) ([]*v1pb.Instance, error) { if c.instanceClient == nil { - return nil, fmt.Errorf("instance service client not initialized") + return nil, errors.New("instance service client not initialized") } res := []*v1pb.Instance{} @@ -97,7 +98,7 @@ func (c *client) ListInstance(ctx context.Context, filter *api.InstanceFilter) ( // GetInstance gets the instance by full name using Connect RPC. func (c *client) GetInstance(ctx context.Context, instanceName string) (*v1pb.Instance, error) { if c.instanceClient == nil { - return nil, fmt.Errorf("instance service client not initialized") + return nil, errors.New("instance service client not initialized") } req := connect.NewRequest(&v1pb.GetInstanceRequest{ @@ -115,7 +116,7 @@ func (c *client) GetInstance(ctx context.Context, instanceName string) (*v1pb.In // CreateInstance creates the instance using Connect RPC. func (c *client) CreateInstance(ctx context.Context, instanceID string, instance *v1pb.Instance) (*v1pb.Instance, error) { if c.instanceClient == nil { - return nil, fmt.Errorf("instance service client not initialized") + return nil, errors.New("instance service client not initialized") } req := connect.NewRequest(&v1pb.CreateInstanceRequest{ @@ -134,7 +135,7 @@ func (c *client) CreateInstance(ctx context.Context, instanceID string, instance // UpdateInstance updates the instance using Connect RPC. func (c *client) UpdateInstance(ctx context.Context, patch *v1pb.Instance, updateMasks []string) (*v1pb.Instance, error) { if c.instanceClient == nil { - return nil, fmt.Errorf("instance service client not initialized") + return nil, errors.New("instance service client not initialized") } req := connect.NewRequest(&v1pb.UpdateInstanceRequest{ @@ -153,7 +154,7 @@ func (c *client) UpdateInstance(ctx context.Context, patch *v1pb.Instance, updat // UndeleteInstance undeletes the instance using Connect RPC. func (c *client) UndeleteInstance(ctx context.Context, instanceName string) (*v1pb.Instance, error) { if c.instanceClient == nil { - return nil, fmt.Errorf("instance service client not initialized") + return nil, errors.New("instance service client not initialized") } req := connect.NewRequest(&v1pb.UndeleteInstanceRequest{ @@ -171,7 +172,7 @@ func (c *client) UndeleteInstance(ctx context.Context, instanceName string) (*v1 // SyncInstanceSchema will trigger the schema sync for an instance using Connect RPC. func (c *client) SyncInstanceSchema(ctx context.Context, instanceName string) error { if c.instanceClient == nil { - return fmt.Errorf("instance service client not initialized") + return errors.New("instance service client not initialized") } req := connect.NewRequest(&v1pb.SyncInstanceRequest{ @@ -185,7 +186,7 @@ func (c *client) SyncInstanceSchema(ctx context.Context, instanceName string) er // DeleteInstance deletes the instance. func (c *client) DeleteInstance(ctx context.Context, name string) error { if c.instanceClient == nil { - return fmt.Errorf("instance service client not initialized") + return errors.New("instance service client not initialized") } req := connect.NewRequest(&v1pb.DeleteInstanceRequest{ diff --git a/client/policy.go b/client/policy.go index 728865b..3daeb17 100644 --- a/client/policy.go +++ b/client/policy.go @@ -2,7 +2,7 @@ package client import ( "context" - "fmt" + "errors" v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "connectrpc.com/connect" @@ -12,7 +12,7 @@ import ( // ListPolicies lists policies in a specific resource. func (c *client) ListPolicies(ctx context.Context, parent string) (*v1pb.ListPoliciesResponse, error) { if c.orgPolicyClient == nil { - return nil, fmt.Errorf("org policy service client not initialized") + return nil, errors.New("org policy service client not initialized") } req := connect.NewRequest(&v1pb.ListPoliciesRequest{ @@ -30,7 +30,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) { if c.orgPolicyClient == nil { - return nil, fmt.Errorf("org policy service client not initialized") + return nil, errors.New("org policy service client not initialized") } req := connect.NewRequest(&v1pb.GetPolicyRequest{ @@ -48,7 +48,7 @@ func (c *client) GetPolicy(ctx context.Context, policyName string) (*v1pb.Policy // UpsertPolicy creates or updates the policy. func (c *client) UpsertPolicy(ctx context.Context, policy *v1pb.Policy, updateMasks []string) (*v1pb.Policy, error) { if c.orgPolicyClient == nil { - return nil, fmt.Errorf("org policy service client not initialized") + return nil, errors.New("org policy service client not initialized") } req := connect.NewRequest(&v1pb.UpdatePolicyRequest{ @@ -70,7 +70,7 @@ func (c *client) UpsertPolicy(ctx context.Context, policy *v1pb.Policy, updateMa // DeletePolicy deletes the policy. func (c *client) DeletePolicy(ctx context.Context, policyName string) error { if c.orgPolicyClient == nil { - return fmt.Errorf("org policy service client not initialized") + return errors.New("org policy service client not initialized") } req := connect.NewRequest(&v1pb.DeletePolicyRequest{ @@ -79,4 +79,4 @@ func (c *client) DeletePolicy(ctx context.Context, policyName string) error { _, err := c.orgPolicyClient.DeletePolicy(ctx, req) return err -} \ No newline at end of file +} diff --git a/client/project.go b/client/project.go index 85bc15c..285f81d 100644 --- a/client/project.go +++ b/client/project.go @@ -2,7 +2,7 @@ package client import ( "context" - "fmt" + "errors" "strings" v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" @@ -16,7 +16,7 @@ import ( // GetProject gets the project by project full name. func (c *client) GetProject(ctx context.Context, projectName string) (*v1pb.Project, error) { if c.projectClient == nil { - return nil, fmt.Errorf("project service client not initialized") + return nil, errors.New("project service client not initialized") } req := connect.NewRequest(&v1pb.GetProjectRequest{ @@ -34,7 +34,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) { if c.projectClient == nil { - return nil, fmt.Errorf("project service client not initialized") + return nil, errors.New("project service client not initialized") } req := connect.NewRequest(&v1pb.GetIamPolicyRequest{ @@ -52,7 +52,7 @@ func (c *client) GetProjectIAMPolicy(ctx context.Context, projectName string) (* // SetProjectIAMPolicy sets the project IAM policy. func (c *client) SetProjectIAMPolicy(ctx context.Context, projectName string, update *v1pb.SetIamPolicyRequest) (*v1pb.IamPolicy, error) { if c.projectClient == nil { - return nil, fmt.Errorf("project service client not initialized") + return nil, errors.New("project service client not initialized") } // Update the resource field to match the project name @@ -71,7 +71,7 @@ func (c *client) SetProjectIAMPolicy(ctx context.Context, projectName string, up // CreateProjectWebhook creates the webhook in the project. func (c *client) CreateProjectWebhook(ctx context.Context, projectName string, webhook *v1pb.Webhook) (*v1pb.Webhook, error) { if c.projectClient == nil { - return nil, fmt.Errorf("project service client not initialized") + return nil, errors.New("project service client not initialized") } req := connect.NewRequest(&v1pb.AddWebhookRequest{ @@ -90,13 +90,13 @@ func (c *client) CreateProjectWebhook(ctx context.Context, projectName string, w return resp.Msg.Webhooks[len(resp.Msg.Webhooks)-1], nil } - return nil, fmt.Errorf("webhook not found in response") + return nil, errors.New("webhook not found in response") } // UpdateProjectWebhook updates the webhook. func (c *client) UpdateProjectWebhook(ctx context.Context, patch *v1pb.Webhook, updateMasks []string) (*v1pb.Webhook, error) { if c.projectClient == nil { - return nil, fmt.Errorf("project service client not initialized") + return nil, errors.New("project service client not initialized") } req := connect.NewRequest(&v1pb.UpdateWebhookRequest{ @@ -120,13 +120,13 @@ func (c *client) UpdateProjectWebhook(ctx context.Context, patch *v1pb.Webhook, } } - return nil, fmt.Errorf("updated webhook not found in response") + return nil, errors.New("updated webhook not found in response") } // DeleteProjectWebhook deletes the webhook. func (c *client) DeleteProjectWebhook(ctx context.Context, webhookName string) error { if c.projectClient == nil { - return fmt.Errorf("project service client not initialized") + return errors.New("project service client not initialized") } req := connect.NewRequest(&v1pb.RemoveWebhookRequest{ @@ -142,7 +142,7 @@ func (c *client) DeleteProjectWebhook(ctx context.Context, webhookName string) e // ListProject list all projects. func (c *client) ListProject(ctx context.Context, filter *api.ProjectFilter) ([]*v1pb.Project, error) { if c.projectClient == nil { - return nil, fmt.Errorf("project service client not initialized") + return nil, errors.New("project service client not initialized") } var projects []*v1pb.Project @@ -150,8 +150,8 @@ func (c *client) ListProject(ctx context.Context, filter *api.ProjectFilter) ([] for { req := connect.NewRequest(&v1pb.ListProjectsRequest{ - PageSize: 500, - PageToken: pageToken, + PageSize: 500, + PageToken: pageToken, ShowDeleted: filter.State == v1pb.State_DELETED, }) @@ -205,7 +205,7 @@ func (c *client) ListProject(ctx context.Context, filter *api.ProjectFilter) ([] // CreateProject creates the project. func (c *client) CreateProject(ctx context.Context, projectID string, project *v1pb.Project) (*v1pb.Project, error) { if c.projectClient == nil { - return nil, fmt.Errorf("project service client not initialized") + return nil, errors.New("project service client not initialized") } req := connect.NewRequest(&v1pb.CreateProjectRequest{ @@ -224,7 +224,7 @@ func (c *client) CreateProject(ctx context.Context, projectID string, project *v // UpdateProject updates the project. func (c *client) UpdateProject(ctx context.Context, patch *v1pb.Project, updateMasks []string) (*v1pb.Project, error) { if c.projectClient == nil { - return nil, fmt.Errorf("project service client not initialized") + return nil, errors.New("project service client not initialized") } req := connect.NewRequest(&v1pb.UpdateProjectRequest{ @@ -245,7 +245,7 @@ func (c *client) UpdateProject(ctx context.Context, patch *v1pb.Project, updateM // UndeleteProject undeletes the project. func (c *client) UndeleteProject(ctx context.Context, projectName string) (*v1pb.Project, error) { if c.projectClient == nil { - return nil, fmt.Errorf("project service client not initialized") + return nil, errors.New("project service client not initialized") } req := connect.NewRequest(&v1pb.UndeleteProjectRequest{ @@ -263,7 +263,7 @@ func (c *client) UndeleteProject(ctx context.Context, projectName string) (*v1pb // DeleteProject deletes the project. func (c *client) DeleteProject(ctx context.Context, name string) error { if c.projectClient == nil { - return fmt.Errorf("project service client not initialized") + return errors.New("project service client not initialized") } req := connect.NewRequest(&v1pb.DeleteProjectRequest{ @@ -274,7 +274,7 @@ func (c *client) DeleteProject(ctx context.Context, name string) error { return err } -// Helper function for case-insensitive string contains +// Helper function for case-insensitive string contains. func containsIgnoreCase(s, substr string) bool { return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) -} \ No newline at end of file +} diff --git a/client/review_config.go b/client/review_config.go index 1741802..023e210 100644 --- a/client/review_config.go +++ b/client/review_config.go @@ -2,7 +2,7 @@ package client import ( "context" - "fmt" + "errors" v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "connectrpc.com/connect" @@ -12,7 +12,7 @@ import ( // ListReviewConfig will return review configs using Connect RPC. func (c *client) ListReviewConfig(ctx context.Context) (*v1pb.ListReviewConfigsResponse, error) { if c.reviewConfigClient == nil { - return nil, fmt.Errorf("review config service client not initialized") + return nil, errors.New("review config service client not initialized") } req := connect.NewRequest(&v1pb.ListReviewConfigsRequest{}) @@ -28,7 +28,7 @@ func (c *client) ListReviewConfig(ctx context.Context) (*v1pb.ListReviewConfigsR // GetReviewConfig gets the review config by full name using Connect RPC. func (c *client) GetReviewConfig(ctx context.Context, reviewName string) (*v1pb.ReviewConfig, error) { if c.reviewConfigClient == nil { - return nil, fmt.Errorf("review config service client not initialized") + return nil, errors.New("review config service client not initialized") } req := connect.NewRequest(&v1pb.GetReviewConfigRequest{ @@ -46,7 +46,7 @@ func (c *client) GetReviewConfig(ctx context.Context, reviewName string) (*v1pb. // UpsertReviewConfig updates or creates the review config using Connect RPC. func (c *client) UpsertReviewConfig(ctx context.Context, patch *v1pb.ReviewConfig, updateMasks []string) (*v1pb.ReviewConfig, error) { if c.reviewConfigClient == nil { - return nil, fmt.Errorf("review config service client not initialized") + return nil, errors.New("review config service client not initialized") } req := connect.NewRequest(&v1pb.UpdateReviewConfigRequest{ @@ -66,7 +66,7 @@ func (c *client) UpsertReviewConfig(ctx context.Context, patch *v1pb.ReviewConfi // DeleteReviewConfig deletes the review config. func (c *client) DeleteReviewConfig(ctx context.Context, name string) error { if c.reviewConfigClient == nil { - return fmt.Errorf("review config service client not initialized") + return errors.New("review config service client not initialized") } req := connect.NewRequest(&v1pb.DeleteReviewConfigRequest{ diff --git a/client/risk.go b/client/risk.go index 6a2053d..1b7bb8a 100644 --- a/client/risk.go +++ b/client/risk.go @@ -2,7 +2,7 @@ package client import ( "context" - "fmt" + "errors" v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "connectrpc.com/connect" @@ -12,7 +12,7 @@ import ( // ListRisk lists the risk using Connect RPC. func (c *client) ListRisk(ctx context.Context) ([]*v1pb.Risk, error) { if c.riskClient == nil { - return nil, fmt.Errorf("risk service client not initialized") + return nil, errors.New("risk service client not initialized") } req := connect.NewRequest(&v1pb.ListRisksRequest{}) @@ -28,7 +28,7 @@ func (c *client) ListRisk(ctx context.Context) ([]*v1pb.Risk, error) { // GetRisk gets the risk by full name using Connect RPC. func (c *client) GetRisk(ctx context.Context, name string) (*v1pb.Risk, error) { if c.riskClient == nil { - return nil, fmt.Errorf("risk service client not initialized") + return nil, errors.New("risk service client not initialized") } req := connect.NewRequest(&v1pb.GetRiskRequest{ @@ -46,7 +46,7 @@ func (c *client) GetRisk(ctx context.Context, name string) (*v1pb.Risk, error) { // CreateRisk creates the risk using Connect RPC. func (c *client) CreateRisk(ctx context.Context, risk *v1pb.Risk) (*v1pb.Risk, error) { if c.riskClient == nil { - return nil, fmt.Errorf("risk service client not initialized") + return nil, errors.New("risk service client not initialized") } req := connect.NewRequest(&v1pb.CreateRiskRequest{ @@ -64,7 +64,7 @@ func (c *client) CreateRisk(ctx context.Context, risk *v1pb.Risk) (*v1pb.Risk, e // UpdateRisk updates the risk using Connect RPC. func (c *client) UpdateRisk(ctx context.Context, patch *v1pb.Risk, updateMasks []string) (*v1pb.Risk, error) { if c.riskClient == nil { - return nil, fmt.Errorf("risk service client not initialized") + return nil, errors.New("risk service client not initialized") } req := connect.NewRequest(&v1pb.UpdateRiskRequest{ @@ -83,7 +83,7 @@ func (c *client) UpdateRisk(ctx context.Context, patch *v1pb.Risk, updateMasks [ // DeleteRisk deletes the risk. func (c *client) DeleteRisk(ctx context.Context, name string) error { if c.riskClient == nil { - return fmt.Errorf("risk service client not initialized") + return errors.New("risk service client not initialized") } req := connect.NewRequest(&v1pb.DeleteRiskRequest{ diff --git a/client/role.go b/client/role.go index 88c9816..ba7e904 100644 --- a/client/role.go +++ b/client/role.go @@ -2,7 +2,7 @@ package client import ( "context" - "fmt" + "errors" v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "connectrpc.com/connect" @@ -12,7 +12,7 @@ import ( // GetRole gets the role by full name using Connect RPC. func (c *client) GetRole(ctx context.Context, name string) (*v1pb.Role, error) { if c.roleClient == nil { - return nil, fmt.Errorf("role service client not initialized") + return nil, errors.New("role service client not initialized") } req := connect.NewRequest(&v1pb.GetRoleRequest{ @@ -30,7 +30,7 @@ func (c *client) GetRole(ctx context.Context, name string) (*v1pb.Role, error) { // CreateRole creates the role using Connect RPC. func (c *client) CreateRole(ctx context.Context, roleID string, role *v1pb.Role) (*v1pb.Role, error) { if c.roleClient == nil { - return nil, fmt.Errorf("role service client not initialized") + return nil, errors.New("role service client not initialized") } req := connect.NewRequest(&v1pb.CreateRoleRequest{ @@ -49,7 +49,7 @@ func (c *client) CreateRole(ctx context.Context, roleID string, role *v1pb.Role) // UpdateRole updates the role using Connect RPC. func (c *client) UpdateRole(ctx context.Context, patch *v1pb.Role, updateMasks []string) (*v1pb.Role, error) { if c.roleClient == nil { - return nil, fmt.Errorf("role service client not initialized") + return nil, errors.New("role service client not initialized") } req := connect.NewRequest(&v1pb.UpdateRoleRequest{ @@ -69,7 +69,7 @@ func (c *client) UpdateRole(ctx context.Context, patch *v1pb.Role, updateMasks [ // ListRole will returns all roles using Connect RPC. func (c *client) ListRole(ctx context.Context) (*v1pb.ListRolesResponse, error) { if c.roleClient == nil { - return nil, fmt.Errorf("role service client not initialized") + return nil, errors.New("role service client not initialized") } req := connect.NewRequest(&v1pb.ListRolesRequest{}) @@ -85,7 +85,7 @@ func (c *client) ListRole(ctx context.Context) (*v1pb.ListRolesResponse, error) // DeleteRole deletes the role. func (c *client) DeleteRole(ctx context.Context, name string) error { if c.roleClient == nil { - return fmt.Errorf("role service client not initialized") + return errors.New("role service client not initialized") } req := connect.NewRequest(&v1pb.DeleteRoleRequest{ diff --git a/client/setting.go b/client/setting.go index 82dce29..46c1cc1 100644 --- a/client/setting.go +++ b/client/setting.go @@ -2,7 +2,7 @@ package client import ( "context" - "fmt" + "errors" v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "connectrpc.com/connect" @@ -12,7 +12,7 @@ import ( // ListSettings lists all settings using Connect RPC. func (c *client) ListSettings(ctx context.Context) (*v1pb.ListSettingsResponse, error) { if c.settingClient == nil { - return nil, fmt.Errorf("setting service client not initialized") + return nil, errors.New("setting service client not initialized") } req := connect.NewRequest(&v1pb.ListSettingsRequest{}) @@ -28,7 +28,7 @@ func (c *client) ListSettings(ctx context.Context) (*v1pb.ListSettingsResponse, // GetSetting gets the setting by the name using Connect RPC. func (c *client) GetSetting(ctx context.Context, settingName string) (*v1pb.Setting, error) { if c.settingClient == nil { - return nil, fmt.Errorf("setting service client not initialized") + return nil, errors.New("setting service client not initialized") } req := connect.NewRequest(&v1pb.GetSettingRequest{ @@ -46,7 +46,7 @@ func (c *client) GetSetting(ctx context.Context, settingName string) (*v1pb.Sett // UpsertSetting updates or creates the setting using Connect RPC. func (c *client) UpsertSetting(ctx context.Context, upsert *v1pb.Setting, updateMasks []string) (*v1pb.Setting, error) { if c.settingClient == nil { - return nil, fmt.Errorf("setting service client not initialized") + return nil, errors.New("setting service client not initialized") } req := connect.NewRequest(&v1pb.UpdateSettingRequest{ diff --git a/client/user.go b/client/user.go index 84c1045..eae8db8 100644 --- a/client/user.go +++ b/client/user.go @@ -2,6 +2,7 @@ package client import ( "context" + "errors" "fmt" "strings" "time" @@ -43,7 +44,7 @@ func buildUserFilter(filter *api.UserFilter) string { // ListUser list all users using Connect RPC. func (c *client) ListUser(ctx context.Context, filter *api.UserFilter) ([]*v1pb.User, error) { if c.userClient == nil { - return nil, fmt.Errorf("user service client not initialized") + return nil, errors.New("user service client not initialized") } res := []*v1pb.User{} @@ -54,7 +55,7 @@ func (c *client) ListUser(ctx context.Context, filter *api.UserFilter) ([]*v1pb. for { startTimePerPage := time.Now() - + req := connect.NewRequest(&v1pb.ListUsersRequest{ Filter: filterStr, PageSize: 500, @@ -91,7 +92,7 @@ func (c *client) ListUser(ctx context.Context, filter *api.UserFilter) ([]*v1pb. // CreateUser creates the user using Connect RPC. func (c *client) CreateUser(ctx context.Context, user *v1pb.User) (*v1pb.User, error) { if c.userClient == nil { - return nil, fmt.Errorf("user service client not initialized") + return nil, errors.New("user service client not initialized") } req := connect.NewRequest(&v1pb.CreateUserRequest{ @@ -109,7 +110,7 @@ func (c *client) CreateUser(ctx context.Context, user *v1pb.User) (*v1pb.User, e // GetUser gets the user by name using Connect RPC. func (c *client) GetUser(ctx context.Context, userName string) (*v1pb.User, error) { if c.userClient == nil { - return nil, fmt.Errorf("user service client not initialized") + return nil, errors.New("user service client not initialized") } req := connect.NewRequest(&v1pb.GetUserRequest{ @@ -127,7 +128,7 @@ func (c *client) GetUser(ctx context.Context, userName string) (*v1pb.User, erro // UpdateUser updates the user using Connect RPC. func (c *client) UpdateUser(ctx context.Context, patch *v1pb.User, updateMasks []string) (*v1pb.User, error) { if c.userClient == nil { - return nil, fmt.Errorf("user service client not initialized") + return nil, errors.New("user service client not initialized") } req := connect.NewRequest(&v1pb.UpdateUserRequest{ @@ -146,7 +147,7 @@ func (c *client) UpdateUser(ctx context.Context, patch *v1pb.User, updateMasks [ // UndeleteUser undeletes the user by name using Connect RPC. func (c *client) UndeleteUser(ctx context.Context, userName string) (*v1pb.User, error) { if c.userClient == nil { - return nil, fmt.Errorf("user service client not initialized") + return nil, errors.New("user service client not initialized") } req := connect.NewRequest(&v1pb.UndeleteUserRequest{ @@ -164,7 +165,7 @@ func (c *client) UndeleteUser(ctx context.Context, userName string) (*v1pb.User, // DeleteUser deletes the user. func (c *client) DeleteUser(ctx context.Context, name string) error { if c.userClient == nil { - return fmt.Errorf("user service client not initialized") + return errors.New("user service client not initialized") } req := connect.NewRequest(&v1pb.DeleteUserRequest{ @@ -173,4 +174,4 @@ func (c *client) DeleteUser(ctx context.Context, name string) error { _, err := c.userClient.DeleteUser(ctx, req) return err -} \ No newline at end of file +} diff --git a/client/workspace.go b/client/workspace.go index c822c2c..45f3a02 100644 --- a/client/workspace.go +++ b/client/workspace.go @@ -2,7 +2,7 @@ package client import ( "context" - "fmt" + "errors" v1pb "buf.build/gen/go/bytebase/bytebase/protocolbuffers/go/v1" "connectrpc.com/connect" @@ -11,7 +11,7 @@ import ( // GetWorkspaceIAMPolicy gets the workspace IAM policy. func (c *client) GetWorkspaceIAMPolicy(ctx context.Context) (*v1pb.IamPolicy, error) { if c.workspaceClient == nil { - return nil, fmt.Errorf("workspace service client not initialized") + return nil, errors.New("workspace service client not initialized") } req := connect.NewRequest(&v1pb.GetIamPolicyRequest{ @@ -29,7 +29,7 @@ func (c *client) GetWorkspaceIAMPolicy(ctx context.Context) (*v1pb.IamPolicy, er // SetWorkspaceIAMPolicy sets the workspace IAM policy. func (c *client) SetWorkspaceIAMPolicy(ctx context.Context, setIamPolicyRequest *v1pb.SetIamPolicyRequest) (*v1pb.IamPolicy, error) { if c.workspaceClient == nil { - return nil, fmt.Errorf("workspace service client not initialized") + return nil, errors.New("workspace service client not initialized") } // Ensure the resource is set correctly diff --git a/provider/internal/mock_client.go b/provider/internal/mock_client.go index 48b05c7..234bf4a 100644 --- a/provider/internal/mock_client.go +++ b/provider/internal/mock_client.go @@ -109,7 +109,7 @@ func (c *mockClient) CreateInstance(_ context.Context, instanceID string, instan if ins.Environment != nil { envID, err = GetEnvironmentID(*ins.Environment) } else { - err = fmt.Errorf("instance environment is nil") + err = errors.New("instance environment is nil") } if err != nil { return nil, err diff --git a/provider/internal/resource.go b/provider/internal/resource.go index ce91cc8..c03ff05 100644 --- a/provider/internal/resource.go +++ b/provider/internal/resource.go @@ -17,8 +17,11 @@ func IsNotFoundError(err error) bool { return strings.Contains(err.Error(), "not_found") } +// ResourceDeleteFunc is the func to delete the resource by name. +type ResourceDeleteFunc func(ctx context.Context, name string) error + // ResourceDelete wrap the delete func. -func ResourceDelete(ctx context.Context, d *schema.ResourceData, delete func(ctx context.Context, name string) error) diag.Diagnostics { +func ResourceDelete(ctx context.Context, d *schema.ResourceData, delete ResourceDeleteFunc) diag.Diagnostics { fullName := d.Id() // Warning or errors can be collected in a slice type From a9e111036828e524f890689660cd3f8dbf833fb7 Mon Sep 17 00:00:00 2001 From: ecmadao Date: Mon, 25 Aug 2025 11:49:49 +0800 Subject: [PATCH 3/5] fix: lint --- provider/internal/resource.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/provider/internal/resource.go b/provider/internal/resource.go index c03ff05..9adc97f 100644 --- a/provider/internal/resource.go +++ b/provider/internal/resource.go @@ -21,13 +21,13 @@ func IsNotFoundError(err error) bool { type ResourceDeleteFunc func(ctx context.Context, name string) error // ResourceDelete wrap the delete func. -func ResourceDelete(ctx context.Context, d *schema.ResourceData, delete ResourceDeleteFunc) diag.Diagnostics { +func ResourceDelete(ctx context.Context, d *schema.ResourceData, deleteResource ResourceDeleteFunc) diag.Diagnostics { fullName := d.Id() // Warning or errors can be collected in a slice type var diags diag.Diagnostics - if err := delete(ctx, fullName); err != nil { + if err := deleteResource(ctx, fullName); err != nil { // Check if the resource was deleted outside of Terraform if !IsNotFoundError(err) { return diag.FromErr(err) From 4d6c59f5ec2b5007df4a7f49b422dcbf82f6e3fc Mon Sep 17 00:00:00 2001 From: ecmadao Date: Mon, 25 Aug 2025 17:26:40 +0800 Subject: [PATCH 4/5] chore: support multiple columns --- VERSION | 2 +- examples/database/main.tf | 2 +- examples/database_group/main.tf | 2 +- examples/environments/main.tf | 2 +- examples/groups/main.tf | 4 +- examples/iamPolicy/main.tf | 2 +- examples/instances/main.tf | 2 +- examples/policies/main.tf | 2 +- examples/projects/main.tf | 2 +- examples/risk/main.tf | 2 +- examples/roles/main.tf | 2 +- examples/settings/main.tf | 2 +- examples/setup/data_masking.tf | 27 +++-- examples/setup/main.tf | 2 +- examples/sql_review/main.tf | 2 +- examples/users/main.tf | 2 +- provider/data_source_policy.go | 163 +++++++++++++++++++--------- provider/data_source_policy_list.go | 20 ++-- provider/data_source_policy_test.go | 2 +- provider/resource_policy.go | 62 +++++++---- provider/resource_policy_test.go | 8 +- tutorials/0-provider.tf | 2 +- tutorials/8-5-masking-exception.tf | 21 +--- 23 files changed, 209 insertions(+), 128 deletions(-) diff --git a/VERSION b/VERSION index 33f465d..4764627 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.9.1 \ No newline at end of file +3.9.2 \ No newline at end of file diff --git a/examples/database/main.tf b/examples/database/main.tf index 4e70469..e9a738a 100644 --- a/examples/database/main.tf +++ b/examples/database/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/database_group/main.tf b/examples/database_group/main.tf index 1d20a56..de8d008 100644 --- a/examples/database_group/main.tf +++ b/examples/database_group/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/environments/main.tf b/examples/environments/main.tf index dac313c..15bf112 100644 --- a/examples/environments/main.tf +++ b/examples/environments/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 059fcac..32981d4 100644 --- a/examples/groups/main.tf +++ b/examples/groups/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } @@ -30,7 +30,7 @@ data "bytebase_project" "sample_project" { data "bytebase_group_list" "groups_in_project" { project = data.bytebase_project.sample_project.name - query = "Bytebase" + query = "Bytebase" } output "groups_in_project" { diff --git a/examples/iamPolicy/main.tf b/examples/iamPolicy/main.tf index 9fe20ec..67b6c50 100644 --- a/examples/iamPolicy/main.tf +++ b/examples/iamPolicy/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 ce241f8..aaaa9c4 100644 --- a/examples/instances/main.tf +++ b/examples/instances/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 a0b8dd8..39fbfdd 100644 --- a/examples/policies/main.tf +++ b/examples/policies/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 d534bfa..0c735f7 100644 --- a/examples/projects/main.tf +++ b/examples/projects/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/risk/main.tf b/examples/risk/main.tf index 0c69e90..054dbd9 100644 --- a/examples/risk/main.tf +++ b/examples/risk/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 2b7f9cc..6eaf391 100644 --- a/examples/roles/main.tf +++ b/examples/roles/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 b6d7e09..544b3ec 100644 --- a/examples/settings/main.tf +++ b/examples/settings/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/setup/data_masking.tf b/examples/setup/data_masking.tf index 416ed4b..b8075a2 100644 --- a/examples/setup/data_masking.tf +++ b/examples/setup/data_masking.tf @@ -100,7 +100,9 @@ resource "bytebase_setting" "semantic_types" { resource "bytebase_policy" "masking_exception_policy" { depends_on = [ bytebase_project.sample_project, - bytebase_instance.test + bytebase_instance.test, + bytebase_user.project_developer, + bytebase_user.workspace_dba ] parent = bytebase_project.sample_project.name @@ -112,17 +114,24 @@ resource "bytebase_policy" "masking_exception_policy" { exceptions { database = "instances/test-sample-instance/databases/employee" table = "salary" - column = "amount" - member = "user:ed@bytebase.com" - action = "EXPORT" - reason = "Grant access to ed for export" + columns = ["amount", "emp_no"] + members = [ + format("user:%s", bytebase_user.project_developer.email), + format("user:%s", bytebase_user.workspace_dba.email), + ] + actions = ["QUERY", "EXPORT"] + reason = "Grant access" } + exceptions { database = "instances/test-sample-instance/databases/employee" - table = "salary" - column = "amount" - member = "user:ed@bytebase.com" - action = "QUERY" + table = "employee" + columns = ["emp_no"] + members = [ + format("user:%s", bytebase_user.workspace_dba.email), + ] + actions = ["EXPORT"] + reason = "Grant access" } } } diff --git a/examples/setup/main.tf b/examples/setup/main.tf index 709ed27..a8b4784 100644 --- a/examples/setup/main.tf +++ b/examples/setup/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/sql_review/main.tf b/examples/sql_review/main.tf index 72e3484..5352690 100644 --- a/examples/sql_review/main.tf +++ b/examples/sql_review/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/users/main.tf b/examples/users/main.tf index 6d4e02a..9954ec1 100644 --- a/examples/users/main.tf +++ b/examples/users/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/provider/data_source_policy.go b/provider/data_source_policy.go index 0c90ca9..7997d1c 100644 --- a/provider/data_source_policy.go +++ b/provider/data_source_policy.go @@ -113,25 +113,40 @@ func getMaskingExceptionPolicySchema(computed bool) *schema.Schema { Optional: true, ValidateFunc: validation.StringIsNotEmpty, }, - "column": { - Type: schema.TypeString, - Computed: computed, - Optional: true, - ValidateFunc: validation.StringIsNotEmpty, + "columns": { + Type: schema.TypeSet, + Computed: computed, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, }, - "member": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringIsNotEmpty, - Description: "The member in user:{email} or group:{email} format.", + "members": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + Description: "The member in user:{email} or group:{email} format.", + ValidateDiagFunc: internal.ResourceNameValidation( + "^user:", + "^group:", + ), + }, }, - "action": { - Type: schema.TypeString, + "actions": { + Type: schema.TypeSet, Required: true, - ValidateFunc: validation.StringInSlice([]string{ - v1pb.MaskingExceptionPolicy_MaskingException_QUERY.String(), - v1pb.MaskingExceptionPolicy_MaskingException_EXPORT.String(), - }, false), + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + Description: "The action to allow for members. Support QUERY or EXPORT", + ValidateFunc: validation.StringInSlice([]string{ + v1pb.MaskingExceptionPolicy_MaskingException_QUERY.String(), + v1pb.MaskingExceptionPolicy_MaskingException_EXPORT.String(), + }, false), + }, }, "reason": { Type: schema.TypeString, @@ -318,10 +333,6 @@ func dataSourcePolicyRead(ctx context.Context, d *schema.ResourceData, m interfa } func setPolicyMessage(d *schema.ResourceData, policy *v1pb.Policy) diag.Diagnostics { - _, policyType, 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()) } @@ -332,51 +343,57 @@ func setPolicyMessage(d *schema.ResourceData, policy *v1pb.Policy) diag.Diagnost return diag.Errorf("cannot set enforce for policy: %s", err.Error()) } + key, payload, diags := flattenPolicyPayload(policy) + if diags != nil { + return diags + } + if err := d.Set(key, payload); err != nil { + return diag.Errorf("cannot set %s for policy: %s", key, err.Error()) + } + + return nil +} + +func flattenPolicyPayload(policy *v1pb.Policy) (string, interface{}, diag.Diagnostics) { + _, policyType, err := internal.GetPolicyParentAndType(policy.Name) + if err != nil { + return "", nil, diag.Errorf("cannot parse name for policy: %s", err.Error()) + } switch policyType { case v1pb.PolicyType_MASKING_EXCEPTION: 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_exception_policy: %s", err.Error()) + return "", nil, diag.FromErr(err) } + return "masking_exception_policy", exceptionPolicy, nil } case v1pb.PolicyType_MASKING_RULE: if p := policy.GetMaskingRulePolicy(); p != nil { maskingPolicy, err := flattenGlobalMaskingPolicy(p) if err != nil { - return diag.FromErr(err) - } - if err := d.Set("global_masking_policy", maskingPolicy); err != nil { - return diag.Errorf("cannot set global_masking_policy: %s", err.Error()) + return "", nil, diag.FromErr(err) } + return "global_masking_policy", maskingPolicy, nil } case v1pb.PolicyType_DISABLE_COPY_DATA: if p := policy.GetDisableCopyDataPolicy(); p != nil { disableCopyDataPolicy := flattenDisableCopyDataPolicy(p) - if err := d.Set("disable_copy_data_policy", disableCopyDataPolicy); err != nil { - return diag.Errorf("cannot set disable_copy_data_policy: %s", err.Error()) - } + return "disable_copy_data_policy", disableCopyDataPolicy, nil } case v1pb.PolicyType_DATA_SOURCE_QUERY: if p := policy.GetDataSourceQueryPolicy(); p != nil { dataSourceQueryPolicy := flattenDataSourceQueryPolicy(p) - if err := d.Set("data_source_query_policy", dataSourceQueryPolicy); err != nil { - return diag.Errorf("cannot set data_source_query_policy: %s", err.Error()) - } + return "data_source_query_policy", dataSourceQueryPolicy, nil } case v1pb.PolicyType_ROLLOUT_POLICY: if p := policy.GetRolloutPolicy(); p != nil { rolloutPolicy := flattenRolloutPolicy(p) - if err := d.Set("rollout_policy", rolloutPolicy); err != nil { - return diag.Errorf("cannot set rollout_policy: %s", err.Error()) - } + return "rollout_policy", rolloutPolicy, nil } } - return nil + return "", nil, diag.Errorf("unsupported policy: %s", policy.Name) } func flattenRolloutPolicy(p *v1pb.RolloutPolicy) []interface{} { @@ -428,21 +445,48 @@ func flattenGlobalMaskingPolicy(p *v1pb.MaskingRulePolicy) ([]interface{}, error return []interface{}{policy}, nil } +type combineException struct { + expression string + reason string + members []interface{} + actions []interface{} +} + 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() + exceptionMap := map[string]*combineException{} + for _, exception := range p.MaskingExceptions { if exception.Condition == nil || exception.Condition.Expression == "" { - return nil, errors.Errorf("invalid exception policy condition") + // Skip invalid data. + continue } - raw["reason"] = exception.Condition.Description - expressions := strings.Split(exception.Condition.Expression, " && ") + key := fmt.Sprintf("[expression:%s] [reason:%s]", exception.Condition.Expression, exception.Condition.Description) + if _, ok := exceptionMap[key]; !ok { + exceptionMap[key] = &combineException{ + expression: exception.Condition.Expression, + reason: exception.Condition.Description, + members: []interface{}{}, + actions: []interface{}{}, + } + } + exceptionMap[key].members = append(exceptionMap[key].members, exception.Member) + exceptionMap[key].actions = append(exceptionMap[key].actions, exception.Action.String()) + } + + for _, combine := range exceptionMap { + raw := map[string]interface{}{ + "members": schema.NewSet(schema.HashString, combine.members), + "actions": schema.NewSet(schema.HashString, combine.actions), + "reason": combine.reason, + } + + expressions := strings.Split(combine.expression, " && ") instanceID := "" databaseName := "" + columns := []interface{}{} + for _, expression := range expressions { if strings.HasPrefix(expression, "resource.instance_id == ") { instanceID = strings.TrimSuffix( @@ -469,10 +513,10 @@ func flattenMaskingExceptionPolicy(p *v1pb.MaskingExceptionPolicy) ([]interface{ ) } if strings.HasPrefix(expression, "resource.column_name == ") { - raw["column"] = strings.TrimSuffix( + columns = append(columns, strings.TrimSuffix( strings.TrimPrefix(expression, `resource.column_name == "`), `"`, - ) + )) } if strings.HasPrefix(expression, "request.time < ") { raw["expire_timestamp"] = strings.TrimSuffix( @@ -480,12 +524,31 @@ func flattenMaskingExceptionPolicy(p *v1pb.MaskingExceptionPolicy) ([]interface{ `")`, ) } + if strings.HasPrefix(expression, "resource.column_name in [") { + // rawColumnListString should be: "col1", "col2" + rawColumnListString := strings.TrimSuffix( + strings.TrimPrefix(expression, `resource.column_name in [`), + `]`, + ) + rawColumnList := strings.SplitSeq(rawColumnListString, ",") + for rawColumn := range rawColumnList { + column := strings.TrimSuffix( + strings.TrimPrefix(strings.TrimSpace(rawColumn), `"`), + `"`, + ) + columns = append(columns, column) + } + } } if instanceID != "" && databaseName != "" { raw["database"] = fmt.Sprintf("%s%s/%s%s", internal.InstanceNamePrefix, instanceID, internal.DatabaseIDPrefix, databaseName) } + if len(columns) > 0 { + raw["columns"] = schema.NewSet(schema.HashString, columns) + } exceptionList = append(exceptionList, raw) } + policy := map[string]interface{}{ "exceptions": exceptionList, } @@ -493,9 +556,11 @@ func flattenMaskingExceptionPolicy(p *v1pb.MaskingExceptionPolicy) ([]interface{ } func exceptionHash(rawSchema interface{}) int { - exception, err := convertToV1Exception(rawSchema) + exceptions, err := convertToV1Exceptions(rawSchema) if err != nil { return 0 } - return internal.ToHash(exception) + return internal.ToHash(&v1pb.MaskingExceptionPolicy{ + MaskingExceptions: exceptions, + }) } diff --git a/provider/data_source_policy_list.go b/provider/data_source_policy_list.go index a7832ac..a1ce768 100644 --- a/provider/data_source_policy_list.go +++ b/provider/data_source_policy_list.go @@ -65,6 +65,9 @@ func dataSourcePolicyList() *schema.Resource { }, "masking_exception_policy": getMaskingExceptionPolicySchema(true), "global_masking_policy": getGlobalMaskingPolicySchema(true), + "disable_copy_data_policy": getDisableCopyDataPolicySchema(true), + "data_source_query_policy": getDataSourceQueryPolicySchema(true), + "rollout_policy": getRolloutPolicySchema(true), }, }, }, @@ -96,20 +99,11 @@ func dataSourcePolicyListRead(ctx context.Context, d *schema.ResourceData, m int raw["inherit_from_parent"] = policy.InheritFromParent raw["enforce"] = policy.Enforce - if p := policy.GetMaskingExceptionPolicy(); p != nil { - exceptionPolicy, err := flattenMaskingExceptionPolicy(p) - if err != nil { - return diag.FromErr(err) - } - raw["masking_exception_policy"] = exceptionPolicy - } - if p := policy.GetMaskingRulePolicy(); p != nil { - maskingPolicy, err := flattenGlobalMaskingPolicy(p) - if err != nil { - return diag.FromErr(err) - } - raw["global_masking_policy"] = maskingPolicy + key, payload, diags := flattenPolicyPayload(policy) + if diags != nil { + return diags } + raw[key] = payload policies = append(policies, raw) } diff --git a/provider/data_source_policy_test.go b/provider/data_source_policy_test.go index ab9b391..4072a0f 100644 --- a/provider/data_source_policy_test.go +++ b/provider/data_source_policy_test.go @@ -39,7 +39,7 @@ func TestAccPolicyDataSource(t *testing.T) { resource.TestCheckResourceAttr("data.bytebase_policy.masking_exception_policy", "masking_exception_policy.#", "1"), resource.TestCheckResourceAttr("data.bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.#", "1"), resource.TestCheckResourceAttr("data.bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.table", "salary"), - resource.TestCheckResourceAttr("data.bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.column", "amount"), + resource.TestCheckResourceAttr("data.bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.columns.0", "amount"), ), }, }, diff --git a/provider/resource_policy.go b/provider/resource_policy.go index 4d4466c..a925965 100644 --- a/provider/resource_policy.go +++ b/provider/resource_policy.go @@ -347,7 +347,7 @@ func convertToMaskingRulePolicy(d *schema.ResourceData) (*v1pb.MaskingRulePolicy return policy, nil } -func convertToV1Exception(rawSchema interface{}) (*v1pb.MaskingExceptionPolicy_MaskingException, error) { +func convertToV1Exceptions(rawSchema interface{}) ([]*v1pb.MaskingExceptionPolicy_MaskingException, error) { rawException := rawSchema.(map[string]interface{}) expressions := []string{} @@ -369,8 +369,13 @@ func convertToV1Exception(rawSchema interface{}) (*v1pb.MaskingExceptionPolicy_M 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 rawColumns, ok := rawException["columns"].(*schema.Set); ok && rawColumns.Len() > 0 { + columnNames := []string{} + for _, column := range rawColumns.List() { + columnNames = append(columnNames, fmt.Sprintf(`"%s"`, column.(string))) + } + expressions = append(expressions, fmt.Sprintf(`resource.column_name in [%s]`, strings.Join(columnNames, ", "))) } } @@ -381,23 +386,40 @@ func convertToV1Exception(rawSchema interface{}) (*v1pb.MaskingExceptionPolicy_M } expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339))) } - member := rawException["member"].(string) - if member == "allUsers" { - return nil, errors.Errorf("not support allUsers in masking_exception_policy") + + exceptions := []*v1pb.MaskingExceptionPolicy_MaskingException{} + reason := rawException["reason"].(string) + + rawMembers, ok := rawException["members"].(*schema.Set) + if !ok || rawMembers.Len() == 0 { + return nil, errors.Errorf("invalid members in masking_exception_policy.exceptions") } - if err := internal.ValidateMemberBinding(member); err != nil { - return nil, err + + rawActions, ok := rawException["actions"].(*schema.Set) + if !ok || rawActions.Len() == 0 { + return nil, errors.Errorf("invalid actions in masking_exception_policy.exceptions") } - return &v1pb.MaskingExceptionPolicy_MaskingException{ - Member: member, - Action: v1pb.MaskingExceptionPolicy_MaskingException_Action( - v1pb.MaskingExceptionPolicy_MaskingException_Action_value[rawException["action"].(string)], - ), - Condition: &expr.Expr{ - Description: rawException["reason"].(string), - Expression: strings.Join(expressions, " && "), - }, - }, nil + + for _, rawMember := range rawMembers.List() { + member := rawMember.(string) + if err := internal.ValidateMemberBinding(member); err != nil { + return nil, err + } + for _, action := range rawActions.List() { + exceptions = append(exceptions, &v1pb.MaskingExceptionPolicy_MaskingException{ + Member: member, + Action: v1pb.MaskingExceptionPolicy_MaskingException_Action( + v1pb.MaskingExceptionPolicy_MaskingException_Action_value[action.(string)], + ), + Condition: &expr.Expr{ + Description: reason, + Expression: strings.Join(expressions, " && "), + }, + }) + } + } + + return exceptions, nil } func convertToMaskingExceptionPolicy(d *schema.ResourceData) (*v1pb.MaskingExceptionPolicy, error) { @@ -415,11 +437,11 @@ func convertToMaskingExceptionPolicy(d *schema.ResourceData) (*v1pb.MaskingExcep policy := &v1pb.MaskingExceptionPolicy{} for _, raw := range exceptionList.List() { - exception, err := convertToV1Exception(raw) + exceptions, err := convertToV1Exceptions(raw) if err != nil { return nil, err } - policy.MaskingExceptions = append(policy.MaskingExceptions, exception) + policy.MaskingExceptions = append(policy.MaskingExceptions, exceptions...) } return policy, nil } diff --git a/provider/resource_policy_test.go b/provider/resource_policy_test.go index cd1eadd..66dbc25 100644 --- a/provider/resource_policy_test.go +++ b/provider/resource_policy_test.go @@ -35,7 +35,7 @@ func TestAccPolicy(t *testing.T) { 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.columns.0", "amount"), ), }, }, @@ -59,9 +59,9 @@ func getMaskingExceptionPolicy(database, table, column string) string { exceptions { database = "%s" table = "%s" - column = "%s" - member = "user:ed@bytebase.com" - action = "QUERY" + columns = ["%s"] + members = ["user:ed@bytebase.com"] + actions = ["QUERY"] } } `, database, table, column) diff --git a/tutorials/0-provider.tf b/tutorials/0-provider.tf index d6c867a..0ce6d75 100644 --- a/tutorials/0-provider.tf +++ b/tutorials/0-provider.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/tutorials/8-5-masking-exception.tf b/tutorials/8-5-masking-exception.tf index ff3275e..9c675e0 100644 --- a/tutorials/8-5-masking-exception.tf +++ b/tutorials/8-5-masking-exception.tf @@ -11,21 +11,12 @@ resource "bytebase_policy" "masking_exception_policy" { masking_exception_policy { exceptions { - reason = "Business requirement" - database = "instances/prod-sample-instance/databases/hr_prod" - table = "employee" - column = "birth_date" - member = "user:admin@example.com" - action = "QUERY" - expire_timestamp = "2027-07-30T16:11:49Z" - } - exceptions { - reason = "Export data for analysis" - database = "instances/prod-sample-instance/databases/hr_prod" - table = "employee" - column = "last_name" - member = "user:admin@example.com" - action = "EXPORT" + reason = "Business requirement" + database = "instances/prod-sample-instance/databases/hr_prod" + table = "employee" + columns = ["birth_date", "last_name"] + members = ["user:admin@example.com"] + actions = ["QUERY", "EXPORT"] expire_timestamp = "2027-07-30T16:11:49Z" } } From 82a89ac6e1bb561f157b3b6d838048916b49e570 Mon Sep 17 00:00:00 2001 From: ecmadao Date: Mon, 25 Aug 2025 17:50:35 +0800 Subject: [PATCH 5/5] chore: support raw_expression --- docs/data-sources/policy.md | 7 ++-- docs/data-sources/policy_list.md | 38 +++++++++++++++++-- docs/resources/policy.md | 7 ++-- examples/setup/data_masking.tf | 11 +++++- provider/data_source_policy.go | 13 +++++-- provider/resource_policy.go | 64 +++++++++++++++++--------------- 6 files changed, 98 insertions(+), 42 deletions(-) diff --git a/docs/data-sources/policy.md b/docs/data-sources/policy.md index 113cba2..b2064bc 100644 --- a/docs/data-sources/policy.md +++ b/docs/data-sources/policy.md @@ -87,14 +87,15 @@ Optional: Required: -- `action` (String) -- `member` (String) The member in user:{email} or group:{email} format. +- `actions` (Set of String) +- `members` (Set of String) Optional: -- `column` (String) +- `columns` (Set of 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 +- `raw_expression` (String) The raw CEL expression. We will use it as the masking exception and ignore the "database"/"schema"/"table"/"columns"/"expire_timestamp" fields if you provide the raw expression. - `reason` (String) The reason for the masking exemption - `schema` (String) - `table` (String) diff --git a/docs/data-sources/policy_list.md b/docs/data-sources/policy_list.md index f66e19e..2d39b8e 100644 --- a/docs/data-sources/policy_list.md +++ b/docs/data-sources/policy_list.md @@ -29,13 +29,34 @@ The policy data source list. Read-Only: +- `data_source_query_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--data_source_query_policy)) +- `disable_copy_data_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--disable_copy_data_policy)) - `enforce` (Boolean) - `global_masking_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--global_masking_policy)) - `inherit_from_parent` (Boolean) - `masking_exception_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--masking_exception_policy)) - `name` (String) +- `rollout_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--rollout_policy)) - `type` (String) + +### Nested Schema for `policies.data_source_query_policy` + +Read-Only: + +- `disallow_ddl` (Boolean) +- `disallow_dml` (Boolean) +- `restriction` (String) + + + +### Nested Schema for `policies.disable_copy_data_policy` + +Read-Only: + +- `enable` (Boolean) + + ### Nested Schema for `policies.global_masking_policy` @@ -67,13 +88,24 @@ Read-Only: Read-Only: -- `action` (String) -- `column` (String) +- `actions` (Set of String) +- `columns` (Set of String) - `database` (String) - `expire_timestamp` (String) -- `member` (String) +- `members` (Set of String) +- `raw_expression` (String) - `reason` (String) - `schema` (String) - `table` (String) + + +### Nested Schema for `policies.rollout_policy` + +Read-Only: + +- `automatic` (Boolean) +- `roles` (Set of String) + + diff --git a/docs/resources/policy.md b/docs/resources/policy.md index 4d5d1a7..cc20d1a 100644 --- a/docs/resources/policy.md +++ b/docs/resources/policy.md @@ -87,14 +87,15 @@ Optional: Required: -- `action` (String) -- `member` (String) The member in user:{email} or group:{email} format. +- `actions` (Set of String) +- `members` (Set of String) Optional: -- `column` (String) +- `columns` (Set of 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 +- `raw_expression` (String) The raw CEL expression. We will use it as the masking exception and ignore the "database"/"schema"/"table"/"columns"/"expire_timestamp" fields if you provide the raw expression. - `reason` (String) The reason for the masking exemption - `schema` (String) - `table` (String) diff --git a/examples/setup/data_masking.tf b/examples/setup/data_masking.tf index b8075a2..ce94e7a 100644 --- a/examples/setup/data_masking.tf +++ b/examples/setup/data_masking.tf @@ -131,7 +131,16 @@ resource "bytebase_policy" "masking_exception_policy" { format("user:%s", bytebase_user.workspace_dba.email), ] actions = ["EXPORT"] - reason = "Grant access" + reason = "Grant export access" + } + + exceptions { + members = [ + format("user:%s", bytebase_user.project_developer.email), + ] + actions = ["QUERY"] + reason = "Grant query access" + raw_expression = "resource.instance_id == \"test-sample-instance\" && resource.database_name == \"employee\" && resource.table_name == \"employee\" && resource.column_name in [\"first_name\", \"last_name\", \"gender\"]" } } } diff --git a/provider/data_source_policy.go b/provider/data_source_policy.go index 7997d1c..77f1bef 100644 --- a/provider/data_source_policy.go +++ b/provider/data_source_policy.go @@ -159,6 +159,12 @@ func getMaskingExceptionPolicySchema(computed bool) *schema.Schema { Optional: true, Description: "The expiration timestamp in YYYY-MM-DDThh:mm:ss.000Z format", }, + "raw_expression": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: `The raw CEL expression. We will use it as the masking exception and ignore the "database"/"schema"/"table"/"columns"/"expire_timestamp" fields if you provide the raw expression.`, + }, }, }, Set: exceptionHash, @@ -477,9 +483,10 @@ func flattenMaskingExceptionPolicy(p *v1pb.MaskingExceptionPolicy) ([]interface{ for _, combine := range exceptionMap { raw := map[string]interface{}{ - "members": schema.NewSet(schema.HashString, combine.members), - "actions": schema.NewSet(schema.HashString, combine.actions), - "reason": combine.reason, + "members": schema.NewSet(schema.HashString, combine.members), + "actions": schema.NewSet(schema.HashString, combine.actions), + "reason": combine.reason, + "raw_expression": combine.expression, } expressions := strings.Split(combine.expression, " && ") diff --git a/provider/resource_policy.go b/provider/resource_policy.go index a925965..e908391 100644 --- a/provider/resource_policy.go +++ b/provider/resource_policy.go @@ -351,40 +351,46 @@ func convertToV1Exceptions(rawSchema interface{}) ([]*v1pb.MaskingExceptionPolic rawException := rawSchema.(map[string]interface{}) expressions := []string{} - databaseFullName := rawException["database"].(string) - if databaseFullName != "" { - instanceID, databaseName, err := internal.GetInstanceDatabaseID(databaseFullName) - if err != nil { - return nil, errors.Wrapf(err, "invalid database full name: %v", databaseFullName) - } - expressions = append( - expressions, - 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)) - } + rawExpression := rawException["raw_expression"].(string) + + if rawExpression != "" { + expressions = append(expressions, rawExpression) + } else { + databaseFullName := rawException["database"].(string) + if databaseFullName != "" { + instanceID, databaseName, err := internal.GetInstanceDatabaseID(databaseFullName) + if err != nil { + return nil, errors.Wrapf(err, "invalid database full name: %v", databaseFullName) + } + expressions = append( + expressions, + 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 rawColumns, ok := rawException["columns"].(*schema.Set); ok && rawColumns.Len() > 0 { - columnNames := []string{} - for _, column := range rawColumns.List() { - columnNames = append(columnNames, fmt.Sprintf(`"%s"`, column.(string))) + if rawColumns, ok := rawException["columns"].(*schema.Set); ok && rawColumns.Len() > 0 { + columnNames := []string{} + for _, column := range rawColumns.List() { + columnNames = append(columnNames, fmt.Sprintf(`"%s"`, column.(string))) + } + expressions = append(expressions, fmt.Sprintf(`resource.column_name in [%s]`, strings.Join(columnNames, ", "))) } - expressions = append(expressions, fmt.Sprintf(`resource.column_name in [%s]`, strings.Join(columnNames, ", "))) } - } - if expire, ok := rawException["expire_timestamp"].(string); ok && expire != "" { - formattedTime, err := time.Parse(time.RFC3339, expire) - if err != nil { - return nil, errors.Wrapf(err, "invalid time: %v", expire) + if expire, ok := rawException["expire_timestamp"].(string); ok && expire != "" { + formattedTime, err := time.Parse(time.RFC3339, expire) + if err != nil { + return nil, errors.Wrapf(err, "invalid time: %v", expire) + } + expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339))) } - expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339))) } exceptions := []*v1pb.MaskingExceptionPolicy_MaskingException{}