diff --git a/api/client.go b/api/client.go index 9f742ca..7288a9d 100644 --- a/api/client.go +++ b/api/client.go @@ -86,4 +86,28 @@ type Client interface { // Cel // ParseExpression parse the expression string. ParseExpression(ctx context.Context, expression string) (*v1alpha1.Expr, error) + + // VCS Provider + // ListVCSProvider will returns all vcs providers. + ListVCSProvider(ctx context.Context) (*v1pb.ListVCSProvidersResponse, error) + // GetVCSProvider gets the vcs by id. + GetVCSProvider(ctx context.Context, name string) (*v1pb.VCSProvider, error) + // CreateVCSProvider creates the vcs provider. + CreateVCSProvider(ctx context.Context, vcsID string, vcs *v1pb.VCSProvider) (*v1pb.VCSProvider, error) + // UpdateVCSProvider updates the vcs provider. + UpdateVCSProvider(ctx context.Context, patch *v1pb.VCSProvider, updateMasks []string) (*v1pb.VCSConnector, error) + // DeleteVCSProvider deletes the vcs provider. + DeleteVCSProvider(ctx context.Context, name string) error + + // VCS Connector + // ListVCSConnector will returns all vcs connector in a project. + ListVCSConnector(ctx context.Context, projectName string) (*v1pb.ListVCSConnectorsResponse, error) + // GetVCSConnector gets the vcs connector by id. + GetVCSConnector(ctx context.Context, name string) (*v1pb.VCSConnector, error) + // CreateVCSConnector creates the vcs connector in a project. + CreateVCSConnector(ctx context.Context, projectName, connectorID string, connector *v1pb.VCSConnector) (*v1pb.VCSConnector, error) + // UpdateVCSConnector updates the vcs connector. + UpdateVCSConnector(ctx context.Context, patch *v1pb.VCSConnector, updateMasks []string) (*v1pb.VCSConnector, error) + // DeleteVCSConnector deletes the vcs provider. + DeleteVCSConnector(ctx context.Context, name string) error } diff --git a/client/common.go b/client/common.go index 92d30fa..23447d1 100644 --- a/client/common.go +++ b/client/common.go @@ -1,8 +1,76 @@ package client import ( + "context" + "fmt" + "net/http" + "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} + +// deleteResource deletes the resource by name. +func (c *client) deleteResource(ctx context.Context, name string) error { + req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/%s/%s", c.url, c.version, 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, name), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + return body, nil +} + +// deleteResource deletes the resource by name. +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, 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 string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", c.url, c.version, name), 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 05f090c..0677c17 100644 --- a/client/database.go +++ b/client/database.go @@ -5,20 +5,13 @@ import ( "fmt" "net/http" "net/url" - "strings" v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" - "google.golang.org/protobuf/encoding/protojson" ) -// GetDatabase gets the database by environment resource id, instance resource id and the database name. +// GetDatabase gets the database by the database name. func (c *client) GetDatabase(ctx context.Context, databaseName string) (*v1pb.Database, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", c.url, c.version, databaseName), nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.getResource(ctx, databaseName) if err != nil { return nil, err } @@ -58,17 +51,7 @@ func (c *client) ListDatabase(ctx context.Context, instanceID, filter string) (* // UpdateDatabase patches the database. func (c *client) UpdateDatabase(ctx context.Context, patch *v1pb.Database, updateMasks []string) (*v1pb.Database, 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", c.url, c.version, patch.Name, strings.Join(updateMasks, ",")), strings.NewReader(string(payload))) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) if err != nil { return nil, err } diff --git a/client/environment.go b/client/environment.go index 55a13be..e742578 100644 --- a/client/environment.go +++ b/client/environment.go @@ -37,12 +37,7 @@ func (c *client) CreateEnvironment(ctx context.Context, environmentID string, cr // GetEnvironment gets the environment by id. func (c *client) GetEnvironment(ctx context.Context, environmentName string) (*v1pb.Environment, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", c.url, c.version, environmentName), nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.getResource(ctx, environmentName) if err != nil { return nil, err } @@ -76,18 +71,8 @@ func (c *client) ListEnvironment(ctx context.Context, showDeleted bool) (*v1pb.L } // UpdateEnvironment updates the environment. -func (c *client) UpdateEnvironment(ctx context.Context, patch *v1pb.Environment, updateMask []string) (*v1pb.Environment, 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", c.url, c.version, patch.Name, strings.Join(updateMask, ",")), strings.NewReader(string(payload))) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) +func (c *client) UpdateEnvironment(ctx context.Context, patch *v1pb.Environment, updateMasks []string) (*v1pb.Environment, error) { + body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) if err != nil { return nil, err } @@ -102,25 +87,12 @@ func (c *client) UpdateEnvironment(ctx context.Context, patch *v1pb.Environment, // DeleteEnvironment deletes the environment. func (c *client) DeleteEnvironment(ctx context.Context, environmentName string) error { - req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/%s/%s", c.url, c.version, environmentName), nil) - if err != nil { - return err - } - - if _, err := c.doRequest(req); err != nil { - return err - } - return nil + return c.deleteResource(ctx, environmentName) } // UndeleteEnvironment undeletes the environment. func (c *client) UndeleteEnvironment(ctx context.Context, environmentName string) (*v1pb.Environment, error) { - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s:undelete", c.url, c.version, environmentName), nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.undeleteResource(ctx, environmentName) if err != nil { return nil, err } diff --git a/client/instance.go b/client/instance.go index 215baf6..09172ac 100644 --- a/client/instance.go +++ b/client/instance.go @@ -32,12 +32,7 @@ func (c *client) ListInstance(ctx context.Context, showDeleted bool) (*v1pb.List // GetInstance gets the instance by id. func (c *client) GetInstance(ctx context.Context, instanceName string) (*v1pb.Instance, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", c.url, c.version, instanceName), nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.getResource(ctx, instanceName) if err != nil { return nil, err } @@ -78,18 +73,7 @@ func (c *client) CreateInstance(ctx context.Context, instanceID string, instance // UpdateInstance updates the instance. func (c *client) UpdateInstance(ctx context.Context, patch *v1pb.Instance, updateMasks []string) (*v1pb.Instance, 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", c.url, c.version, patch.Name, strings.Join(updateMasks, ",")), strings.NewReader(string(payload))) - - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) if err != nil { return nil, err } @@ -104,25 +88,12 @@ func (c *client) UpdateInstance(ctx context.Context, patch *v1pb.Instance, updat // DeleteInstance deletes the instance. func (c *client) DeleteInstance(ctx context.Context, instanceName string) error { - req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/%s/%s", c.url, c.version, instanceName), nil) - if err != nil { - return err - } - - if _, err := c.doRequest(req); err != nil { - return err - } - return nil + return c.deleteResource(ctx, instanceName) } // UndeleteInstance undeletes the instance. func (c *client) UndeleteInstance(ctx context.Context, instanceName string) (*v1pb.Instance, error) { - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s:undelete", c.url, c.version, instanceName), nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.undeleteResource(ctx, instanceName) if err != nil { return nil, err } diff --git a/client/policy.go b/client/policy.go index 23e6e65..a2e8fcc 100644 --- a/client/policy.go +++ b/client/policy.go @@ -4,10 +4,8 @@ import ( "context" "fmt" "net/http" - "strings" v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" - "google.golang.org/protobuf/encoding/protojson" ) // ListPolicies lists policies in a specific resource. @@ -38,12 +36,7 @@ func (c *client) ListPolicies(ctx context.Context, parent string) (*v1pb.ListPol // GetPolicy gets a policy in a specific resource. func (c *client) GetPolicy(ctx context.Context, policyName string) (*v1pb.Policy, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", c.url, c.version, policyName), nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.getResource(ctx, policyName) if err != nil { return nil, err } @@ -58,17 +51,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) { - payload, err := protojson.Marshal(policy) - if err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, "PATCH", fmt.Sprintf("%s/%s/%s?allow_missing=true&update_mask=%s", c.url, c.version, policy.Name, strings.Join(updateMasks, ",")), strings.NewReader(string(payload))) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.updateResource(ctx, policy.Name, policy, updateMasks, true /* allow missing = true*/) if err != nil { return nil, err } @@ -83,13 +66,5 @@ 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 { - req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/%s/%s", c.url, c.version, policyName), nil) - if err != nil { - return err - } - - if _, err := c.doRequest(req); err != nil { - return err - } - return nil + return c.deleteResource(ctx, policyName) } diff --git a/client/project.go b/client/project.go index 278d750..6b5e7ef 100644 --- a/client/project.go +++ b/client/project.go @@ -12,12 +12,7 @@ import ( // GetProject gets the project by resource id. func (c *client) GetProject(ctx context.Context, projectName string) (*v1pb.Project, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", c.url, c.version, projectName), nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.getResource(ctx, projectName) if err != nil { return nil, err } @@ -77,19 +72,8 @@ 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, updateMask []string) (*v1pb.Project, 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", c.url, c.version, patch.Name, strings.Join(updateMask, ",")), strings.NewReader(string(payload))) - - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) +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 } @@ -104,25 +88,12 @@ func (c *client) UpdateProject(ctx context.Context, patch *v1pb.Project, updateM // DeleteProject deletes the project. func (c *client) DeleteProject(ctx context.Context, projectName string) error { - req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/%s/%s", c.url, c.version, projectName), nil) - if err != nil { - return err - } - - if _, err := c.doRequest(req); err != nil { - return err - } - return nil + return c.deleteResource(ctx, projectName) } // UndeleteProject undeletes the project. func (c *client) UndeleteProject(ctx context.Context, projectName string) (*v1pb.Project, error) { - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s:undelete", c.url, c.version, projectName), nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.undeleteResource(ctx, projectName) if err != nil { return nil, err } diff --git a/client/setting.go b/client/setting.go index 96fd47e..a33389d 100644 --- a/client/setting.go +++ b/client/setting.go @@ -4,10 +4,8 @@ import ( "context" "fmt" "net/http" - "strings" v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" - "google.golang.org/protobuf/encoding/protojson" ) // ListSettings lists all settings. @@ -32,12 +30,7 @@ func (c *client) ListSettings(ctx context.Context) (*v1pb.ListSettingsResponse, // GetSetting gets the setting by the name. func (c *client) GetSetting(ctx context.Context, settingName string) (*v1pb.Setting, error) { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s", c.url, c.version, settingName), nil) - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.getResource(ctx, settingName) if err != nil { return nil, err } @@ -52,18 +45,7 @@ func (c *client) GetSetting(ctx context.Context, settingName string) (*v1pb.Sett // UpsertSetting updates or creates the setting. func (c *client) UpsertSetting(ctx context.Context, upsert *v1pb.Setting, updateMasks []string) (*v1pb.Setting, error) { - payload, err := protojson.Marshal(upsert) - if err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, "PATCH", fmt.Sprintf("%s/%s/%s?update_mask=%s&allow_missing=true", c.url, c.version, upsert.Name, strings.Join(updateMasks, ",")), strings.NewReader(string(payload))) - - if err != nil { - return nil, err - } - - body, err := c.doRequest(req) + body, err := c.updateResource(ctx, upsert.Name, upsert, updateMasks, true /* allow missing = true*/) if err != nil { return nil, err } diff --git a/client/user.go b/client/user.go new file mode 100644 index 0000000..0b72705 --- /dev/null +++ b/client/user.go @@ -0,0 +1,107 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strings" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + "google.golang.org/protobuf/encoding/protojson" +) + +// ListUser list all users. +func (c *client) ListUser(ctx context.Context, showDeleted bool) (*v1pb.ListUsersResponse, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/users?showDeleted=%v", c.url, c.version, showDeleted), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var res v1pb.ListUsersResponse + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, 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 + } + + req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/users", c.url, c.version), strings.NewReader(string(payload))) + + if err != nil { + return nil, err + } + + body, err := c.doRequest(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 +} + +// GetUser gets the user by name. +func (c *client) GetUser(ctx context.Context, userName string) (*v1pb.User, error) { + body, err := c.getResource(ctx, userName) + if err != nil { + return nil, err + } + + var res v1pb.User + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, 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 + } + + var res v1pb.User + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// DeleteUser deletes the user by name. +func (c *client) DeleteUser(ctx context.Context, userName string) error { + return c.deleteResource(ctx, userName) +} + +// 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 + } + + var res v1pb.User + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} diff --git a/client/vcs.go b/client/vcs.go new file mode 100644 index 0000000..e36382f --- /dev/null +++ b/client/vcs.go @@ -0,0 +1,173 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strings" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + "google.golang.org/protobuf/encoding/protojson" +) + +// ListVCSProvider will returns all vcs providers. +func (c *client) ListVCSProvider(ctx context.Context) (*v1pb.ListVCSProvidersResponse, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/vcsProviders", c.url, c.version), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var res v1pb.ListVCSProvidersResponse + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// GetVCSProvider gets the vcs by id. +func (c *client) GetVCSProvider(ctx context.Context, name string) (*v1pb.VCSProvider, error) { + body, err := c.getResource(ctx, name) + if err != nil { + return nil, err + } + + var res v1pb.VCSProvider + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// CreateVCSProvider creates the vcs provider. +func (c *client) CreateVCSProvider(ctx context.Context, vcsID string, vcs *v1pb.VCSProvider) (*v1pb.VCSProvider, error) { + payload, err := protojson.Marshal(vcs) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/vcsProviders?vcsProviderId=%s", c.url, c.version, vcsID), strings.NewReader(string(payload))) + + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var res v1pb.VCSProvider + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// UpdateVCSProvider updates the vcs provider. +func (c *client) UpdateVCSProvider(ctx context.Context, patch *v1pb.VCSProvider, updateMasks []string) (*v1pb.VCSConnector, error) { + body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) + if err != nil { + return nil, err + } + + var res v1pb.VCSConnector + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// DeleteVCSProvider deletes the vcs provider. +func (c *client) DeleteVCSProvider(ctx context.Context, name string) error { + return c.deleteResource(ctx, name) +} + +// ListVCSConnector will returns all vcs connector in a project. +func (c *client) ListVCSConnector(ctx context.Context, projectName string) (*v1pb.ListVCSConnectorsResponse, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/%s/vcsConnectors", c.url, c.version, projectName), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var res v1pb.ListVCSConnectorsResponse + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// GetVCSConnector gets the vcs connector by id. +func (c *client) GetVCSConnector(ctx context.Context, name string) (*v1pb.VCSConnector, error) { + body, err := c.getResource(ctx, name) + if err != nil { + return nil, err + } + + var res v1pb.VCSConnector + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// CreateVCSConnector creates the vcs connector in a project. +func (c *client) CreateVCSConnector(ctx context.Context, projectName, connectorID string, connector *v1pb.VCSConnector) (*v1pb.VCSConnector, error) { + payload, err := protojson.Marshal(connector) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/%s/vcsConnectors?vcsConnectorId=%s", c.url, c.version, projectName, connectorID), strings.NewReader(string(payload))) + + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var res v1pb.VCSConnector + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// UpdateVCSConnector updates the vcs connector. +func (c *client) UpdateVCSConnector(ctx context.Context, patch *v1pb.VCSConnector, updateMasks []string) (*v1pb.VCSConnector, error) { + body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) + if err != nil { + return nil, err + } + + var res v1pb.VCSConnector + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// DeleteVCSConnector deletes the vcs provider. +func (c *client) DeleteVCSConnector(ctx context.Context, name string) error { + return c.deleteResource(ctx, name) +} diff --git a/docs/data-sources/policy.md b/docs/data-sources/policy.md index c519319..4ac2664 100644 --- a/docs/data-sources/policy.md +++ b/docs/data-sources/policy.md @@ -21,8 +21,8 @@ The policy data source. ### Optional -- `masking_exception_policy` (Block List) (see [below for nested schema](#nestedblock--masking_exception_policy)) -- `masking_policy` (Block List) (see [below for nested schema](#nestedblock--masking_policy)) +- `masking_exception_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--masking_exception_policy)) +- `masking_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--masking_policy)) - `parent` (String) The policy parent name for the policy, support projects/{resource id}, environments/{resource id}, instances/{resource id}, or instances/{resource id}/databases/{database name} ### Read-Only diff --git a/docs/data-sources/vcs_connector.md b/docs/data-sources/vcs_connector.md new file mode 100644 index 0000000..5e59ec7 --- /dev/null +++ b/docs/data-sources/vcs_connector.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_vcs_connector Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The vcs connector data source. +--- + +# bytebase_vcs_connector (Data Source) + +The vcs connector data source. + + + + +## Schema + +### Required + +- `project` (String) The project name in projects/{resource id} format. +- `resource_id` (String) The vcs connector unique resource id. + +### Optional + +- `database_group` (String) Apply changes to the database group. + +### Read-Only + +- `create_time` (String) The vcs connector create time in YYYY-MM-DDThh:mm:ss.000Z format +- `creator` (String) The vcs connector creator in users/{email} format. +- `id` (String) The ID of this resource. +- `name` (String) The vcs connector full name in projects/{project}/vcsConnector/{resource id} format. +- `repository_branch` (String) The connected repository branch in vcs provider. +- `repository_directory` (String) The connected repository directory in vcs provider. +- `repository_id` (String) The connected repository id in vcs provider. +- `repository_path` (String) The connected repository path in vcs provider. +- `repository_url` (String) The connected repository url in vcs provider. +- `title` (String) The vcs connector title. +- `update_time` (String) The vcs connector update time in YYYY-MM-DDThh:mm:ss.000Z format +- `updater` (String) The vcs connector updater in users/{email} format. +- `vcs_provider` (String) The vcs provider full name in vcsProviders/{resource id} format. + + diff --git a/docs/data-sources/vcs_connector_list.md b/docs/data-sources/vcs_connector_list.md new file mode 100644 index 0000000..7ed259a --- /dev/null +++ b/docs/data-sources/vcs_connector_list.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_vcs_connector_list Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The vcs connector data source list. +--- + +# bytebase_vcs_connector_list (Data Source) + +The vcs connector data source list. + + + + +## Schema + +### Required + +- `project` (String) The project name in projects/{resource id} format. + +### Read-Only + +- `id` (String) The ID of this resource. +- `vcs_connectors` (List of Object) (see [below for nested schema](#nestedatt--vcs_connectors)) + + +### Nested Schema for `vcs_connectors` + +Read-Only: + +- `create_time` (String) +- `creator` (String) +- `database_group` (String) +- `name` (String) +- `project` (String) +- `repository_branch` (String) +- `repository_directory` (String) +- `repository_id` (String) +- `repository_path` (String) +- `repository_url` (String) +- `resource_id` (String) +- `title` (String) +- `update_time` (String) +- `updater` (String) +- `vcs_provider` (String) + + diff --git a/docs/data-sources/vcs_provider.md b/docs/data-sources/vcs_provider.md new file mode 100644 index 0000000..808fef6 --- /dev/null +++ b/docs/data-sources/vcs_provider.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_vcs_provider Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The vcs provider data source. +--- + +# bytebase_vcs_provider (Data Source) + +The vcs provider data source. + + + + +## Schema + +### Required + +- `resource_id` (String) The vcs provider unique resource id. + +### Read-Only + +- `id` (String) The ID of this resource. +- `name` (String) The vcs provider full name in vcsProviders/{resource id} format. +- `title` (String) The vcs provider title. +- `type` (String) The vcs provider type. +- `url` (String) The vcs provider url. + + diff --git a/docs/data-sources/vcs_provider_list.md b/docs/data-sources/vcs_provider_list.md new file mode 100644 index 0000000..fdaa0b7 --- /dev/null +++ b/docs/data-sources/vcs_provider_list.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_vcs_provider_list Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The vcs provider data source list. +--- + +# bytebase_vcs_provider_list (Data Source) + +The vcs provider data source list. + + + + +## Schema + +### Read-Only + +- `id` (String) The ID of this resource. +- `vcs_providers` (List of Object) (see [below for nested schema](#nestedatt--vcs_providers)) + + +### Nested Schema for `vcs_providers` + +Read-Only: + +- `name` (String) +- `resource_id` (String) +- `title` (String) +- `type` (String) +- `url` (String) + + diff --git a/docs/resources/environment.md b/docs/resources/environment.md index 9ccec51..d9f22d1 100644 --- a/docs/resources/environment.md +++ b/docs/resources/environment.md @@ -20,7 +20,7 @@ The environment resource. - `environment_tier_policy` (String) If marked as PROTECTED, developers cannot execute any query on this environment's databases using SQL Editor by default. - `order` (Number) The environment sorting order. - `resource_id` (String) The environment unique resource id. -- `title` (String) The environment unique name. +- `title` (String) The environment title. ### Read-Only diff --git a/docs/resources/policy.md b/docs/resources/policy.md index 7df802b..315c84e 100644 --- a/docs/resources/policy.md +++ b/docs/resources/policy.md @@ -24,8 +24,8 @@ The policy resource. - `enforce` (Boolean) Decide if the policy is enforced. - `inherit_from_parent` (Boolean) Decide if the policy should inherit from the parent. -- `masking_exception_policy` (Block List) (see [below for nested schema](#nestedblock--masking_exception_policy)) -- `masking_policy` (Block List) (see [below for nested schema](#nestedblock--masking_policy)) +- `masking_exception_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--masking_exception_policy)) +- `masking_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--masking_policy)) ### Read-Only diff --git a/docs/resources/vcs_connector.md b/docs/resources/vcs_connector.md new file mode 100644 index 0000000..e89a4b1 --- /dev/null +++ b/docs/resources/vcs_connector.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_vcs_connector Resource - terraform-provider-bytebase" +subcategory: "" +description: |- + The vcs connector resource. +--- + +# bytebase_vcs_connector (Resource) + +The vcs connector resource. + + + + +## Schema + +### Required + +- `project` (String) The project name in projects/{resource id} format. +- `repository_branch` (String) The connected repository branch in vcs provider. +- `repository_directory` (String) The connected repository directory in vcs provider. +- `repository_id` (String) The connected repository id in vcs provider. +- `repository_path` (String) The connected repository path in vcs provider. +- `repository_url` (String) The connected repository url in vcs provider. +- `resource_id` (String) The vcs connector unique resource id. +- `title` (String) The vcs connector title. +- `vcs_provider` (String) The vcs provider full name in vcsProviders/{resource id} format. + +### Optional + +- `database_group` (String) Apply changes to the database group. + +### Read-Only + +- `create_time` (String) The vcs connector create time in YYYY-MM-DDThh:mm:ss.000Z format +- `creator` (String) The vcs connector creator in users/{email} format. +- `id` (String) The ID of this resource. +- `name` (String) The vcs connector full name in projects/{project}/vcsConnector/{resource id} format. +- `update_time` (String) The vcs connector update time in YYYY-MM-DDThh:mm:ss.000Z format +- `updater` (String) The vcs connector updater in users/{email} format. + + diff --git a/docs/resources/vcs_provider.md b/docs/resources/vcs_provider.md new file mode 100644 index 0000000..51bcab7 --- /dev/null +++ b/docs/resources/vcs_provider.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_vcs_provider Resource - terraform-provider-bytebase" +subcategory: "" +description: |- + The vcs provider resource. +--- + +# bytebase_vcs_provider (Resource) + +The vcs provider resource. + + + + +## Schema + +### Required + +- `access_token` (String, Sensitive) The vcs provider token. Check the docs https://bytebase.cc/docs/vcs-integration/add-git-provider for details. +- `resource_id` (String) The vcs provider unique resource id. +- `title` (String) The vcs provider title. +- `type` (String) The vcs provider type. + +### Optional + +- `url` (String) The vcs provider url. You need to provide the url if you're using the self-host GitLab or self-host GitHub. + +### Read-Only + +- `id` (String) The ID of this resource. +- `name` (String) The vcs provider full name in vcsProviders/{resource id} format. + + diff --git a/examples/policies/main.tf b/examples/policies/main.tf index b0da04d..fe07e4a 100644 --- a/examples/policies/main.tf +++ b/examples/policies/main.tf @@ -26,3 +26,11 @@ data "bytebase_policy" "masking_exception_policy" { parent = "projects/project-sample" type = "MASKING_EXCEPTION" } + +output "masking_policy" { + value = data.bytebase_policy.masking_policy +} + +output "masking_exception_policy" { + value = data.bytebase_policy.masking_exception_policy +} diff --git a/examples/settings/main.tf b/examples/settings/main.tf index 377e866..d130a71 100644 --- a/examples/settings/main.tf +++ b/examples/settings/main.tf @@ -24,3 +24,11 @@ data "bytebase_setting" "approval_flow" { data "bytebase_setting" "external_approval" { name = "bb.workspace.approval.external" } + +output "approval_flow" { + value = data.bytebase_setting.approval_flow +} + +output "external_approval" { + value = data.bytebase_setting.external_approval +} diff --git a/examples/setup/main.tf b/examples/setup/main.tf index 93b1dd7..34ee83e 100644 --- a/examples/setup/main.tf +++ b/examples/setup/main.tf @@ -208,3 +208,29 @@ resource "bytebase_policy" "masking_exception_policy" { } } } + +resource "bytebase_vcs_provider" "github" { + resource_id = "vcs-github" + title = "GitHub GitOps" + type = "GITHUB" + access_token = "" +} + +resource "bytebase_vcs_connector" "github" { + depends_on = [ + bytebase_project.sample_project, + bytebase_vcs_provider.github + ] + + resource_id = "connector-github" + title = "GitHub Connector" + project = bytebase_project.sample_project.name + vcs_provider = bytebase_vcs_provider.github.name + repository_id = "ed-bytebase/gitops" + repository_path = "ed-bytebase/gitops" + repository_directory = "/bytebase" + repository_branch = "main" + repository_url = "https://github.com/ed-bytebase/gitops" +} + + diff --git a/examples/vcs/main.tf b/examples/vcs/main.tf new file mode 100644 index 0000000..3e763b3 --- /dev/null +++ b/examples/vcs/main.tf @@ -0,0 +1,46 @@ +terraform { + required_providers { + bytebase = { + version = "1.0.4" + # For local development, please use "terraform.local/bytebase/bytebase" instead + source = "registry.terraform.io/bytebase/bytebase" + } + } +} + +provider "bytebase" { + # You need to replace the account and key with your Bytebase service account. + service_account = "terraform@service.bytebase.com" + service_key = "bbs_BxVIp7uQsARl8nR92ZZV" + # The Bytebase service URL. You can use the external URL in production. + # Check the docs about external URL: https://www.bytebase.com/docs/get-started/install/external-url + url = "https://bytebase.example.com" +} + +locals { + project_id = "project-sample" +} + +data "bytebase_vcs_provider" "github" { + resource_id = "vcs-github" +} + +data "bytebase_project" "sample_project" { + resource_id = local.project_id +} + +data "bytebase_vcs_connector" "github" { + depends_on = [ + data.bytebase_project.sample_project + ] + resource_id = "connector-github" + project = data.bytebase_project.sample_project.name +} + +output "vcs_provider_github" { + value = data.bytebase_vcs_provider.github +} + +output "vcs_connector_github" { + value = data.bytebase_vcs_connector.github +} diff --git a/provider/data_source_policy.go b/provider/data_source_policy.go index 1be5abd..f0ce11a 100644 --- a/provider/data_source_policy.go +++ b/provider/data_source_policy.go @@ -76,12 +76,15 @@ func getMaskingExceptionPolicySchema(computed bool) *schema.Schema { Optional: true, Default: nil, Type: schema.TypeList, + MinItems: 0, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "exceptions": { Computed: computed, Optional: true, Default: nil, + MinItems: 0, Type: schema.TypeList, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -154,9 +157,12 @@ func getMaskingPolicySchema(computed bool) *schema.Schema { Optional: true, Default: nil, Type: schema.TypeList, + MinItems: 0, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "mask_data": { + MinItems: 0, Computed: computed, Optional: true, Default: nil, diff --git a/provider/data_source_setting.go b/provider/data_source_setting.go index 5b6a78a..91c8c4d 100644 --- a/provider/data_source_setting.go +++ b/provider/data_source_setting.go @@ -142,6 +142,7 @@ func getWorkspaceApprovalSetting(computed bool) *schema.Schema { }, }, "conditions": { + MinItems: 0, Computed: computed, Type: schema.TypeList, Optional: true, diff --git a/provider/data_source_vcs_connector.go b/provider/data_source_vcs_connector.go new file mode 100644 index 0000000..599d6e9 --- /dev/null +++ b/provider/data_source_vcs_connector.go @@ -0,0 +1,173 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func dataSourceVCSConnector() *schema.Resource { + return &schema.Resource{ + Description: "The vcs connector data source.", + ReadContext: dataSourceVCSConnectorRead, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: internal.ResourceIDValidation, + Description: "The vcs connector unique resource id.", + }, + "project": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: internal.ResourceNameValidation(regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern))), + Description: "The project name in projects/{resource id} format.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector full name in projects/{project}/vcsConnector/{resource id} format.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector title.", + }, + "creator": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector creator in users/{email} format.", + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector create time in YYYY-MM-DDThh:mm:ss.000Z format", + }, + "updater": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector updater in users/{email} format.", + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector update time in YYYY-MM-DDThh:mm:ss.000Z format", + }, + "vcs_provider": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider full name in vcsProviders/{resource id} format.", + }, + "database_group": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: "Apply changes to the database group.", + }, + "repository_id": { + Type: schema.TypeString, + Computed: true, + Description: "The connected repository id in vcs provider.", + }, + "repository_path": { + Type: schema.TypeString, + Computed: true, + Description: "The connected repository path in vcs provider.", + }, + "repository_directory": { + Type: schema.TypeString, + Computed: true, + Description: "The connected repository directory in vcs provider.", + }, + "repository_branch": { + Type: schema.TypeString, + Computed: true, + Description: "The connected repository branch in vcs provider.", + }, + "repository_url": { + Type: schema.TypeString, + Computed: true, + Description: "The connected repository url in vcs provider.", + }, + }, + } +} + +func dataSourceVCSConnectorRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + project := d.Get("project").(string) + connectorName := fmt.Sprintf("%s/%s%s", project, internal.VCSConnectorNamePrefix, d.Get("resource_id").(string)) + + connector, err := c.GetVCSConnector(ctx, connectorName) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(connector.Name) + + return setVCSConnector(d, connector) +} + +func setVCSConnector(d *schema.ResourceData, connector *v1pb.VCSConnector) diag.Diagnostics { + projectID, connectorID, err := internal.GetVCSConnectorID(connector.Name) + if err != nil { + return diag.FromErr(err) + } + + if err := d.Set("resource_id", connectorID); err != nil { + return diag.Errorf("cannot set resource_id for vcs connector: %s", err.Error()) + } + if err := d.Set("project", fmt.Sprintf("%s%s", internal.ProjectNamePrefix, projectID)); err != nil { + return diag.Errorf("cannot set project for vcs connector: %s", err.Error()) + } + if err := d.Set("title", connector.Title); err != nil { + return diag.Errorf("cannot set title for vcs connector: %s", err.Error()) + } + if err := d.Set("name", connector.Name); err != nil { + return diag.Errorf("cannot set name for vcs connector: %s", err.Error()) + } + if err := d.Set("creator", connector.Creator); err != nil { + return diag.Errorf("cannot set creator for vcs connector: %s", err.Error()) + } + if err := d.Set("create_time", connector.CreateTime.AsTime().UTC().Format(time.RFC3339)); err != nil { + return diag.Errorf("cannot set create_time for vcs connector: %s", err.Error()) + } + if err := d.Set("updater", connector.Updater); err != nil { + return diag.Errorf("cannot set updater for vcs connector: %s", err.Error()) + } + if err := d.Set("update_time", connector.UpdateTime.AsTime().UTC().Format(time.RFC3339)); err != nil { + return diag.Errorf("cannot set update_time for vcs connector: %s", err.Error()) + } + if err := d.Set("vcs_provider", connector.VcsProvider); err != nil { + return diag.Errorf("cannot set vcs_provider for vcs connector: %s", err.Error()) + } + if err := d.Set("database_group", connector.DatabaseGroup); err != nil { + return diag.Errorf("cannot set database_group for vcs connector: %s", err.Error()) + } + if err := d.Set("repository_id", connector.ExternalId); err != nil { + return diag.Errorf("cannot set repository_id for vcs connector: %s", err.Error()) + } + if err := d.Set("repository_path", connector.FullPath); err != nil { + return diag.Errorf("cannot set repository_path for vcs connector: %s", err.Error()) + } + if err := d.Set("repository_directory", connector.BaseDirectory); err != nil { + return diag.Errorf("cannot set repository_directory for vcs connector: %s", err.Error()) + } + if err := d.Set("repository_branch", connector.Branch); err != nil { + return diag.Errorf("cannot set repository_branch for vcs connector: %s", err.Error()) + } + if err := d.Set("repository_url", connector.WebUrl); err != nil { + return diag.Errorf("cannot set repository_url for vcs connector: %s", err.Error()) + } + + return nil +} diff --git a/provider/data_source_vcs_connector_list.go b/provider/data_source_vcs_connector_list.go new file mode 100644 index 0000000..00f7012 --- /dev/null +++ b/provider/data_source_vcs_connector_list.go @@ -0,0 +1,160 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + "strconv" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func dataSourceVCSConnectorList() *schema.Resource { + return &schema.Resource{ + Description: "The vcs connector data source list.", + ReadContext: dataSourceVCSConnectorListRead, + Schema: map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: internal.ResourceNameValidation(regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern))), + Description: "The project name in projects/{resource id} format.", + }, + "vcs_connectors": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector unique resource id.", + }, + "project": { + Type: schema.TypeString, + Computed: true, + Description: "The project name in projects/{resource id} format.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector full name in projects/{project}/vcsConnector/{resource id} format.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector title.", + }, + "creator": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector creator in users/{email} format.", + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector create time in YYYY-MM-DDThh:mm:ss.000Z format", + }, + "updater": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector updater in users/{email} format.", + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector update time in YYYY-MM-DDThh:mm:ss.000Z format", + }, + "vcs_provider": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider full name in vcsProviders/{resource id} format.", + }, + "database_group": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: "Apply changes to the database group.", + }, + "repository_id": { + Type: schema.TypeString, + Computed: true, + Description: "The connected repository id in vcs provider.", + }, + "repository_path": { + Type: schema.TypeString, + Computed: true, + Description: "The connected repository path in vcs provider.", + }, + "repository_directory": { + Type: schema.TypeString, + Computed: true, + Description: "The connected repository directory in vcs provider.", + }, + "repository_branch": { + Type: schema.TypeString, + Computed: true, + Description: "The connected repository branch in vcs provider.", + }, + "repository_url": { + Type: schema.TypeString, + Computed: true, + Description: "The connected repository url in vcs provider.", + }, + }, + }, + }, + }, + } +} + +func dataSourceVCSConnectorListRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + project := d.Get("project").(string) + + response, err := c.ListVCSConnector(ctx, project) + if err != nil { + return diag.FromErr(err) + } + + connectors := []map[string]interface{}{} + for _, connector := range response.VcsConnectors { + projectID, connectorID, err := internal.GetVCSConnectorID(connector.Name) + if err != nil { + return diag.FromErr(err) + } + + rawConnector := make(map[string]interface{}) + rawConnector["resource_id"] = connectorID + rawConnector["project"] = fmt.Sprintf("%s%s", internal.ProjectNamePrefix, projectID) + rawConnector["title"] = connector.Title + rawConnector["name"] = connector.Name + rawConnector["creator"] = connector.Creator + rawConnector["create_time"] = connector.CreateTime.AsTime().UTC().Format(time.RFC3339) + rawConnector["updater"] = connector.Updater + rawConnector["update_time"] = connector.UpdateTime.AsTime().UTC().Format(time.RFC3339) + rawConnector["vcs_provider"] = connector.VcsProvider + rawConnector["database_group"] = connector.DatabaseGroup + rawConnector["repository_id"] = connector.ExternalId + rawConnector["repository_path"] = connector.FullPath + rawConnector["repository_directory"] = connector.BaseDirectory + rawConnector["repository_branch"] = connector.Branch + rawConnector["repository_url"] = connector.WebUrl + + connectors = append(connectors, rawConnector) + } + + if err := d.Set("vcs_connectors", connectors); err != nil { + return diag.FromErr(err) + } + + // always refresh + d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) + + return nil +} diff --git a/provider/data_source_vcs_provider.go b/provider/data_source_vcs_provider.go new file mode 100644 index 0000000..660870c --- /dev/null +++ b/provider/data_source_vcs_provider.go @@ -0,0 +1,88 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func dataSourceVCSProvider() *schema.Resource { + return &schema.Resource{ + Description: "The vcs provider data source.", + ReadContext: dataSourceVCSProviderRead, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: internal.ResourceIDValidation, + Description: "The vcs provider unique resource id.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider title.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider full name in vcsProviders/{resource id} format.", + }, + "type": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider type.", + }, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider url.", + }, + }, + } +} + +func dataSourceVCSProviderRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + providerName := fmt.Sprintf("%s%s", internal.VCSProviderNamePrefix, d.Get("resource_id").(string)) + + provider, err := c.GetVCSProvider(ctx, providerName) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(provider.Name) + + return setVCSProvider(d, provider) +} + +func setVCSProvider(d *schema.ResourceData, provider *v1pb.VCSProvider) diag.Diagnostics { + providerID, err := internal.GetVCSProviderID(provider.Name) + if err != nil { + return diag.FromErr(err) + } + + if err := d.Set("resource_id", providerID); err != nil { + return diag.Errorf("cannot set resource_id for vcs provider: %s", err.Error()) + } + if err := d.Set("title", provider.Title); err != nil { + return diag.Errorf("cannot set title for vcs provider: %s", err.Error()) + } + if err := d.Set("name", provider.Name); err != nil { + return diag.Errorf("cannot set name for vcs provider: %s", err.Error()) + } + if err := d.Set("type", provider.Type.String()); err != nil { + return diag.Errorf("cannot set type for vcs provider: %s", err.Error()) + } + if err := d.Set("url", provider.Url); err != nil { + return diag.Errorf("cannot set url for vcs provider: %s", err.Error()) + } + + return nil +} diff --git a/provider/data_source_vcs_provider_list.go b/provider/data_source_vcs_provider_list.go new file mode 100644 index 0000000..8f4ecf4 --- /dev/null +++ b/provider/data_source_vcs_provider_list.go @@ -0,0 +1,93 @@ +package provider + +import ( + "context" + "strconv" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func dataSourceVCSProviderList() *schema.Resource { + return &schema.Resource{ + Description: "The vcs provider data source list.", + ReadContext: dataSourceVCSProviderListRead, + Schema: map[string]*schema.Schema{ + "vcs_providers": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider unique resource id.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider title.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider full name in vcsProviders/{resource id} format.", + }, + "type": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider type.", + }, + "url": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider url.", + }, + }, + }, + }, + }, + } +} + +func dataSourceVCSProviderListRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + // Warning or errors can be collected in a slice type + var diags diag.Diagnostics + + response, err := c.ListVCSProvider(ctx) + if err != nil { + return diag.FromErr(err) + } + + providers := []map[string]interface{}{} + for _, provider := range response.VcsProviders { + providerID, err := internal.GetVCSProviderID(provider.Name) + if err != nil { + return diag.FromErr(err) + } + + rawProvider := make(map[string]interface{}) + rawProvider["resource_id"] = providerID + rawProvider["title"] = provider.Title + rawProvider["name"] = provider.Name + rawProvider["type"] = provider.Type.String() + rawProvider["url"] = provider.Url + + providers = append(providers, rawProvider) + } + + if err := d.Set("vcs_providers", providers); err != nil { + return diag.FromErr(err) + } + + // always refresh + d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) + + return diags +} diff --git a/provider/internal/mock_client.go b/provider/internal/mock_client.go index 4cf78d3..884f75e 100644 --- a/provider/internal/mock_client.go +++ b/provider/internal/mock_client.go @@ -504,6 +504,57 @@ func (c *mockClient) UpsertSetting(_ context.Context, upsert *v1pb.Setting, _ [] return c.settingMap[upsert.Name], nil } +// ParseExpression parse the expression string. func (*mockClient) ParseExpression(_ context.Context, _ string) (*v1alpha1.Expr, error) { return nil, nil } + +// ListVCSProvider will returns all vcs providers. +func (*mockClient) ListVCSProvider(_ context.Context) (*v1pb.ListVCSProvidersResponse, error) { + return nil, nil +} + +// GetVCSProvider gets the vcs by id. +func (*mockClient) GetVCSProvider(_ context.Context, _ string) (*v1pb.VCSProvider, error) { + return nil, nil +} + +// CreateVCSProvider creates the vcs provider. +func (*mockClient) CreateVCSProvider(_ context.Context, _ string, _ *v1pb.VCSProvider) (*v1pb.VCSProvider, error) { + return nil, nil +} + +// UpdateVCSProvider updates the vcs provider. +func (*mockClient) UpdateVCSProvider(_ context.Context, _ *v1pb.VCSProvider, _ []string) (*v1pb.VCSConnector, error) { + return nil, nil +} + +// DeleteVCSProvider deletes the vcs provider. +func (*mockClient) DeleteVCSProvider(_ context.Context, _ string) error { + return nil +} + +// ListVCSConnector will returns all vcs connector in a project. +func (*mockClient) ListVCSConnector(_ context.Context, _ string) (*v1pb.ListVCSConnectorsResponse, error) { + return nil, nil +} + +// GetVCSConnector gets the vcs connector by id. +func (*mockClient) GetVCSConnector(_ context.Context, _ string) (*v1pb.VCSConnector, error) { + return nil, nil +} + +// CreateVCSConnector creates the vcs connector in a project. +func (*mockClient) CreateVCSConnector(_ context.Context, _, _ string, _ *v1pb.VCSConnector) (*v1pb.VCSConnector, error) { + return nil, nil +} + +// UpdateVCSConnector updates the vcs connector. +func (*mockClient) UpdateVCSConnector(_ context.Context, _ *v1pb.VCSConnector, _ []string) (*v1pb.VCSConnector, error) { + return nil, nil +} + +// DeleteVCSConnector deletes the vcs provider. +func (*mockClient) DeleteVCSConnector(_ context.Context, _ string) error { + return nil +} diff --git a/provider/internal/utils.go b/provider/internal/utils.go index 8a8a435..3e1e877 100644 --- a/provider/internal/utils.go +++ b/provider/internal/utils.go @@ -26,6 +26,12 @@ const ( PolicyNamePrefix = "policies/" // SettingNamePrefix is the prefix for setting unique name. SettingNamePrefix = "settings/" + // VCSProviderNamePrefix is the prefix for vcs provider unique name. + VCSProviderNamePrefix = "vcsProviders/" + // VCSConnectorNamePrefix is the prefix for vcs connector unique name. + VCSConnectorNamePrefix = "vcsConnectors/" + // UserNamePrefix is the prefix for user name. + UserNamePrefix = "users/" // ResourceIDPattern is the pattern for resource id. ResourceIDPattern = "[a-z]([a-z0-9-]{0,61}[a-z0-9])?" ) @@ -92,6 +98,26 @@ func GetEnvironmentID(name string) (string, error) { return tokens[0], nil } +// GetVCSProviderID will parse the vcs provider resource id. +func GetVCSProviderID(name string) (string, error) { + // the vcs provider name should be vcsProviders/{resource-id} + tokens, err := getNameParentTokens(name, VCSProviderNamePrefix) + if err != nil { + return "", err + } + return tokens[0], nil +} + +// GetVCSConnectorID will parse the vcs connector resource id. +func GetVCSConnectorID(name string) (string, string, error) { + // the vcs connector name should be projects/{project}/vcsConnectors/{resource-id} + tokens, err := getNameParentTokens(name, ProjectNamePrefix, VCSConnectorNamePrefix) + if err != nil { + return "", "", err + } + return tokens[0], tokens[1], nil +} + // GetInstanceID will parse the environment resource id and instance resource id. func GetInstanceID(name string) (string, error) { // the instance request should be instances/{instance-id} diff --git a/provider/provider.go b/provider/provider.go index d53005b..4abfe57 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -49,22 +49,28 @@ func NewProvider() *schema.Provider { }, ConfigureContextFunc: providerConfigure, DataSourcesMap: map[string]*schema.Resource{ - "bytebase_instance": dataSourceInstance(), - "bytebase_instance_list": dataSourceInstanceList(), - "bytebase_environment": dataSourceEnvironment(), - "bytebase_environment_list": dataSourceEnvironmentList(), - "bytebase_policy": dataSourcePolicy(), - "bytebase_policy_list": dataSourcePolicyList(), - "bytebase_project": dataSourceProject(), - "bytebase_project_list": dataSourceProjectList(), - "bytebase_setting": dataSourceSetting(), + "bytebase_instance": dataSourceInstance(), + "bytebase_instance_list": dataSourceInstanceList(), + "bytebase_environment": dataSourceEnvironment(), + "bytebase_environment_list": dataSourceEnvironmentList(), + "bytebase_policy": dataSourcePolicy(), + "bytebase_policy_list": dataSourcePolicyList(), + "bytebase_project": dataSourceProject(), + "bytebase_project_list": dataSourceProjectList(), + "bytebase_setting": dataSourceSetting(), + "bytebase_vcs_provider": dataSourceVCSProvider(), + "bytebase_vcs_provider_list": dataSourceVCSProviderList(), + "bytebase_vcs_connector": dataSourceVCSConnector(), + "bytebase_vcs_connector_list": dataSourceVCSConnectorList(), }, ResourcesMap: map[string]*schema.Resource{ - "bytebase_environment": resourceEnvironment(), - "bytebase_instance": resourceInstance(), - "bytebase_policy": resourcePolicy(), - "bytebase_project": resourceProjct(), - "bytebase_setting": resourceSetting(), + "bytebase_environment": resourceEnvironment(), + "bytebase_instance": resourceInstance(), + "bytebase_policy": resourcePolicy(), + "bytebase_project": resourceProjct(), + "bytebase_setting": resourceSetting(), + "bytebase_vcs_provider": resourceVCSProvider(), + "bytebase_vcs_connector": resourceVCSConnector(), }, } } diff --git a/provider/resource_environment.go b/provider/resource_environment.go index 1e703a7..e783870 100644 --- a/provider/resource_environment.go +++ b/provider/resource_environment.go @@ -38,7 +38,7 @@ func resourceEnvironment() *schema.Resource { "title": { Type: schema.TypeString, Required: true, - Description: "The environment unique name.", + Description: "The environment title.", ValidateFunc: validation.StringMatch(environmentTitleRegex, fmt.Sprintf("environment title must matches %v", environmentTitleRegex)), }, "name": { @@ -104,36 +104,47 @@ func resourceEnvironmentCreate(ctx context.Context, d *schema.ResourceData, m in } } - env, err := c.UpdateEnvironment(ctx, &v1pb.Environment{ - Name: environmentName, - Title: title, - Order: int32(order), - Tier: tier, - }, []string{"title", "order", "tier"}) - if err != nil { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Failed to update environment", - Detail: fmt.Sprintf("Update environment %s failed, error: %v", environmentName, err), - }) - return diags + updateMasks := []string{} + if title != "" && title != existedEnv.Title { + updateMasks = append(updateMasks, "title") + } + if order != int(existedEnv.Order) { + updateMasks = append(updateMasks, "order") + } + if tier != existedEnv.Tier { + updateMasks = append(updateMasks, "tier") } - d.SetId(env.Name) + if len(updateMasks) > 0 { + if _, err := c.UpdateEnvironment(ctx, &v1pb.Environment{ + Name: environmentName, + Title: title, + Order: int32(order), + Tier: tier, + State: v1pb.State_ACTIVE, + }, updateMasks); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update environment", + Detail: fmt.Sprintf("Update environment %s failed, error: %v", environmentName, err), + }) + return diags + } + } } else { - env, err := c.CreateEnvironment(ctx, environmentID, &v1pb.Environment{ + if _, err := c.CreateEnvironment(ctx, environmentID, &v1pb.Environment{ Name: environmentName, Title: title, Order: int32(order), Tier: tier, - }) - if err != nil { + State: v1pb.State_ACTIVE, + }); err != nil { return diag.FromErr(err) } - - d.SetId(env.Name) } + d.SetId(environmentName) + diag := resourceEnvironmentRead(ctx, d, m) if diag != nil { diags = append(diags, diag...) @@ -197,17 +208,20 @@ func resourceEnvironmentUpdate(ctx context.Context, d *schema.ResourceData, m in paths = append(paths, "tier") } - title := d.Get("title").(string) - order := d.Get("order").(int) - tier := v1pb.EnvironmentTier(v1pb.EnvironmentTier_value[d.Get("environment_tier_policy").(string)]) + if len(paths) > 0 { + title := d.Get("title").(string) + order := d.Get("order").(int) + tier := v1pb.EnvironmentTier(v1pb.EnvironmentTier_value[d.Get("environment_tier_policy").(string)]) - if _, err := c.UpdateEnvironment(ctx, &v1pb.Environment{ - Name: environmentName, - Title: title, - Order: int32(order), - Tier: tier, - }, paths); err != nil { - return diag.FromErr(err) + if _, err := c.UpdateEnvironment(ctx, &v1pb.Environment{ + Name: environmentName, + Title: title, + Order: int32(order), + Tier: tier, + State: v1pb.State_ACTIVE, + }, paths); err != nil { + return diag.FromErr(err) + } } diag := resourceEnvironmentRead(ctx, d, m) diff --git a/provider/resource_instance.go b/provider/resource_instance.go index 948d77a..cbd2b91 100644 --- a/provider/resource_instance.go +++ b/provider/resource_instance.go @@ -175,6 +175,8 @@ func resourceInstanceCreate(ctx context.Context, d *schema.ResourceData, m inter instanceID := d.Get("resource_id").(string) instanceName := fmt.Sprintf("%s%s", internal.InstanceNamePrefix, instanceID) + title := d.Get("title").(string) + externalLink := d.Get("external_link").(string) engineString := d.Get("engine").(string) engineValue, ok := v1pb.Engine_value[engineString] @@ -221,21 +223,32 @@ func resourceInstanceCreate(ctx context.Context, d *schema.ResourceData, m inter } } - title := d.Get("title").(string) - externalLink := d.Get("external_link").(string) - if _, err := c.UpdateInstance(ctx, &v1pb.Instance{ - Name: instanceName, - Title: title, - ExternalLink: externalLink, - DataSources: dataSourceList, - State: existedInstance.State, - }, []string{"title", "external_link", "data_sources"}); err != nil { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Failed to update instance", - Detail: fmt.Sprintf("Update instance %s failed, error: %v", instanceName, err), - }) - return diags + updateMasks := []string{} + if title != "" && title != existedInstance.Title { + updateMasks = append(updateMasks, "title") + } + if externalLink != "" && externalLink != existedInstance.ExternalLink { + updateMasks = append(updateMasks, "external_link") + } + if len(dataSourceList) > 0 { + updateMasks = append(updateMasks, "data_sources") + } + + if len(updateMasks) > 0 { + if _, err := c.UpdateInstance(ctx, &v1pb.Instance{ + Name: instanceName, + Title: title, + ExternalLink: externalLink, + DataSources: dataSourceList, + State: v1pb.State_ACTIVE, + }, updateMasks); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update instance", + Detail: fmt.Sprintf("Update instance %s failed, error: %v", instanceName, err), + }) + return diags + } } } else { if _, err := c.CreateInstance(ctx, instanceID, &v1pb.Instance{ @@ -333,21 +346,23 @@ func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, m inter paths = append(paths, "data_sources") } - if _, err := c.UpdateInstance(ctx, &v1pb.Instance{ - Name: instanceName, - Title: d.Get("title").(string), - ExternalLink: d.Get("external_link").(string), - DataSources: dataSourceList, - State: existedInstance.State, - }, paths); err != nil { - return diag.FromErr(err) - } - if err := c.SyncInstanceSchema(ctx, instanceName); err != nil { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Warning, - Summary: "Instance schema sync failed", - Detail: fmt.Sprintf("Failed to sync schema for instance %s with error: %v. You can try to trigger the sync manually via Bytebase UI.", instanceName, err.Error()), - }) + if len(paths) > 0 { + if _, err := c.UpdateInstance(ctx, &v1pb.Instance{ + Name: instanceName, + Title: d.Get("title").(string), + ExternalLink: d.Get("external_link").(string), + DataSources: dataSourceList, + State: v1pb.State_ACTIVE, + }, paths); err != nil { + return diag.FromErr(err) + } + if err := c.SyncInstanceSchema(ctx, instanceName); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Instance schema sync failed", + Detail: fmt.Sprintf("Failed to sync schema for instance %s with error: %v. You can try to trigger the sync manually via Bytebase UI.", instanceName, err.Error()), + }) + } } diag := resourceInstanceRead(ctx, d, m) diff --git a/provider/resource_policy.go b/provider/resource_policy.go index 18ccffa..35b373e 100644 --- a/provider/resource_policy.go +++ b/provider/resource_policy.go @@ -209,13 +209,15 @@ func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interfa } var diags diag.Diagnostics - if _, err := c.UpsertPolicy(ctx, patch, updateMasks); err != nil { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Failed to upsert policy", - Detail: fmt.Sprintf("Upsert policy %s failed, error: %v", policyName, err), - }) - return diags + if len(updateMasks) > 0 { + if _, err := c.UpsertPolicy(ctx, patch, updateMasks); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to upsert policy", + Detail: fmt.Sprintf("Upsert policy %s failed, error: %v", policyName, err), + }) + return diags + } } diag := resourcePolicyRead(ctx, d, m) diff --git a/provider/resource_project.go b/provider/resource_project.go index 5fd849b..7f46dd8 100644 --- a/provider/resource_project.go +++ b/provider/resource_project.go @@ -141,38 +141,44 @@ func resourceProjectCreate(ctx context.Context, d *schema.ResourceData, m interf } } - project, err := c.UpdateProject(ctx, &v1pb.Project{ - Name: projectName, - Title: title, - Key: key, - State: existedProject.State, - Workflow: existedProject.Workflow, - }, []string{"title", "key"}) - if err != nil { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Failed to update project", - Detail: fmt.Sprintf("Update project %s failed, error: %v", projectName, err), - }) - return diags + updateMasks := []string{} + if title != "" && title != existedProject.Title { + updateMasks = append(updateMasks, "title") + } + if key != "" && key != existedProject.Key { + updateMasks = append(updateMasks, "key") } - d.SetId(project.Name) + if len(updateMasks) > 0 { + if _, err := c.UpdateProject(ctx, &v1pb.Project{ + Name: projectName, + Title: title, + Key: key, + State: v1pb.State_ACTIVE, + Workflow: existedProject.Workflow, + }, updateMasks); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update project", + Detail: fmt.Sprintf("Update project %s failed, error: %v", projectName, err), + }) + return diags + } + } } else { - project, err := c.CreateProject(ctx, projectID, &v1pb.Project{ + if _, err := c.CreateProject(ctx, projectID, &v1pb.Project{ Name: projectName, Title: title, Key: key, State: v1pb.State_ACTIVE, Workflow: v1pb.Workflow_UI, - }) - if err != nil { + }); err != nil { return diag.FromErr(err) } - - d.SetId(project.Name) } + d.SetId(projectName) + if diag := updateDatabasesInProject(ctx, d, c, d.Id()); diag != nil { diags = append(diags, diag...) return diags @@ -225,15 +231,17 @@ func resourceProjectUpdate(ctx context.Context, d *schema.ResourceData, m interf paths = append(paths, "key") } - if _, err := c.UpdateProject(ctx, &v1pb.Project{ - Name: projectName, - Title: d.Get("title").(string), - Key: d.Get("key").(string), - State: existedProject.State, - Workflow: existedProject.Workflow, - }, paths); err != nil { - diags = append(diags, diag.FromErr(err)...) - return diags + if len(paths) > 0 { + if _, err := c.UpdateProject(ctx, &v1pb.Project{ + Name: projectName, + Title: d.Get("title").(string), + Key: d.Get("key").(string), + State: v1pb.State_ACTIVE, + Workflow: existedProject.Workflow, + }, paths); err != nil { + diags = append(diags, diag.FromErr(err)...) + return diags + } } if d.HasChange("databases") { diff --git a/provider/resource_vcs_connector.go b/provider/resource_vcs_connector.go new file mode 100644 index 0000000..2702225 --- /dev/null +++ b/provider/resource_vcs_connector.go @@ -0,0 +1,298 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func resourceVCSConnector() *schema.Resource { + return &schema.Resource{ + Description: "The vcs connector resource.", + CreateContext: resourceVCSConnectorCreate, + ReadContext: resourceVCSConnectorRead, + UpdateContext: resourceVCSConnectorUpdate, + DeleteContext: resourceVCSConnectorDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: internal.ResourceIDValidation, + Description: "The vcs connector unique resource id.", + }, + "project": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: internal.ResourceNameValidation(regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern))), + Description: "The project name in projects/{resource id} format.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector full name in projects/{project}/vcsConnector/{resource id} format.", + }, + "title": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The vcs connector title.", + }, + "creator": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector creator in users/{email} format.", + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector create time in YYYY-MM-DDThh:mm:ss.000Z format", + }, + "updater": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector updater in users/{email} format.", + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs connector update time in YYYY-MM-DDThh:mm:ss.000Z format", + }, + "vcs_provider": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: internal.ResourceNameValidation(regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.VCSProviderNamePrefix, internal.ResourceIDPattern))), + Description: "The vcs provider full name in vcsProviders/{resource id} format.", + }, + "database_group": { + Type: schema.TypeString, + Optional: true, + Default: nil, + Description: "Apply changes to the database group.", + }, + "repository_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The connected repository id in vcs provider.", + }, + "repository_path": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The connected repository path in vcs provider.", + }, + "repository_directory": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The connected repository directory in vcs provider.", + }, + "repository_branch": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The connected repository branch in vcs provider.", + }, + "repository_url": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The connected repository url in vcs provider.", + }, + }, + } +} + +func resourceVCSConnectorRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + fullName := d.Id() + connector, err := c.GetVCSConnector(ctx, fullName) + if err != nil { + return diag.FromErr(err) + } + + return setVCSConnector(d, connector) +} + +func resourceVCSConnectorDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + fullName := d.Id() + + // Warning or errors can be collected in a slice type + var diags diag.Diagnostics + + if err := c.DeleteVCSConnector(ctx, fullName); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return diags +} + +func resourceVCSConnectorCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + connectorID := d.Get("resource_id").(string) + projectName := d.Get("project").(string) + connectorName := fmt.Sprintf("%s/%s%s", projectName, internal.VCSConnectorNamePrefix, connectorID) + + existedConnector, err := c.GetVCSConnector(ctx, connectorName) + if err != nil { + tflog.Debug(ctx, fmt.Sprintf("get vcs connector %s failed with error: %v", connectorName, err)) + } + + title := d.Get("title").(string) + vcsProviderName := d.Get("vcs_provider").(string) + databaseGroup := d.Get("database_group").(string) + repositoryID := d.Get("repository_id").(string) + repositoryPath := d.Get("repository_path").(string) + repositoryDirectory := d.Get("repository_directory").(string) + repositoryBranch := d.Get("repository_branch").(string) + repositoryURL := d.Get("repository_url").(string) + + var diags diag.Diagnostics + if existedConnector != nil && err == nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "VCS connector already exists", + Detail: fmt.Sprintf("VCS connector %s already exists, try to exec the update operation", connectorName), + }) + + updateMasks := []string{} + if repositoryBranch != "" && repositoryBranch != existedConnector.Branch { + updateMasks = append(updateMasks, "branch") + } + if repositoryDirectory != "" && repositoryDirectory != existedConnector.BaseDirectory { + updateMasks = append(updateMasks, "base_directory") + } + if databaseGroup != "" && databaseGroup != existedConnector.DatabaseGroup { + updateMasks = append(updateMasks, "database_group") + } + + if len(updateMasks) > 0 { + if _, err := c.UpdateVCSConnector(ctx, &v1pb.VCSConnector{ + Name: connectorName, + Title: title, + VcsProvider: existedConnector.VcsProvider, + DatabaseGroup: databaseGroup, + ExternalId: existedConnector.ExternalId, + FullPath: existedConnector.FullPath, + BaseDirectory: repositoryDirectory, + Branch: repositoryBranch, + WebUrl: existedConnector.WebUrl, + }, updateMasks); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update vcs connector", + Detail: fmt.Sprintf("Update vcs connector %s failed, error: %v", connectorName, err), + }) + return diags + } + } + } else { + if _, err := c.CreateVCSConnector(ctx, projectName, connectorID, &v1pb.VCSConnector{ + Name: connectorName, + Title: title, + VcsProvider: vcsProviderName, + DatabaseGroup: databaseGroup, + ExternalId: repositoryID, + FullPath: repositoryPath, + BaseDirectory: repositoryDirectory, + Branch: repositoryBranch, + WebUrl: repositoryURL, + }); err != nil { + return diag.FromErr(err) + } + } + + d.SetId(connectorName) + + diag := resourceVCSConnectorRead(ctx, d, m) + if diag != nil { + diags = append(diags, diag...) + } + + return diags +} + +func resourceVCSConnectorUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + if d.HasChange("resource_id") { + return diag.Errorf("cannot change the resource id") + } + if d.HasChange("project") { + return diag.Errorf("cannot change the project") + } + if d.HasChange("repository_id") { + return diag.Errorf("cannot change the repository_id") + } + if d.HasChange("repository_path") { + return diag.Errorf("cannot change the repository_path") + } + if d.HasChange("repository_url") { + return diag.Errorf("cannot change the repository_url") + } + + c := m.(api.Client) + connectorName := d.Id() + + existedConnector, err := c.GetVCSConnector(ctx, connectorName) + if err != nil { + tflog.Debug(ctx, fmt.Sprintf("get vcs connector %s failed with error: %v", connectorName, err)) + return diag.FromErr(err) + } + + paths := []string{} + if d.HasChange("database_group") { + paths = append(paths, "database_group") + } + if d.HasChange("repository_directory") { + paths = append(paths, "base_directory") + } + if d.HasChange("repository_branch") { + paths = append(paths, "branch") + } + + var diags diag.Diagnostics + if len(paths) > 0 { + if _, err := c.UpdateVCSConnector(ctx, &v1pb.VCSConnector{ + Name: connectorName, + Title: existedConnector.Title, + VcsProvider: existedConnector.VcsProvider, + DatabaseGroup: d.Get("database_group").(string), + ExternalId: existedConnector.ExternalId, + FullPath: existedConnector.FullPath, + BaseDirectory: d.Get("repository_directory").(string), + Branch: d.Get("repository_branch").(string), + WebUrl: existedConnector.WebUrl, + }, paths); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update vcs connector", + Detail: fmt.Sprintf("Update vcs connector %s failed, error: %v", connectorName, err), + }) + return diags + } + } + + diag := resourceVCSConnectorRead(ctx, d, m) + if diag != nil { + diags = append(diags, diag...) + } + + return diags +} diff --git a/provider/resource_vcs_provider.go b/provider/resource_vcs_provider.go new file mode 100644 index 0000000..fd88f1b --- /dev/null +++ b/provider/resource_vcs_provider.go @@ -0,0 +1,237 @@ +package provider + +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" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func resourceVCSProvider() *schema.Resource { + return &schema.Resource{ + Description: "The vcs provider resource.", + CreateContext: resourceVCSProviderCreate, + ReadContext: resourceVCSProviderRead, + UpdateContext: resourceVCSProviderUpdate, + DeleteContext: resourceVCSProviderDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: internal.ResourceIDValidation, + Description: "The vcs provider unique resource id.", + }, + "title": { + Type: schema.TypeString, + Required: true, + Description: "The vcs provider title.", + ValidateFunc: validation.StringIsNotEmpty, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The vcs provider full name in vcsProviders/{resource id} format.", + }, + "url": { + Type: schema.TypeString, + Optional: true, + Description: "The vcs provider url. You need to provide the url if you're using the self-host GitLab or self-host GitHub.", + }, + "type": { + Type: schema.TypeString, + Required: true, + Description: "The vcs provider type.", + ValidateFunc: validation.StringInSlice([]string{ + v1pb.VCSType_GITHUB.String(), + v1pb.VCSType_GITLAB.String(), + v1pb.VCSType_BITBUCKET.String(), + v1pb.VCSType_AZURE_DEVOPS.String(), + }, false), + }, + "access_token": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The vcs provider token. Check the docs https://bytebase.cc/docs/vcs-integration/add-git-provider for details.", + }, + }, + } +} + +func resourceVCSProviderRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + fullName := d.Id() + provider, err := c.GetVCSProvider(ctx, fullName) + if err != nil { + return diag.FromErr(err) + } + + return setVCSProvider(d, provider) +} + +func resourceVCSProviderDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + fullName := d.Id() + + // Warning or errors can be collected in a slice type + var diags diag.Diagnostics + + if err := c.DeleteVCSProvider(ctx, fullName); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return diags +} + +func resourceVCSProviderCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + providerID := d.Get("resource_id").(string) + providerName := fmt.Sprintf("%s%s", internal.VCSProviderNamePrefix, providerID) + + existedProvider, err := c.GetVCSProvider(ctx, providerName) + if err != nil { + tflog.Debug(ctx, fmt.Sprintf("get vcs provider %s failed with error: %v", providerName, err)) + } + + title := d.Get("title").(string) + url := d.Get("url").(string) + vcsType := v1pb.VCSType(v1pb.VCSType_value[d.Get("type").(string)]) + token := d.Get("access_token").(string) + + var diags diag.Diagnostics + if existedProvider != nil && err == nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "VCS provider already exists", + Detail: fmt.Sprintf("VCS provider %s already exists, try to exec the update operation", providerName), + }) + + updateMasks := []string{} + if token != "" { + updateMasks = append(updateMasks, "access_token") + } + if title != "" && title != existedProvider.Title { + updateMasks = append(updateMasks, "title") + } + + if len(updateMasks) > 0 { + if _, err := c.UpdateVCSProvider(ctx, &v1pb.VCSProvider{ + Name: providerName, + Title: title, + Url: existedProvider.Url, + Type: existedProvider.Type, + AccessToken: token, + }, updateMasks); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update vcs provider", + Detail: fmt.Sprintf("Update vcs provider %s failed, error: %v", providerName, err), + }) + return diags + } + } + } else { + switch vcsType { + case v1pb.VCSType_GITHUB: + if url == "" { + url = "https://github.com" + } + case v1pb.VCSType_AZURE_DEVOPS: + url = "https://dev.azure.com" + case v1pb.VCSType_GITLAB: + if url == "" { + url = "https://gitlab.com" + } + case v1pb.VCSType_BITBUCKET: + url = "https://bitbucket.org" + } + if url == "" { + return diag.Errorf("missing url") + } + if _, err := c.CreateVCSProvider(ctx, providerID, &v1pb.VCSProvider{ + Name: providerName, + Title: title, + Url: url, + Type: vcsType, + AccessToken: token, + }); err != nil { + return diag.FromErr(err) + } + } + + d.SetId(providerName) + + diag := resourceVCSProviderRead(ctx, d, m) + if diag != nil { + diags = append(diags, diag...) + } + + return diags +} + +func resourceVCSProviderUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + if d.HasChange("resource_id") { + return diag.Errorf("cannot change the resource id") + } + + c := m.(api.Client) + providerName := d.Id() + + existedProvider, err := c.GetVCSProvider(ctx, providerName) + if err != nil { + tflog.Debug(ctx, fmt.Sprintf("get vcs provider %s failed with error: %v", providerName, err)) + return diag.FromErr(err) + } + + paths := []string{} + if d.HasChange("type") { + return diag.Errorf("cannot change the vcs provider type") + } + if d.HasChange("title") { + paths = append(paths, "title") + } + if d.HasChange("access_token") { + paths = append(paths, "access_token") + } + + var diags diag.Diagnostics + if len(paths) > 0 { + if _, err := c.UpdateVCSProvider(ctx, &v1pb.VCSProvider{ + Name: providerName, + Title: d.Get("title").(string), + Url: existedProvider.Url, + Type: existedProvider.Type, + AccessToken: d.Get("access_token").(string), + }, paths); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update vcs provider", + Detail: fmt.Sprintf("Update vcs provider %s failed, error: %v", providerName, err), + }) + return diags + } + } + + diag := resourceVCSProviderRead(ctx, d, m) + if diag != nil { + diags = append(diags, diag...) + } + + return diags +}