diff --git a/api/client.go b/api/client.go index 63bebcc..edfe03c 100644 --- a/api/client.go +++ b/api/client.go @@ -179,4 +179,16 @@ type Client interface { UpsertReviewConfig(ctx context.Context, patch *v1pb.ReviewConfig, updateMasks []string) (*v1pb.ReviewConfig, error) // DeleteReviewConfig deletes the review config. DeleteReviewConfig(ctx context.Context, reviewName string) error + + // Risk + // ListRisk lists the risk. + ListRisk(ctx context.Context) ([]*v1pb.Risk, error) + // GetRisk gets the risk by full name. + GetRisk(ctx context.Context, name string) (*v1pb.Risk, error) + // CreateRisk creates the risk. + 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 by name. + DeleteRisk(ctx context.Context, name string) error } diff --git a/client/risk.go b/client/risk.go new file mode 100644 index 0000000..bf04b41 --- /dev/null +++ b/client/risk.go @@ -0,0 +1,92 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strings" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + "google.golang.org/protobuf/encoding/protojson" +) + +// ListRisk lists the risk. +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 + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var res v1pb.ListRisksResponse + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return res.Risks, nil +} + +// GetRisk gets the risk by full name. +func (c *client) GetRisk(ctx context.Context, name string) (*v1pb.Risk, error) { + body, err := c.getResource(ctx, name) + if err != nil { + return nil, err + } + + var res v1pb.Risk + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// CreateRisk creates the risk. +func (c *client) CreateRisk(ctx context.Context, risk *v1pb.Risk) (*v1pb.Risk, error) { + payload, err := protojson.Marshal(risk) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/%s/risks", 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.Risk + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// 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*/) + if err != nil { + return nil, err + } + + var res v1pb.Risk + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// DeleteRisk deletes the risk by name. +func (c *client) DeleteRisk(ctx context.Context, name string) error { + return c.deleteResource(ctx, name) +} diff --git a/client/role.go b/client/role.go index e99ea39..502eb8c 100644 --- a/client/role.go +++ b/client/role.go @@ -58,7 +58,7 @@ func (c *client) DeleteRole(ctx context.Context, name string) error { // UpdateRole updates the role. func (c *client) UpdateRole(ctx context.Context, patch *v1pb.Role, updateMasks []string) (*v1pb.Role, error) { - body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, false /* allow missing = false*/) + body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, true /* allow missing = true*/) if err != nil { return nil, err } diff --git a/docs/data-sources/environment.md b/docs/data-sources/environment.md new file mode 100644 index 0000000..8ad672e --- /dev/null +++ b/docs/data-sources/environment.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_environment Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The environment data source. +--- + +# bytebase_environment (Data Source) + +The environment data source. + + + + +## Schema + +### Required + +- `resource_id` (String) The environment unique resource id. + +### Optional + +- `color` (String) The environment color. +- `protected` (Boolean) The environment is protected or not. + +### Read-Only + +- `id` (String) The ID of this resource. +- `name` (String) The environment full name in environments/{resource id} format. +- `order` (Number) The environment sorting order. +- `title` (String) The environment unique name. + + diff --git a/docs/data-sources/risk.md b/docs/data-sources/risk.md new file mode 100644 index 0000000..7e50ad6 --- /dev/null +++ b/docs/data-sources/risk.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_risk Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The risk data source. +--- + +# bytebase_risk (Data Source) + +The risk data source. + + + + +## Schema + +### Required + +- `name` (String) The risk full name in risks/{uid} format. + +### Read-Only + +- `active` (Boolean) The risk active. +- `condition` (String) The risk condition. +- `id` (String) The ID of this resource. +- `level` (Number) The risk level. +- `source` (String) The risk source. +- `title` (String) The risk title. + + diff --git a/docs/data-sources/risk_list.md b/docs/data-sources/risk_list.md new file mode 100644 index 0000000..7695e10 --- /dev/null +++ b/docs/data-sources/risk_list.md @@ -0,0 +1,35 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_risk_list Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The risk data source list. +--- + +# bytebase_risk_list (Data Source) + +The risk data source list. + + + + +## Schema + +### Read-Only + +- `id` (String) The ID of this resource. +- `risks` (List of Object) (see [below for nested schema](#nestedatt--risks)) + + +### Nested Schema for `risks` + +Read-Only: + +- `active` (Boolean) +- `condition` (String) +- `level` (Number) +- `name` (String) +- `source` (String) +- `title` (String) + + diff --git a/docs/resources/environment.md b/docs/resources/environment.md new file mode 100644 index 0000000..1350776 --- /dev/null +++ b/docs/resources/environment.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_environment Resource - terraform-provider-bytebase" +subcategory: "" +description: |- + The environment resource. +--- + +# bytebase_environment (Resource) + +The environment resource. + + + + +## Schema + +### Required + +- `order` (Number) The environment sorting order. +- `resource_id` (String) The environment unique id. +- `title` (String) The environment display name. + +### Optional + +- `color` (String) The environment color. +- `protected` (Boolean) The environment is protected or not. + +### Read-Only + +- `id` (String) The ID of this resource. +- `name` (String) The environment readonly name in environments/{id} format. + + diff --git a/docs/resources/risk.md b/docs/resources/risk.md new file mode 100644 index 0000000..0cffc5f --- /dev/null +++ b/docs/resources/risk.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_risk Resource - terraform-provider-bytebase" +subcategory: "" +description: |- + The risk resource. Require ENTERPRISE subscription. Check the docs https://www.bytebase.com/docs/administration/risk-center?source=terraform for more information. +--- + +# bytebase_risk (Resource) + +The risk resource. Require ENTERPRISE subscription. Check the docs https://www.bytebase.com/docs/administration/risk-center?source=terraform for more information. + + + + +## Schema + +### Required + +- `condition` (String) The risk condition. +- `level` (Number) The risk level. +- `source` (String) The risk source. +- `title` (String) The risk title. + +### Optional + +- `active` (Boolean) If the risk is active. + +### Read-Only + +- `id` (String) The ID of this resource. +- `name` (String) The risk full name in risks/{uid} format. + + diff --git a/examples/risk/main.tf b/examples/risk/main.tf new file mode 100644 index 0000000..c3bf75f --- /dev/null +++ b/examples/risk/main.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + bytebase = { + version = "3.6.0" + # For local development, please use "terraform.local/bytebase/bytebase" instead + source = "registry.terraform.io/bytebase/bytebase" + } + } +} + +provider "bytebase" { + # You need to replace the account and key with your Bytebase service account. + service_account = "terraform@service.bytebase.com" + service_key = "bbs_BxVIp7uQsARl8nR92ZZV" + # The Bytebase service URL. You can use the external URL in production. + # Check the docs about external URL: https://www.bytebase.com/docs/get-started/install/external-url + url = "https://bytebase.example.com" +} + +data "bytebase_risk_list" "all" { +} + +output "all_risks" { + value = data.bytebase_risk_list.all +} diff --git a/examples/setup/risk.tf b/examples/setup/risk.tf new file mode 100644 index 0000000..2d63a3e --- /dev/null +++ b/examples/setup/risk.tf @@ -0,0 +1,7 @@ +resource "bytebase_risk" "risk" { + title = "Risk for prod environment" + source = "DML" + level = 300 + active = true + condition = "environment_id == \"prod\" && affected_rows >= 100" +} diff --git a/go.mod b/go.mod index e471b0c..1f2d91d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/bytebase/terraform-provider-bytebase go 1.24.2 require ( - github.com/bytebase/bytebase v0.0.0-20250424073126-d57cbba37d61 + github.com/bytebase/bytebase v0.0.0-20250513033606-5479107aeeb3 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-docs v0.13.0 github.com/hashicorp/terraform-plugin-log v0.7.0 diff --git a/go.sum b/go.sum index 118c4c1..8126d3e 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bytebase/bytebase v0.0.0-20250424073126-d57cbba37d61 h1:+ptgPqM2aSzlPjeauecETOwbuGcoSKd5wUsNLLtMuCQ= -github.com/bytebase/bytebase v0.0.0-20250424073126-d57cbba37d61/go.mod h1:Gu5A9lSsc8OMJ5nUbKAxOn5X8gDj1Rxuzy0NwxVt90k= +github.com/bytebase/bytebase v0.0.0-20250513033606-5479107aeeb3 h1:D60aM1+dFti+VTqhmjt6zDXSwStc99OUAUowmfFY5uI= +github.com/bytebase/bytebase v0.0.0-20250513033606-5479107aeeb3/go.mod h1:HHRGRkJYb2FKv2Iyl1feFzaadoXc14t2iMk4z+Xc/Kg= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/provider/data_source_risk.go b/provider/data_source_risk.go new file mode 100644 index 0000000..2d3b4f7 --- /dev/null +++ b/provider/data_source_risk.go @@ -0,0 +1,63 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/bytebase/terraform-provider-bytebase/api" +) + +func dataSourceRisk() *schema.Resource { + return &schema.Resource{ + Description: "The risk data source.", + ReadContext: dataSourceRiskRead, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The risk full name in risks/{uid} format.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The risk title.", + }, + "source": { + Type: schema.TypeString, + Computed: true, + Description: "The risk source.", + }, + "level": { + Type: schema.TypeInt, + Computed: true, + Description: "The risk level.", + }, + "active": { + Type: schema.TypeBool, + Computed: true, + Description: "The risk active.", + }, + "condition": { + Type: schema.TypeString, + Computed: true, + Description: "The risk condition.", + }, + }, + } +} + +func dataSourceRiskRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + riskName := d.Get("name").(string) + + risk, err := c.GetRisk(ctx, riskName) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(risk.Name) + + return setRisk(d, risk) +} diff --git a/provider/data_source_risk_list.go b/provider/data_source_risk_list.go new file mode 100644 index 0000000..3089952 --- /dev/null +++ b/provider/data_source_risk_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" +) + +func dataSourceRiskList() *schema.Resource { + return &schema.Resource{ + Description: "The risk data source list.", + ReadContext: dataSourceRiskListRead, + Schema: map[string]*schema.Schema{ + "risks": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The risk full name in risks/{resource id} format.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The risk title.", + }, + "source": { + Type: schema.TypeString, + Computed: true, + Description: "The risk source.", + }, + "level": { + Type: schema.TypeInt, + Computed: true, + Description: "The risk level.", + }, + "active": { + Type: schema.TypeBool, + Computed: true, + Description: "The risk active.", + }, + "condition": { + Type: schema.TypeString, + Computed: true, + Description: "The risk condition.", + }, + }, + }, + }, + }, + } +} + +func dataSourceRiskListRead(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 + + risks, err := c.ListRisk(ctx) + if err != nil { + return diag.FromErr(err) + } + + dataList := []map[string]interface{}{} + for _, risk := range risks { + raw := make(map[string]interface{}) + raw["name"] = risk.Name + raw["title"] = risk.Title + raw["source"] = risk.Source.String() + raw["level"] = int(risk.Level) + raw["active"] = risk.Active + raw["condition"] = risk.Condition.Expression + + dataList = append(dataList, raw) + } + + if err := d.Set("risks", dataList); 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 4dc84e1..94bf988 100644 --- a/provider/internal/mock_client.go +++ b/provider/internal/mock_client.go @@ -702,3 +702,28 @@ func (*mockClient) UpsertReviewConfig(_ context.Context, _ *v1pb.ReviewConfig, _ func (*mockClient) DeleteReviewConfig(_ context.Context, _ string) error { return nil } + +// ListRisk lists the risk. +func (*mockClient) ListRisk(_ context.Context) ([]*v1pb.Risk, error) { + return []*v1pb.Risk{}, nil +} + +// GetRisk gets the risk by full name. +func (*mockClient) GetRisk(_ context.Context, _ string) (*v1pb.Risk, error) { + return &v1pb.Risk{}, nil +} + +// CreateRisk creates the risk. +func (*mockClient) CreateRisk(_ context.Context, _ *v1pb.Risk) (*v1pb.Risk, error) { + return &v1pb.Risk{}, nil +} + +// UpdateRisk updates the risk. +func (*mockClient) UpdateRisk(_ context.Context, _ *v1pb.Risk, _ []string) (*v1pb.Risk, error) { + return &v1pb.Risk{}, nil +} + +// DeleteRisk deletes the risk by name. +func (*mockClient) DeleteRisk(_ context.Context, _ string) error { + return nil +} diff --git a/provider/provider.go b/provider/provider.go index 142286f..bb556e1 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -68,6 +68,8 @@ func NewProvider() *schema.Provider { "bytebase_review_config_list": dataSourceReviewConfigList(), "bytebase_iam_policy": dataSourceIAMPolicy(), "bytebase_environment": dataSourceEnvironment(), + "bytebase_risk": dataSourceRisk(), + "bytebase_risk_list": dataSourceRiskList(), }, ResourcesMap: map[string]*schema.Resource{ "bytebase_instance": resourceInstance(), @@ -81,6 +83,7 @@ func NewProvider() *schema.Provider { "bytebase_review_config": resourceReviewConfig(), "bytebase_iam_policy": resourceIAMPolicy(), "bytebase_environment": resourceEnvironment(), + "bytebase_risk": resourceRisk(), }, } } diff --git a/provider/resource_risk.go b/provider/resource_risk.go new file mode 100644 index 0000000..fd9230a --- /dev/null +++ b/provider/resource_risk.go @@ -0,0 +1,205 @@ +package provider + +import ( + "context" + "fmt" + + "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/proto/generated-go/v1" + + "github.com/bytebase/terraform-provider-bytebase/api" +) + +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: resourceRiskRead, + DeleteContext: resourceRiskDelete, + CreateContext: resourceRiskCreate, + UpdateContext: resourceRiskUpdate, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "title": { + Type: schema.TypeString, + Required: true, + Description: "The risk title.", + ValidateFunc: validation.StringIsNotEmpty, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The risk full name in risks/{uid} format.", + }, + "source": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + v1pb.Risk_DDL.String(), + v1pb.Risk_DML.String(), + v1pb.Risk_CREATE_DATABASE.String(), + v1pb.Risk_REQUEST_QUERY.String(), + v1pb.Risk_REQUEST_EXPORT.String(), + v1pb.Risk_DATA_EXPORT.String(), + }, false), + Description: "The risk source.", + }, + "level": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntInSlice([]int{ + 300, 200, 100, + }), + Description: "The risk level.", + }, + "active": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "If the risk is active.", + }, + "condition": { + Type: schema.TypeString, + Required: true, + Description: "The risk condition.", + }, + }, + } +} + +func resourceRiskRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + fullName := d.Id() + risk, err := c.GetRisk(ctx, fullName) + if err != nil { + return diag.FromErr(err) + } + + return setRisk(d, risk) +} + +func setRisk(d *schema.ResourceData, risk *v1pb.Risk) diag.Diagnostics { + if err := d.Set("title", risk.Title); err != nil { + return diag.Errorf("cannot set title for risk: %s", err.Error()) + } + if err := d.Set("name", risk.Name); err != nil { + return diag.Errorf("cannot set name for risk: %s", err.Error()) + } + if err := d.Set("source", risk.Source.String()); err != nil { + return diag.Errorf("cannot set source for risk: %s", err.Error()) + } + if err := d.Set("level", int(risk.Level)); err != nil { + return diag.Errorf("cannot set level for risk: %s", err.Error()) + } + if err := d.Set("active", risk.Active); err != nil { + return diag.Errorf("cannot set active for risk: %s", err.Error()) + } + if err := d.Set("condition", risk.Condition.Expression); err != nil { + return diag.Errorf("cannot set condition for risk: %s", err.Error()) + } + + return nil +} + +func resourceRiskDelete(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.DeleteRisk(ctx, fullName); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return diags +} + +func resourceRiskCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + title := d.Get("title").(string) + active := d.Get("active").(bool) + level := int32(d.Get("level").(int)) + source := v1pb.Risk_Source(v1pb.Risk_Source_value[d.Get("source").(string)]) + + risk := &v1pb.Risk{ + Title: title, + Active: active, + Level: level, + Source: source, + Condition: &expr.Expr{ + Expression: d.Get("condition").(string), + }, + } + + created, err := c.CreateRisk(ctx, risk) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(created.Name) + + return resourceRiskRead(ctx, d, m) +} + +func resourceRiskUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + riskName := d.Id() + + existedRisk, err := c.GetRisk(ctx, riskName) + if err != nil { + return diag.Errorf("get risk %s failed with error: %v", riskName, err) + } + + updateMasks := []string{} + if d.HasChange("title") { + updateMasks = append(updateMasks, "title") + existedRisk.Title = d.Get("title").(string) + } + if d.HasChange("active") { + updateMasks = append(updateMasks, "active") + existedRisk.Active = d.Get("active").(bool) + } + if d.HasChange("level") { + updateMasks = append(updateMasks, "level") + existedRisk.Level = int32(d.Get("level").(int)) + } + if d.HasChange("source") { + updateMasks = append(updateMasks, "source") + existedRisk.Source = v1pb.Risk_Source(v1pb.Risk_Source_value[d.Get("source").(string)]) + } + if d.HasChange("condition") { + updateMasks = append(updateMasks, "condition") + existedRisk.Condition = &expr.Expr{ + Expression: d.Get("condition").(string), + } + } + + var diags diag.Diagnostics + if len(updateMasks) > 0 { + if _, err := c.UpdateRisk(ctx, existedRisk, updateMasks); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to update risk", + Detail: fmt.Sprintf("Update risk %s failed, error: %v", riskName, err), + }) + return diags + } + } + + diag := resourceRiskRead(ctx, d, m) + if diag != nil { + diags = append(diags, diag...) + } + + return diags +} diff --git a/provider/resource_role.go b/provider/resource_role.go index d039fd9..aaf3616 100644 --- a/provider/resource_role.go +++ b/provider/resource_role.go @@ -205,24 +205,36 @@ func resourceRoleUpdate(ctx context.Context, d *schema.ResourceData, m interface c := m.(api.Client) roleName := d.Id() + permissions, diagnostic := getRolePermissions(d) + if diagnostic != nil { + return diagnostic + } + existedRole, err := c.GetRole(ctx, roleName) if err != nil { tflog.Debug(ctx, fmt.Sprintf("get role %s failed with error: %v", roleName, err)) } + if existedRole == nil { + // Allow missing. + existedRole = &v1pb.Role{ + Name: roleName, + Title: d.Get("title").(string), + Description: d.Get("description").(string), + Permissions: permissions, + } + } updateMasks := []string{} if d.HasChange("title") { updateMasks = append(updateMasks, "title") + existedRole.Title = d.Get("title").(string) } if d.HasChange("description") { updateMasks = append(updateMasks, "description") + existedRole.Description = d.Get("description").(string) } if d.HasChange("permissions") { updateMasks = append(updateMasks, "permissions") - permissions, diagnostic := getRolePermissions(d) - if err != nil { - return diagnostic - } existedRole.Permissions = permissions }