diff --git a/bitbucket/data_source_bitbucket_pipeline_variable.go b/bitbucket/data_source_bitbucket_pipeline_variable.go new file mode 100644 index 0000000..edbcfbd --- /dev/null +++ b/bitbucket/data_source_bitbucket_pipeline_variable.go @@ -0,0 +1,44 @@ +package bitbucket + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceBitbucketPipelineVariable() *schema.Resource { + return &schema.Resource{ + ReadContext: resourceBitbucketPipelineVariableRead, + Schema: map[string]*schema.Schema{ + "id": { + Description: "The ID of the pipeline variable.", + Type: schema.TypeString, + Required: true, + }, + "workspace": { + Description: "The slug or UUID (including the enclosing `{}`) of the workspace.", + Type: schema.TypeString, + Required: true, + }, + "repository": { + Description: "The name of the repository (must consist of only lowercase ASCII letters, numbers, underscores and hyphens).", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateRepositoryName, + }, + "key": { + Description: "The name of the variable.", + Type: schema.TypeString, + Computed: true, + }, + "value": { + Description: "The value of the variable (note: if this variable is marked 'secured', this attribute will be blank).", + Type: schema.TypeString, + Computed: true, + }, + "secured": { + Description: "Whether this variable is considered secure/sensitive. If true, then it's value will not be exposed in any logs or API requests.", + Type: schema.TypeBool, + Computed: true, + }, + }, + } +} diff --git a/bitbucket/data_source_bitbucket_pipeline_variable_test.go b/bitbucket/data_source_bitbucket_pipeline_variable_test.go new file mode 100644 index 0000000..0cb381e --- /dev/null +++ b/bitbucket/data_source_bitbucket_pipeline_variable_test.go @@ -0,0 +1,109 @@ +package bitbucket + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccBitbucketPipelineVariableDataSource_basic(t *testing.T) { + workspaceSlug := os.Getenv("BITBUCKET_USERNAME") + projectName := "tf-acc-test-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + projectKey := strings.ToUpper(acctest.RandStringFromCharSet(3, acctest.CharSetAlpha)) + repoName := "tf-acc-test-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + pipelineVariableName := "tf_acc_test" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + pipelineVariableValue := "tf-acc-test" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "bitbucket_workspace" "testacc" { + id = "%s" + } + + resource "bitbucket_project" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + name = "%s" + key = "%s" + } + + resource "bitbucket_repository" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + project_key = bitbucket_project.testacc.key + name = "%s" + enable_pipelines = true + } + + resource "bitbucket_pipeline_variable" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + repository = bitbucket_repository.testacc.name + key = "%s" + value = "%s" + secured = true + } + + data "bitbucket_pipeline_variable" "testacc" { + id = bitbucket_pipeline_variable.testacc.id + workspace = data.bitbucket_workspace.testacc.id + repository = bitbucket_repository.testacc.name + }`, workspaceSlug, projectName, projectKey, repoName, pipelineVariableName, pipelineVariableValue), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.bitbucket_pipeline_variable.testacc", "workspace", workspaceSlug), + resource.TestCheckResourceAttr("data.bitbucket_pipeline_variable.testacc", "repository", repoName), + resource.TestCheckResourceAttr("data.bitbucket_pipeline_variable.testacc", "key", pipelineVariableName), + resource.TestCheckResourceAttr("data.bitbucket_pipeline_variable.testacc", "value", ""), + resource.TestCheckResourceAttr("data.bitbucket_pipeline_variable.testacc", "secured", "true"), + resource.TestCheckResourceAttrSet("data.bitbucket_pipeline_variable.testacc", "id"), + ), + }, + { + Config: fmt.Sprintf(` + data "bitbucket_workspace" "testacc" { + id = "%s" + } + + resource "bitbucket_project" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + name = "%s" + key = "%s" + } + + resource "bitbucket_repository" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + project_key = bitbucket_project.testacc.key + name = "%s" + enable_pipelines = true + } + + resource "bitbucket_pipeline_variable" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + repository = bitbucket_repository.testacc.name + key = "%s" + value = "%s" + secured = false + } + + data "bitbucket_pipeline_variable" "testacc" { + id = bitbucket_pipeline_variable.testacc.id + workspace = data.bitbucket_workspace.testacc.id + repository = bitbucket_repository.testacc.name + }`, workspaceSlug, projectName, projectKey, repoName, pipelineVariableName, pipelineVariableValue), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.bitbucket_pipeline_variable.testacc", "workspace", workspaceSlug), + resource.TestCheckResourceAttr("data.bitbucket_pipeline_variable.testacc", "repository", repoName), + resource.TestCheckResourceAttr("data.bitbucket_pipeline_variable.testacc", "key", pipelineVariableName), + resource.TestCheckResourceAttr("data.bitbucket_pipeline_variable.testacc", "value", pipelineVariableValue), + resource.TestCheckResourceAttr("data.bitbucket_pipeline_variable.testacc", "secured", "false"), + resource.TestCheckResourceAttrSet("data.bitbucket_pipeline_variable.testacc", "id"), + ), + }, + }, + }) +} diff --git a/bitbucket/provider.go b/bitbucket/provider.go index b10a122..a6dbf5b 100644 --- a/bitbucket/provider.go +++ b/bitbucket/provider.go @@ -33,6 +33,7 @@ func Provider() *schema.Provider { "bitbucket_deploy_key": dataSourceBitbucketDeployKey(), "bitbucket_group": dataSourceBitbucketGroup(), "bitbucket_group_permission": dataSourceBitbucketGroupPermission(), + "bitbucket_pipeline_variable": dataSourceBitbucketPipelineVariable(), "bitbucket_project": dataSourceBitbucketProject(), "bitbucket_repository": dataSourceBitbucketRepository(), "bitbucket_user": dataSourceBitbucketUser(), @@ -47,6 +48,7 @@ func Provider() *schema.Provider { "bitbucket_group": resourceBitbucketGroup(), "bitbucket_group_member": resourceBitbucketGroupMember(), "bitbucket_group_permission": resourceBitbucketGroupPermission(), + "bitbucket_pipeline_variable": resourceBitbucketPipelineVariable(), "bitbucket_project": resourceBitbucketProject(), "bitbucket_repository": resourceBitbucketRepository(), "bitbucket_webhook": resourceBitbucketWebhook(), diff --git a/bitbucket/resource_bitbucket_pipeline_variable.go b/bitbucket/resource_bitbucket_pipeline_variable.go new file mode 100644 index 0000000..a71645c --- /dev/null +++ b/bitbucket/resource_bitbucket_pipeline_variable.go @@ -0,0 +1,175 @@ +package bitbucket + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + gobb "github.com/ktrysmt/go-bitbucket" +) + +func resourceBitbucketPipelineVariable() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceBitbucketPipelineVariableCreate, + ReadContext: resourceBitbucketPipelineVariableRead, + UpdateContext: resourceBitbucketPipelineVariableUpdate, + DeleteContext: resourceBitbucketPipelineVariableDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceBitbucketPipelineVariableImport, + }, + Schema: map[string]*schema.Schema{ + "id": { + Description: "The ID of the pipeline variable.", + Type: schema.TypeString, + Computed: true, + }, + "workspace": { + Description: "The slug or UUID (including the enclosing `{}`) of the workspace.", + Type: schema.TypeString, + Required: true, + }, + "repository": { + Description: "The name of the repository (must consist of only lowercase ASCII letters, numbers, underscores and hyphens).", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateRepositoryName, + }, + "key": { + Description: "The name of the variable (must consist of only ASCII letters, numbers, underscores & not begin with a number).", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateRepositoryVariableName, + }, + "value": { + Description: "The value of the variable.", + Type: schema.TypeString, + Required: true, + }, + "secured": { + Description: "Whether this variable is considered secure/sensitive. If true, then it's value will not be exposed in any logs or API requests.", + Type: schema.TypeBool, + Default: false, + Optional: true, + }, + }, + } +} + +func resourceBitbucketPipelineVariableCreate(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*Clients).V2 + + pipelineVariable, err := client.Repositories.Repository.AddPipelineVariable( + &gobb.RepositoryPipelineVariableOptions{ + Owner: resourceData.Get("workspace").(string), + RepoSlug: resourceData.Get("repository").(string), + Key: resourceData.Get("key").(string), + Value: resourceData.Get("value").(string), + Secured: resourceData.Get("secured").(bool), + }, + ) + if err != nil { + return diag.FromErr(fmt.Errorf("unable to create pipeline variable with error: %s", err)) + } + + resourceData.SetId(pipelineVariable.Uuid) + + return resourceBitbucketPipelineVariableRead(ctx, resourceData, meta) +} + +func resourceBitbucketPipelineVariableRead(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*Clients).V2 + + pipelineVariable, err := client.Repositories.Repository.GetPipelineVariable( + &gobb.RepositoryPipelineVariableOptions{ + Owner: resourceData.Get("workspace").(string), + RepoSlug: resourceData.Get("repository").(string), + Uuid: resourceData.Get("id").(string), + }, + ) + if err != nil { + return diag.FromErr(fmt.Errorf("unable to get repository variable with error: %s", err)) + } + + _ = resourceData.Set("key", pipelineVariable.Key) + + if !pipelineVariable.Secured { + _ = resourceData.Set("value", pipelineVariable.Value) + } else { + _ = resourceData.Set("value", resourceData.Get("value").(string)) + } + + _ = resourceData.Set("secured", pipelineVariable.Secured) + + resourceData.SetId(pipelineVariable.Uuid) + + return nil +} + +func resourceBitbucketPipelineVariableUpdate(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*Clients).V2 + + _, err := client.Repositories.Repository.UpdatePipelineVariable( + &gobb.RepositoryPipelineVariableOptions{ + Owner: resourceData.Get("workspace").(string), + RepoSlug: resourceData.Get("repository").(string), + Uuid: resourceData.Get("id").(string), + Key: resourceData.Get("key").(string), + Value: resourceData.Get("value").(string), + Secured: resourceData.Get("secured").(bool), + }, + ) + if err != nil { + return diag.FromErr(fmt.Errorf("unable to update repository variable with error: %s", err)) + } + + return resourceBitbucketPipelineVariableRead(ctx, resourceData, meta) +} + +func resourceBitbucketPipelineVariableDelete(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*Clients).V2 + + _, err := client.Repositories.Repository.DeletePipelineVariable( + &gobb.RepositoryPipelineVariableDeleteOptions{ + Owner: resourceData.Get("workspace").(string), + RepoSlug: resourceData.Get("repository").(string), + Uuid: resourceData.Id(), + }, + ) + if err != nil { + return diag.FromErr(fmt.Errorf("unable to delete repository variable with error: %s", err)) + } + + resourceData.SetId("") + + return nil +} + +func resourceBitbucketPipelineVariableImport(ctx context.Context, resourceData *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + ret := []*schema.ResourceData{resourceData} + + splitID := strings.Split(resourceData.Id(), "/") + if len(splitID) < 3 { + return ret, fmt.Errorf("invalid import ID. It must to be in this format \"//\"") + } + + _ = resourceData.Set("workspace", splitID[0]) + _ = resourceData.Set("repository", splitID[1]) + _ = resourceData.Set("id", splitID[2]) + + _ = resourceBitbucketPipelineVariableRead(ctx, resourceData, meta) + + return ret, nil +} + +func validateRepositoryVariableName(val interface{}, path cty.Path) diag.Diagnostics { + match, _ := regexp.MatchString("^([a-zA-Z])[a-zA-Z0-9_]+$", val.(string)) + if !match { + return diag.FromErr(fmt.Errorf("repository variable name must consist of only ASCII letters, numbers, underscores & not begin with a number (a-z, 0-9, _)")) + } + + return diag.Diagnostics{} +} diff --git a/bitbucket/resource_bitbucket_pipeline_variable_test.go b/bitbucket/resource_bitbucket_pipeline_variable_test.go new file mode 100644 index 0000000..90c6125 --- /dev/null +++ b/bitbucket/resource_bitbucket_pipeline_variable_test.go @@ -0,0 +1,124 @@ +package bitbucket + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/assert" +) + +func TestAccBitbucketPipelineVariableResource_basic(t *testing.T) { + workspaceSlug := os.Getenv("BITBUCKET_USERNAME") + projectName := "tf-acc-test-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + projectKey := strings.ToUpper(acctest.RandStringFromCharSet(3, acctest.CharSetAlpha)) + repoName := "tf-acc-test-" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + pipelineVariableName := "tf_acc_test" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + pipelineVariableValue := "tf-acc-test" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + data "bitbucket_workspace" "testacc" { + id = "%s" + } + + resource "bitbucket_project" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + name = "%s" + key = "%s" + } + + resource "bitbucket_repository" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + project_key = bitbucket_project.testacc.key + name = "%s" + enable_pipelines = true + } + + resource "bitbucket_pipeline_variable" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + repository = bitbucket_repository.testacc.name + key = "%s" + value = "%s" + secured = true + }`, workspaceSlug, projectName, projectKey, repoName, pipelineVariableName, pipelineVariableValue), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("bitbucket_pipeline_variable.testacc", "workspace", workspaceSlug), + resource.TestCheckResourceAttr("bitbucket_pipeline_variable.testacc", "repository", repoName), + resource.TestCheckResourceAttr("bitbucket_pipeline_variable.testacc", "key", pipelineVariableName), + resource.TestCheckResourceAttr("bitbucket_pipeline_variable.testacc", "value", pipelineVariableValue), + resource.TestCheckResourceAttr("bitbucket_pipeline_variable.testacc", "secured", "true"), + + resource.TestCheckResourceAttrSet("bitbucket_pipeline_variable.testacc", "id"), + ), + }, + { + Config: fmt.Sprintf(` + data "bitbucket_workspace" "testacc" { + id = "%s" + } + + resource "bitbucket_project" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + name = "%s" + key = "%s" + } + + resource "bitbucket_repository" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + project_key = bitbucket_project.testacc.key + name = "%s" + enable_pipelines = true + } + + resource "bitbucket_pipeline_variable" "testacc" { + workspace = data.bitbucket_workspace.testacc.id + repository = bitbucket_repository.testacc.name + key = "%s" + value = "%s" + }`, workspaceSlug, projectName, projectKey, repoName, pipelineVariableName, pipelineVariableValue), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("bitbucket_pipeline_variable.testacc", "workspace", workspaceSlug), + resource.TestCheckResourceAttr("bitbucket_pipeline_variable.testacc", "repository", repoName), + resource.TestCheckResourceAttr("bitbucket_pipeline_variable.testacc", "key", pipelineVariableName), + resource.TestCheckResourceAttr("bitbucket_pipeline_variable.testacc", "value", pipelineVariableValue), + resource.TestCheckResourceAttr("bitbucket_pipeline_variable.testacc", "secured", "false"), + + resource.TestCheckResourceAttrSet("bitbucket_pipeline_variable.testacc", "id"), + ), + }, + { + ResourceName: "bitbucket_pipeline_variable.testacc", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(state *terraform.State) (string, error) { + resources := state.Modules[0].Resources + pipelineVariableResourceAttr := resources["bitbucket_pipeline_variable.testacc"].Primary.Attributes + return fmt.Sprintf("%s/%s/%s", workspaceSlug, repoName, pipelineVariableResourceAttr["id"]), nil + }, + }, + }, + }) +} + +func TestValidateRepositoryVariableName(t *testing.T) { + invalidNames := []string{"4asdasd", "$£$%$", "AS@£$@£$@", "as-adasd"} + for _, name := range invalidNames { + validator := validateRepositoryVariableName(name, nil) + assert.True(t, validator.HasError()) + } + + validNames := []string{"adasd", "asds2342432", "asdadsa_asdasd23424242", "Asdfdfsdf", "AsfdsdfSDFDFSf"} + for _, name := range validNames { + validator := validateRepositoryVariableName(name, nil) + assert.False(t, validator.HasError()) + } +} diff --git a/docs/data-sources/bitbucket_pipeline_variable.md b/docs/data-sources/bitbucket_pipeline_variable.md new file mode 100644 index 0000000..c8f4a7e --- /dev/null +++ b/docs/data-sources/bitbucket_pipeline_variable.md @@ -0,0 +1,30 @@ +# Data Source: bitbucket_pipeline_variable +Use this data source to get the pipeline variable resource, you can then reference its attributes without having to hardcode them. + +## Example Usage +```hcl +data "bitbucket_pipeline_variable" "example" { + id = "{pipeline-variable-id}" + workspace = "workspace-slug" + repository = "example-repo" +} +``` +```hcl +data "bitbucket_pipeline_variable" "example" { + id = "{pipeline-variable-id}" + workspace = "{workspace-uuid}" + repository = "example-repo" +} +``` + +## Argument Reference +The following arguments are supported: +* `id` - (Required) The ID of the pipeline variable. +* `workspace` - (Required) The slug or UUID (including the enclosing `{}`) of the workspace. +* `repository` - (Required) The name of the repository (must consist of only lowercase ASCII letters, numbers, underscores and hyphens). + +## Attribute Reference +In addition to the arguments above, the following additional attributes are exported: +* `key` - The name of the variable. +* `value` - The value of the variable (note: if this variable is marked 'secured', this attribute will be blank). +* `secured` - Whether this variable is considered secure/sensitive. If true, then it's value will not be exposed in any logs or API requests. diff --git a/docs/resources/bitbucket_pipeline_variable.md b/docs/resources/bitbucket_pipeline_variable.md new file mode 100644 index 0000000..9a4e1c8 --- /dev/null +++ b/docs/resources/bitbucket_pipeline_variable.md @@ -0,0 +1,40 @@ +# Resource: bitbucket_pipeline_variable +Manage a pipeline variable for a repository within Bitbucket. + +## Example Usage +```hcl +resource "bitbucket_pipeline_variable" "example" { + workspace = "workspace-slug" + repository = "example-repo" + key = "some-variable-name" + value = "some-variable-value" + secured = false +} +``` + +## Argument Reference +The following arguments are supported: +* `workspace` - (Required) The slug or UUID (including the enclosing `{}`) of the workspace. +* `repository` - (Required) The name of the repository (must consist of only lowercase ASCII letters, numbers, underscores and hyphens). +* `key` - (Required) The name of the variable (must consist of only ASCII letters, numbers, underscores & not begin with a number). +* `value` - (Required) The value of the variable. +* `secured` - (Optional) Whether this variable is considered secure/sensitive. If true, then it's value will not be exposed in any logs or API requests. Defaults to `false`. + +## Attribute Reference +In addition to the arguments above, the following additional attributes are exported: +* `id` - The ID of the pipeline variable. + +## Import +Bitbucket pipeline variable's can be imported with a combination of its workspace slug/UUID, repository name & pipeline variable ID. + +**_Note: secured values will not be imported!_** + +### Example using workspace slug, repository name & pipeline variable ID +```sh +$ terraform import bitbucket_pipeline_variable.example "workspace-slug/example-repo/1234" +``` + +### Example using workspace UUID, repository name & pipeline variable ID +```sh +$ terraform import bitbucket_pipeline_variable.example "{123ab4cd-5678-9e01-f234-5678g9h01i2j}/example-repo/1234" +```