From 342de1f33ad1f68070767c16ff7c60edf1f823d6 Mon Sep 17 00:00:00 2001 From: Petro Protsakh Date: Wed, 15 Jan 2025 17:21:59 +0200 Subject: [PATCH 1/3] SCALRCORE-33497 Add remote_state_consumers attribute to scalr_workspace --- CHANGELOG.md | 3 + docs/resources/workspace.md | 1 + go.mod | 2 +- go.sum | 4 +- internal/provider/helpers.go | 12 +++ internal/provider/resource_scalr_workspace.go | 84 ++++++++++++++++++- 6 files changed, 102 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be1f809..9cc33f44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + - `scalr_policy_group` and `data.scalr_policy_group`: new attribute `common_functions_folder` ([#380](https://github.com/Scalr/terraform-provider-scalr/pull/380)) +- `scalr_workspace`: new attribute `remote_state_consumers` ([#384](https://github.com/Scalr/terraform-provider-scalr/pull/384)) ## [2.3.0] - 2024-12-20 diff --git a/docs/resources/workspace.md b/docs/resources/workspace.md index 2f77a9ed..8ffb3ea0 100644 --- a/docs/resources/workspace.md +++ b/docs/resources/workspace.md @@ -163,6 +163,7 @@ resource "scalr_workspace" "example-b" { - `module_version_id` (String) The identifier of a module version in the format `modver-`. This attribute conflicts with `vcs_provider_id` and `vcs_repo` attributes. - `operations` (Boolean, Deprecated) Set (true/false) to configure workspace remote execution. When `false` workspace is only used to store state. Defaults to `true`. - `provider_configuration` (Block Set) Provider configurations used in workspace runs. (see [below for nested schema](#nestedblock--provider_configuration)) +- `remote_state_consumers` (Set of String) The list of workspace identifiers that are allowed to access the state of this workspace. Use `["*"]` to share the state with all the workspaces within the environment. - `run_operation_timeout` (Number) The number of minutes run operation can be executed before termination. Defaults to `0` (not set, backend default is used). - `ssh_key_id` (String) The identifier of the SSH key to use for the workspace. - `tag_ids` (Set of String) List of tag IDs associated with the workspace. diff --git a/go.mod b/go.mod index 8fa4a840..e5f9a7ba 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 github.com/hashicorp/terraform-plugin-testing v1.11.0 github.com/hashicorp/terraform-svchost v0.1.1 - github.com/scalr/go-scalr v0.0.0-20250106085405-b4b290b8364e + github.com/scalr/go-scalr v0.0.0-20250115101005-ccf67688eb14 ) require ( diff --git a/go.sum b/go.sum index e81a1601..fb7c1dc1 100644 --- a/go.sum +++ b/go.sum @@ -201,8 +201,8 @@ github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBO github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= -github.com/scalr/go-scalr v0.0.0-20250106085405-b4b290b8364e h1:Ae/dv3iR7viRcrqxsGzs4e1g6i/3gT+1m8nYWuV+U5U= -github.com/scalr/go-scalr v0.0.0-20250106085405-b4b290b8364e/go.mod h1:p34SHb25YRvbgft7SUjSDYESeoQhWzAlxGXId/BbaSE= +github.com/scalr/go-scalr v0.0.0-20250115101005-ccf67688eb14 h1:lzee+F20vQN/iQA0eQZGS1ZXtf7la1ak3cdZdo739BI= +github.com/scalr/go-scalr v0.0.0-20250115101005-ccf67688eb14/go.mod h1:p34SHb25YRvbgft7SUjSDYESeoQhWzAlxGXId/BbaSE= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go index a10d3916..93194d99 100644 --- a/internal/provider/helpers.go +++ b/internal/provider/helpers.go @@ -108,6 +108,18 @@ func InterfaceArrToTagRelationArr(arr []interface{}) []*scalr.TagRelation { return tags } +func InterfaceArrToWorkspaceRelationArr(arr []interface{}) []*scalr.WorkspaceRelation { + relations := make([]*scalr.WorkspaceRelation, 0) + for _, id := range arr { + strID := id.(string) + if strID == "*" { + continue + } + relations = append(relations, &scalr.WorkspaceRelation{ID: strID}) + } + return relations +} + func getDefaultScalrAccountID() (string, bool) { if v := os.Getenv(defaults.CurrentAccountIDEnvVar); v != "" { return v, true diff --git a/internal/provider/resource_scalr_workspace.go b/internal/provider/resource_scalr_workspace.go index 546ebf0e..714bc013 100644 --- a/internal/provider/resource_scalr_workspace.go +++ b/internal/provider/resource_scalr_workspace.go @@ -383,6 +383,13 @@ func resourceScalrWorkspace() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + "remote_state_consumers": { + Description: "The list of workspace identifiers that are allowed to access the state of this workspace. Use `[\"*\"]` to share the state with all the workspaces within the environment.", + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, }, } } @@ -537,6 +544,19 @@ func resourceScalrWorkspaceCreate(ctx context.Context, d *schema.ResourceData, m options.Tags = tags } + remoteStateConsumers := make([]*scalr.WorkspaceRelation, 0) + if consumersI, ok := d.GetOkExists("remote_state_consumers"); ok { + options.RemoteStateSharing = ptr(false) + consumers := consumersI.(*schema.Set).List() + if (len(consumers) == 1) && (consumers[0].(string) == "*") { + options.RemoteStateSharing = ptr(true) + } else if len(consumers) > 0 { + for _, ws := range consumers { + remoteStateConsumers = append(remoteStateConsumers, &scalr.WorkspaceRelation{ID: ws.(string)}) + } + } + } + log.Printf("[DEBUG] Create workspace %s for environment: %s", name, environmentID) workspace, err := scalrClient.Workspaces.Create(ctx, options) if err != nil { @@ -571,6 +591,13 @@ func resourceScalrWorkspaceCreate(ctx context.Context, d *schema.ResourceData, m } } + if len(remoteStateConsumers) > 0 { + err = scalrClient.RemoteStateConsumers.Add(ctx, workspace.ID, remoteStateConsumers) + if err != nil { + return diag.Errorf("Error adding remote state consumers to workspace: %v", err) + } + } + return resourceScalrWorkspaceRead(ctx, d, meta) } @@ -695,6 +722,30 @@ func resourceScalrWorkspaceRead(ctx context.Context, d *schema.ResourceData, met } _ = d.Set("tag_ids", tagIDs) + if workspace.RemoteStateSharing { + all := []string{"*"} + _ = d.Set("remote_state_consumers", all) + } else { + consumers := make([]string, 0) + listOpts := scalr.ListOptions{} + for { + cl, err := scalrClient.RemoteStateConsumers.List(ctx, id, listOpts) + if err != nil { + return diag.Errorf("Error reading remote state consumers: %v", err) + } + + for _, c := range cl.Items { + consumers = append(consumers, c.ID) + } + + if cl.CurrentPage >= cl.TotalPages { + break + } + listOpts.PageNumber = cl.NextPage + } + _ = d.Set("remote_state_consumers", consumers) + } + return nil } @@ -709,7 +760,8 @@ func resourceScalrWorkspaceUpdate(ctx context.Context, d *schema.ResourceData, m d.HasChange("vcs_provider_id") || d.HasChange("agent_pool_id") || d.HasChange("deletion_protection_enabled") || d.HasChange("hooks") || d.HasChange("module_version_id") || d.HasChange("var_files") || d.HasChange("run_operation_timeout") || d.HasChange("iac_platform") || - d.HasChange("type") || d.HasChange("terragrunt_version") || d.HasChange("terragrunt_use_run_all") { + d.HasChange("type") || d.HasChange("terragrunt_version") || d.HasChange("terragrunt_use_run_all") || + d.HasChange("remote_state_consumers") { // Create a new options struct. options := scalr.WorkspaceUpdateOptions{ Name: ptr(d.Get("name").(string)), @@ -828,6 +880,14 @@ func resourceScalrWorkspaceUpdate(ctx context.Context, d *schema.ResourceData, m } } + if consumersI, ok := d.GetOkExists("remote_state_consumers"); ok { + options.RemoteStateSharing = ptr(false) + consumers := consumersI.(*schema.Set).List() + if (len(consumers) == 1) && (consumers[0].(string) == "*") { + options.RemoteStateSharing = ptr(true) + } + } + log.Printf("[DEBUG] Update workspace %s", id) _, err := scalrClient.Workspaces.Update(ctx, id, options) if err != nil { @@ -923,6 +983,28 @@ func resourceScalrWorkspaceUpdate(ctx context.Context, d *schema.ResourceData, m } } + if d.HasChange("remote_state_consumers") { + oldConsumers, newConsumers := d.GetChange("remote_state_consumers") + oldSet := oldConsumers.(*schema.Set) + newSet := newConsumers.(*schema.Set) + consumersToAdd := InterfaceArrToWorkspaceRelationArr(newSet.Difference(oldSet).List()) + consumersToDelete := InterfaceArrToWorkspaceRelationArr(oldSet.Difference(newSet).List()) + + if len(consumersToAdd) > 0 { + err := scalrClient.RemoteStateConsumers.Add(ctx, id, consumersToAdd) + if err != nil { + return diag.Errorf("Error adding remote state consumers: %v", err) + } + } + + if len(consumersToDelete) > 0 { + err := scalrClient.RemoteStateConsumers.Delete(ctx, id, consumersToDelete) + if err != nil { + return diag.Errorf("Error deleting remote state consumers: %v", err) + } + } + } + return resourceScalrWorkspaceRead(ctx, d, meta) } From 89f0431d4acb05e09dd419d5864d4bffc4df9acb Mon Sep 17 00:00:00 2001 From: Petro Protsakh Date: Wed, 15 Jan 2025 17:24:49 +0200 Subject: [PATCH 2/3] SCALRCORE-33497 Ignore linter for deprecated check --- internal/provider/resource_scalr_workspace.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/provider/resource_scalr_workspace.go b/internal/provider/resource_scalr_workspace.go index 714bc013..fece4bbe 100644 --- a/internal/provider/resource_scalr_workspace.go +++ b/internal/provider/resource_scalr_workspace.go @@ -545,7 +545,7 @@ func resourceScalrWorkspaceCreate(ctx context.Context, d *schema.ResourceData, m } remoteStateConsumers := make([]*scalr.WorkspaceRelation, 0) - if consumersI, ok := d.GetOkExists("remote_state_consumers"); ok { + if consumersI, ok := d.GetOkExists("remote_state_consumers"); ok { //nolint:staticcheck options.RemoteStateSharing = ptr(false) consumers := consumersI.(*schema.Set).List() if (len(consumers) == 1) && (consumers[0].(string) == "*") { @@ -880,7 +880,7 @@ func resourceScalrWorkspaceUpdate(ctx context.Context, d *schema.ResourceData, m } } - if consumersI, ok := d.GetOkExists("remote_state_consumers"); ok { + if consumersI, ok := d.GetOkExists("remote_state_consumers"); ok { //nolint:staticcheck options.RemoteStateSharing = ptr(false) consumers := consumersI.(*schema.Set).List() if (len(consumers) == 1) && (consumers[0].(string) == "*") { From 79b066b55616ac3eda25f338aeda89a9f1504c32 Mon Sep 17 00:00:00 2001 From: Petro Protsakh Date: Thu, 16 Jan 2025 11:31:31 +0200 Subject: [PATCH 3/3] SCALRCORE-33497 Add test --- .../provider/resource_scalr_workspace_test.go | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/internal/provider/resource_scalr_workspace_test.go b/internal/provider/resource_scalr_workspace_test.go index c8df2227..c3a1027a 100644 --- a/internal/provider/resource_scalr_workspace_test.go +++ b/internal/provider/resource_scalr_workspace_test.go @@ -319,6 +319,37 @@ func TestAccScalrWorkspaceSSHKey(t *testing.T) { }) } +func TestAccScalrWorkspaceStateConsumers(t *testing.T) { + workspace := &scalr.Workspace{} + rInt := GetRandomInteger() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: protoV5ProviderFactories(t), + CheckDestroy: testAccCheckScalrWorkspaceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccScalrWorkspaceWithStateConsumersConfig(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckScalrWorkspaceExists("scalr_workspace.test", workspace), + resource.TestCheckResourceAttr( + "scalr_workspace.test", "remote_state_consumers.#", "2"), + testAccCheckScalrWorkspaceStateSharing("scalr_workspace.test", false), + ), + }, + { + Config: testAccScalrWorkspaceWithStateConsumersUpdatedConfig(rInt), + Check: resource.ComposeTestCheckFunc( + testAccCheckScalrWorkspaceExists("scalr_workspace.test", workspace), + resource.TestCheckResourceAttr( + "scalr_workspace.test", "remote_state_consumers.#", "1"), + testAccCheckScalrWorkspaceStateSharing("scalr_workspace.test", true), + ), + }, + }, + }) +} + func testAccCheckScalrSSHKeyExists(n string, sshKey *scalr.SSHKey) resource.TestCheckFunc { return func(s *terraform.State) error { scalrClient := testAccProviderSDK.Meta().(*scalr.Client) @@ -557,6 +588,34 @@ func testAccCheckScalrWorkspaceProviderConfigurationsUpdated( } } +func testAccCheckScalrWorkspaceStateSharing( + n string, isShared bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + scalrClient := testAccProviderSDK.Meta().(*scalr.Client) + + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + // Get the workspace + w, err := scalrClient.Workspaces.ReadByID(ctx, rs.Primary.ID) + if err != nil { + return err + } + + if w.RemoteStateSharing != isShared { + return fmt.Errorf("Expected RemoteStateSharing %t, got %t", isShared, w.RemoteStateSharing) + } + + return nil + } +} + func testAccCheckScalrWorkspaceDestroy(s *terraform.State) error { scalrClient := testAccProviderSDK.Meta().(*scalr.Client) @@ -853,3 +912,51 @@ resource "scalr_workspace" "test" { working_directory = "" }`, rInt, defaultAccount, sshKeyName) } + +func testAccScalrWorkspaceWithStateConsumersConfig(rInt int) string { + return fmt.Sprintf(` +resource "scalr_environment" "test" { + name = "test-env-%[1]d" + account_id = "%[2]s" +} + +resource "scalr_workspace" "consumer1" { + name = "consumer1-%[1]d" + environment_id = scalr_environment.test.id +} + +resource "scalr_workspace" "consumer2" { + name = "consumer2-%[1]d" + environment_id = scalr_environment.test.id +} + +resource "scalr_workspace" "test" { + name = "state-sharing-%[1]d" + environment_id = scalr_environment.test.id + remote_state_consumers = [ scalr_workspace.consumer1.id, scalr_workspace.consumer2.id ] +}`, rInt, defaultAccount) +} + +func testAccScalrWorkspaceWithStateConsumersUpdatedConfig(rInt int) string { + return fmt.Sprintf(` +resource "scalr_environment" "test" { + name = "test-env-%[1]d" + account_id = "%[2]s" +} + +resource "scalr_workspace" "consumer1" { + name = "consumer1-%[1]d" + environment_id = scalr_environment.test.id +} + +resource "scalr_workspace" "consumer2" { + name = "consumer2-%[1]d" + environment_id = scalr_environment.test.id +} + +resource "scalr_workspace" "test" { + name = "state-sharing-%[1]d" + environment_id = scalr_environment.test.id + remote_state_consumers = [ "*" ] +}`, rInt, defaultAccount) +}