diff --git a/VERSION b/VERSION index 321816a..084e244 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.24 \ No newline at end of file +3.6.0 \ No newline at end of file diff --git a/examples/database/main.tf b/examples/database/main.tf index 014bbb0..b824302 100644 --- a/examples/database/main.tf +++ b/examples/database/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/environments/main.tf b/examples/environments/main.tf index b116aad..f768dee 100644 --- a/examples/environments/main.tf +++ b/examples/environments/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } @@ -25,3 +25,11 @@ data "bytebase_setting" "environments" { output "all_environments" { value = data.bytebase_setting.environments } + +data "bytebase_environment" "prod" { + resource_id = "prod" +} + +output "prod_environment" { + value = data.bytebase_environment.prod +} diff --git a/examples/groups/main.tf b/examples/groups/main.tf index 606ec19..91c58e2 100644 --- a/examples/groups/main.tf +++ b/examples/groups/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/iamPolicy/main.tf b/examples/iamPolicy/main.tf index 9b5b8bf..ad52464 100644 --- a/examples/iamPolicy/main.tf +++ b/examples/iamPolicy/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/instances/main.tf b/examples/instances/main.tf index a2bdeec..251529f 100644 --- a/examples/instances/main.tf +++ b/examples/instances/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/policies/main.tf b/examples/policies/main.tf index d2767cf..83c40f0 100644 --- a/examples/policies/main.tf +++ b/examples/policies/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/projects/main.tf b/examples/projects/main.tf index 33d8fb8..2bc37e7 100644 --- a/examples/projects/main.tf +++ b/examples/projects/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/roles/main.tf b/examples/roles/main.tf index 42e3464..0e49ab1 100644 --- a/examples/roles/main.tf +++ b/examples/roles/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/settings/main.tf b/examples/settings/main.tf index f79e136..a6c50ce 100644 --- a/examples/settings/main.tf +++ b/examples/settings/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/setup/environment.tf b/examples/setup/environment.tf index c175259..d1ffe98 100644 --- a/examples/setup/environment.tf +++ b/examples/setup/environment.tf @@ -1,16 +1,38 @@ resource "bytebase_setting" "environments" { name = "bb.workspace.environment" environment_setting { - environment { - id = local.environment_id_test - title = "Test" - protected = false - } - environment { id = local.environment_id_prod title = "Prod" protected = true } + + environment { + id = local.environment_id_test + title = "Test" + protected = false + } } } + +# Upsert test environment. +resource "bytebase_environment" "test" { + depends_on = [ + bytebase_setting.environments + ] + resource_id = local.environment_id_test + title = "Staging" // rename to "Staging" + order = 0 // change order to 0 + protected = false +} + +# Upsert prod environment. +resource "bytebase_environment" "prod" { + depends_on = [ + bytebase_environment.test + ] + resource_id = local.environment_id_prod + title = "Prod" + order = 1 // change order to 1 + protected = true +} diff --git a/examples/setup/main.tf b/examples/setup/main.tf index 0924ede..ec36824 100644 --- a/examples/setup/main.tf +++ b/examples/setup/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.23" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/sql_review/main.tf b/examples/sql_review/main.tf index 96e8edf..74f5cec 100644 --- a/examples/sql_review/main.tf +++ b/examples/sql_review/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/users/main.tf b/examples/users/main.tf index 15acb14..6e31806 100644 --- a/examples/users/main.tf +++ b/examples/users/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.24" + version = "3.6.0" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/provider/data_source_environment.go b/provider/data_source_environment.go new file mode 100644 index 0000000..3fc0725 --- /dev/null +++ b/provider/data_source_environment.go @@ -0,0 +1,66 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func dataSourceEnvironment() *schema.Resource { + return &schema.Resource{ + Description: "The environment data source.", + ReadContext: dataSourceEnvironmentRead, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: internal.ResourceIDValidation, + Description: "The environment unique resource id.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The environment full name in environments/{resource id} format.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The environment unique name.", + }, + "order": { + Type: schema.TypeInt, + Computed: true, + Description: "The environment sorting order.", + }, + "color": { + Type: schema.TypeString, + Optional: true, + Description: "The environment color.", + }, + "protected": { + Type: schema.TypeBool, + Optional: true, + Description: "The environment is protected or not.", + }, + }, + } +} + +func dataSourceEnvironmentRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + environmentName := fmt.Sprintf("%s%s", internal.EnvironmentNamePrefix, d.Get("resource_id").(string)) + + env, order, _, err := findEnvironment(ctx, c, environmentName) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(env.Name) + + return setEnvironment(d, env, order) +} diff --git a/provider/data_source_setting.go b/provider/data_source_setting.go index 2c409be..0d7103e 100644 --- a/provider/data_source_setting.go +++ b/provider/data_source_setting.go @@ -353,9 +353,10 @@ func getEnvironmentSetting(computed bool) *schema.Schema { Description: "The environment readonly name in environments/{id} format.", }, "title": { - Type: schema.TypeString, - Required: true, - Description: "The environment display name.", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The environment display name.", }, "color": { Type: schema.TypeString, diff --git a/provider/provider.go b/provider/provider.go index 04bf561..142286f 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -67,6 +67,7 @@ func NewProvider() *schema.Provider { "bytebase_review_config": dataSourceReviewConfig(), "bytebase_review_config_list": dataSourceReviewConfigList(), "bytebase_iam_policy": dataSourceIAMPolicy(), + "bytebase_environment": dataSourceEnvironment(), }, ResourcesMap: map[string]*schema.Resource{ "bytebase_instance": resourceInstance(), @@ -79,6 +80,7 @@ func NewProvider() *schema.Provider { "bytebase_database": resourceDatabase(), "bytebase_review_config": resourceReviewConfig(), "bytebase_iam_policy": resourceIAMPolicy(), + "bytebase_environment": resourceEnvironment(), }, } } diff --git a/provider/resource_environment.go b/provider/resource_environment.go new file mode 100644 index 0000000..00cac59 --- /dev/null +++ b/provider/resource_environment.go @@ -0,0 +1,227 @@ +package provider + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/pkg/errors" + + 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 resourceEnvironment() *schema.Resource { + return &schema.Resource{ + Description: "The environment resource.", + CreateContext: resourceEnvironmentUpsert, + ReadContext: resourceEnvironmentRead, + UpdateContext: resourceEnvironmentUpsert, + DeleteContext: resourceEnvironmentDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: internal.ResourceIDValidation, + Description: "The environment unique id.", + }, + "title": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The environment display name.", + }, + "order": { + Type: schema.TypeInt, + Required: true, + Description: "The environment sorting order.", + ValidateFunc: validation.IntAtLeast(0), + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The environment readonly name in environments/{id} format.", + }, + "color": { + Type: schema.TypeString, + Optional: true, + Description: "The environment color.", + }, + "protected": { + Type: schema.TypeBool, + Optional: true, + Description: "The environment is protected or not.", + }, + }, + } +} + +func resourceEnvironmentUpsert(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + newOrder := d.Get("order").(int) + environmentID := d.Get("resource_id").(string) + environmentName := fmt.Sprintf("%s%s", internal.EnvironmentNamePrefix, environmentID) + v1Env := &v1pb.EnvironmentSetting_Environment{ + Id: environmentID, + Title: d.Get("title").(string), + Color: d.Get("color").(string), + Tags: map[string]string{ + "protected": "protected", + }, + } + if !d.Get("protected").(bool) { + v1Env.Tags["protected"] = "unprotected" + } + + existedEnv, order, enironmentList, err := findEnvironment(ctx, c, environmentName) + if err != nil { + if !strings.HasPrefix(err.Error(), "cannot found the environment") { + return diag.FromErr(err) + } + } + if enironmentList == nil { + return diag.Errorf("cannot found environment setting") + } + + if newOrder >= len(enironmentList) { + return diag.Errorf("the new order %v out of range %v", newOrder, len(enironmentList)-1) + } + + var diags diag.Diagnostics + if existedEnv != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Environment already exists", + Detail: fmt.Sprintf("Environment %s already exists, try to exec the update operation", environmentID), + }) + + if order == newOrder { + enironmentList[order] = v1Env + } else { + enironmentList = slices.Delete(enironmentList, order, order+1) + enironmentList = slices.Insert(enironmentList, newOrder, v1Env) + } + } else { + enironmentList = slices.Insert(enironmentList, newOrder, v1Env) + } + + if err := updateEnvironmentSetting(ctx, c, enironmentList); err != nil { + return diag.FromErr(err) + } + + d.SetId(environmentName) + diag := resourceEnvironmentRead(ctx, d, m) + if diag != nil { + diags = append(diags, diag...) + } + + return diags +} + +func resourceEnvironmentRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + environmentName := d.Id() + + env, order, _, err := findEnvironment(ctx, c, environmentName) + if err != nil { + return diag.FromErr(err) + } + + return setEnvironment(d, env, order) +} + +func resourceEnvironmentDelete(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 + environmentName := d.Id() + + _, order, enironmentList, err := findEnvironment(ctx, c, environmentName) + if err != nil { + return diag.FromErr(err) + } + + enironmentList = slices.Delete(enironmentList, order, order+1) + if err := updateEnvironmentSetting(ctx, c, enironmentList); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return diags +} + +func updateEnvironmentSetting(ctx context.Context, client api.Client, list []*v1pb.EnvironmentSetting_Environment) error { + _, err := client.UpsertSetting(ctx, &v1pb.Setting{ + Name: fmt.Sprintf("%s%s", internal.SettingNamePrefix, string(api.SettingEnvironment)), + Value: &v1pb.Value{ + Value: &v1pb.Value_EnvironmentSetting{ + EnvironmentSetting: &v1pb.EnvironmentSetting{ + Environments: list, + }, + }, + }, + }, []string{}) + return err +} + +func getEnvironmentList(ctx context.Context, client api.Client) ([]*v1pb.EnvironmentSetting_Environment, error) { + environmentSetting, err := client.GetSetting(ctx, fmt.Sprintf("%s%s", internal.SettingNamePrefix, string(api.SettingEnvironment))) + if err != nil { + return nil, errors.Wrapf(err, "failed to get environment setting") + } + return environmentSetting.GetValue().GetEnvironmentSetting().Environments, nil +} + +func findEnvironment(ctx context.Context, client api.Client, name string) (*v1pb.EnvironmentSetting_Environment, int, []*v1pb.EnvironmentSetting_Environment, error) { + enironmentList, err := getEnvironmentList(ctx, client) + if err != nil { + return nil, 0, nil, err + } + + for index, env := range enironmentList { + if env.Name == name { + return env, index, enironmentList, nil + } + } + return nil, 0, enironmentList, errors.Errorf("cannot found the environment %v", name) +} + +func setEnvironment(d *schema.ResourceData, env *v1pb.EnvironmentSetting_Environment, order int) diag.Diagnostics { + environmentID, err := internal.GetEnvironmentID(env.Name) + if err != nil { + return diag.FromErr(err) + } + + if err := d.Set("resource_id", environmentID); err != nil { + return diag.Errorf("cannot set resource_id for environment: %s", err.Error()) + } + if err := d.Set("title", env.Title); err != nil { + return diag.Errorf("cannot set title for environment: %s", err.Error()) + } + if err := d.Set("name", env.Name); err != nil { + return diag.Errorf("cannot set name for environment: %s", err.Error()) + } + if err := d.Set("order", order); err != nil { + return diag.Errorf("cannot set order for environment: %s", err.Error()) + } + if err := d.Set("color", env.Color); err != nil { + return diag.Errorf("cannot set color for environment: %s", err.Error()) + } + if err := d.Set("protected", env.Tags["protected"] == "protected"); err != nil { + return diag.Errorf("cannot set protected for environment: %s", err.Error()) + } + + return nil +}