diff --git a/bitbucket/provider.go b/bitbucket/provider.go index c63ab95a..32aeac9b 100644 --- a/bitbucket/provider.go +++ b/bitbucket/provider.go @@ -91,6 +91,7 @@ func Provider() *schema.Provider { "bitbucket_repository_variable": resourceRepositoryVariable(), "bitbucket_ssh_key": resourceSshKey(), "bitbucket_workspace_hook": resourceWorkspaceHook(), + "bitbucket_workspace_variable": resourceWorkspaceVariable(), }, DataSourcesMap: map[string]*schema.Resource{ "bitbucket_current_user": dataCurrentUser(), diff --git a/bitbucket/resource_workspace_variable.go b/bitbucket/resource_workspace_variable.go new file mode 100644 index 00000000..3cbf941b --- /dev/null +++ b/bitbucket/resource_workspace_variable.go @@ -0,0 +1,168 @@ +package bitbucket + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + + "github.com/DrFaust92/bitbucket-go-client" + "github.com/antihax/optional" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceWorkspaceVariable() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceWorkspaceVariableCreate, + UpdateWithoutTimeout: resourceWorkspaceVariableUpdate, + ReadWithoutTimeout: resourceWorkspaceVariableRead, + DeleteWithoutTimeout: resourceWorkspaceVariableDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "uuid": { + Type: schema.TypeString, + Computed: true, + }, + "key": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + "secured": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "workspace": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func newWorkspaceVariableFromResource(d *schema.ResourceData) bitbucket.PipelineVariable { + dk := bitbucket.PipelineVariable{ + Key: d.Get("key").(string), + Value: d.Get("value").(string), + Secured: d.Get("secured").(bool), + } + return dk +} + +func resourceWorkspaceVariableCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(Clients).genClient + pipeApi := c.ApiClient.PipelinesApi + rvcr := newWorkspaceVariableFromResource(d) + + workspacePipeBody := &bitbucket.PipelinesApiCreatePipelineVariableForWorkspaceOpts{ + Body: optional.NewInterface(rvcr), + } + + workspace := d.Get("workspace").(string) + + log.Printf("[DEBUG] Workspace Variable Request: %#v", workspacePipeBody) + + rvRes, res, err := pipeApi.CreatePipelineVariableForWorkspace(c.AuthContext, workspace, workspacePipeBody) + + log.Printf("[DEBUG] Workspace Variable Create Request Res: %#v", res) + + if err := handleClientError(err); err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%s/%s", workspace, rvRes.Uuid)) + + return resourceWorkspaceVariableRead(ctx, d, m) +} + +func resourceWorkspaceVariableRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(Clients).genClient + pipeApi := c.ApiClient.PipelinesApi + + workspace, uuid, err := workspaceVarId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + rvRes, res, err := pipeApi.GetPipelineVariableForWorkspace(c.AuthContext, workspace, uuid) + + log.Printf("[DEBUG] Workspace Variable Get Request Res: %#v", res) + + if res.StatusCode == http.StatusNotFound { + log.Printf("[WARN] Workspace Variable (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err := handleClientError(err); err != nil { + return diag.FromErr(err) + } + + d.Set("uuid", rvRes.Uuid) + d.Set("workspace", workspace) + d.Set("key", rvRes.Key) + d.Set("secured", rvRes.Secured) + + if !rvRes.Secured { + d.Set("value", rvRes.Value) + } else { + d.Set("value", d.Get("value").(string)) + } + + return nil +} + +func resourceWorkspaceVariableUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(Clients).genClient + pipeApi := c.ApiClient.PipelinesApi + + workspace, uuid, err := workspaceVarId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + rvcr := newWorkspaceVariableFromResource(d) + + _, _, err = pipeApi.UpdatePipelineVariableForWorkspace(c.AuthContext, rvcr, workspace, uuid) + if err := handleClientError(err); err != nil { + return diag.FromErr(err) + } + + return resourceWorkspaceVariableRead(ctx, d, m) +} + +func resourceWorkspaceVariableDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(Clients).genClient + pipeApi := c.ApiClient.PipelinesApi + + workspace, uuid, err := workspaceVarId(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + _, err = pipeApi.DeletePipelineVariableForWorkspace(c.AuthContext, workspace, uuid) + if err := handleClientError(err); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func workspaceVarId(workspace string) (string, string, error) { + idparts := strings.Split(workspace, "/") + if len(idparts) == 2 { + return idparts[0], idparts[1], nil + } else { + return "", "", fmt.Errorf("incorrect ID format, should match `workspace/uuid`") + } +} diff --git a/bitbucket/resource_workspace_variable_test.go b/bitbucket/resource_workspace_variable_test.go new file mode 100644 index 00000000..9ee7ade1 --- /dev/null +++ b/bitbucket/resource_workspace_variable_test.go @@ -0,0 +1,175 @@ +package bitbucket + +import ( + "fmt" + "net/http" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccBitbucketWorkspaceVariable_basic(t *testing.T) { + workspace := os.Getenv("BITBUCKET_TEAM") + resourceName := "bitbucket_workspace_variable.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBitbucketWorkspaceVariableDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBitbucketWorkspaceVariableConfig(workspace, "test", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckBitbucketWorkspaceVariableExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "workspace", workspace), + resource.TestCheckResourceAttr(resourceName, "key", "test"), + resource.TestCheckResourceAttr(resourceName, "value", "test"), + resource.TestCheckResourceAttr(resourceName, "secured", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBitbucketWorkspaceVariableConfig(workspace, "test-2", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckBitbucketWorkspaceVariableExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "workspace", workspace), + resource.TestCheckResourceAttr(resourceName, "key", "test"), + resource.TestCheckResourceAttr(resourceName, "value", "test-2"), + resource.TestCheckResourceAttr(resourceName, "secured", "false"), + ), + }, + }, + }) +} + +func TestAccBitbucketWorkspaceVariable_manyVars(t *testing.T) { + workspace := os.Getenv("BITBUCKET_TEAM") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBitbucketWorkspaceVariableDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBitbucketWorkspaceVariableManyConfig(workspace, "test", false), + }, + }, + }) +} + +func TestAccBitbucketWorkspaceVariable_secure(t *testing.T) { + workspace := os.Getenv("BITBUCKET_TEAM") + resourceName := "bitbucket_workspace_variable.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBitbucketWorkspaceVariableDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBitbucketWorkspaceVariableConfig(workspace, "test", true), + Check: resource.ComposeTestCheckFunc( + testAccCheckBitbucketWorkspaceVariableExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "workspace", workspace), + resource.TestCheckResourceAttr(resourceName, "key", "test"), + resource.TestCheckResourceAttr(resourceName, "value", "test"), + resource.TestCheckResourceAttr(resourceName, "secured", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"value"}, + }, + { + Config: testAccBitbucketWorkspaceVariableConfig(workspace, "test", false), + Check: resource.ComposeTestCheckFunc( + testAccCheckBitbucketWorkspaceVariableExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "workspace", workspace), + resource.TestCheckResourceAttr(resourceName, "key", "test"), + resource.TestCheckResourceAttr(resourceName, "value", "test"), + resource.TestCheckResourceAttr(resourceName, "secured", "false"), + ), + }, + { + Config: testAccBitbucketWorkspaceVariableConfig(workspace, "test", true), + Check: resource.ComposeTestCheckFunc( + testAccCheckBitbucketWorkspaceVariableExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "workspace", workspace), + resource.TestCheckResourceAttr(resourceName, "key", "test"), + resource.TestCheckResourceAttr(resourceName, "value", "test"), + resource.TestCheckResourceAttr(resourceName, "secured", "true"), + ), + }, + }, + }) +} + +func testAccCheckBitbucketWorkspaceVariableDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(Clients).genClient + pipeApi := client.ApiClient.PipelinesApi + for _, rs := range s.RootModule().Resources { + if rs.Type != "bitbucket_workspace_variable" { + continue + } + + workspace, uuid, err := workspaceVarId(rs.Primary.ID) + if err != nil { + return err + } + + _, res, err := pipeApi.GetPipelineVariableForWorkspace(client.AuthContext, workspace, uuid) + + if err == nil { + return fmt.Errorf("The resource was found should have errored") + } + + if res.StatusCode != http.StatusNotFound { + return fmt.Errorf("Workspace Variable still exists") + } + } + return nil +} + +func testAccCheckBitbucketWorkspaceVariableExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found %s", n) + } + + return nil + } +} + +func testAccBitbucketWorkspaceVariableConfig(workspace, val string, secure bool) string { + return fmt.Sprintf(` +resource "bitbucket_workspace_variable" "test" { + key = "test" + value = %[2]q + workspace = %[1]q + secured = %[3]t +} +`, workspace, val, secure) +} + +func testAccBitbucketWorkspaceVariableManyConfig(workspace, val string, secure bool) string { + return fmt.Sprintf(` +resource "bitbucket_workspace_variable" "test" { + count = 50 + + key = "test${count.index}" + value = %[2]q + workspace = %[1]q + secured = %[3]t +} +`, workspace, val, secure) +} diff --git a/docs/resources/workspace_variable.md b/docs/resources/workspace_variable.md new file mode 100644 index 00000000..cab7fd01 --- /dev/null +++ b/docs/resources/workspace_variable.md @@ -0,0 +1,44 @@ +--- +layout: "bitbucket" +page_title: "Bitbucket: bitbucket_workspace_variable" +sidebar_current: "docs-bitbucket-resource-workspace-variable" +description: |- + Manage variables for your pipelines workspace environments +--- + + +# bitbucket\_workspace\_variable + +This resource allows you to configure workspace variables. + +OAuth2 Scopes: `none` + +## Example Usage + +```hcl +resource "bitbucket_workspace_variable" "country" { + workspace = bitbucket_workspace.test.id + key = "COUNTRY" + value = "Kenya" + secured = false +} +``` + +## Argument Reference + +* `workspace` - (Required) The workspace ID you want to assign this variable to. +* `key` - (Required) The unique name of the variable. +* `value` - (Required) The value of the variable. +* `secured` - (Optional) If true, this variable will be treated as secured. The value will never be exposed in the logs or the REST API. + +## Attributes Reference + +* `uuid` - (Computed) The UUID identifying the variable. + +## Import + +Workspace Variables can be imported using their `workspace-id/uuid` ID, e.g. + +```sh +terraform import bitbucket_workspace_variable.example workspace-id/uuid +```