diff --git a/bitbucket/client.go b/bitbucket/client.go index a97b89ea..cde24b7f 100644 --- a/bitbucket/client.go +++ b/bitbucket/client.go @@ -41,7 +41,7 @@ type Client struct { } // Do Will just call the bitbucket api but also add auth to it and some extra headers -func (c *Client) Do(method, endpoint string, payload *bytes.Buffer, addJsonHeader bool) (*http.Response, error) { +func (c *Client) Do(method, endpoint string, payload *bytes.Buffer, contentType string) (*http.Response, error) { absoluteendpoint := BitbucketEndpoint + endpoint log.Printf("[DEBUG] Sending request to %s %s", method, absoluteendpoint) @@ -77,9 +77,9 @@ func (c *Client) Do(method, endpoint string, payload *bytes.Buffer, addJsonHeade token.SetAuthHeader(req) } - if payload != nil && addJsonHeader { + if payload != nil && contentType != "" { // Can cause bad request when putting default reviews if set. - req.Header.Add("Content-Type", "application/json") + req.Header.Add("Content-Type", contentType) } req.Close = true @@ -112,30 +112,35 @@ func (c *Client) Do(method, endpoint string, payload *bytes.Buffer, addJsonHeade // Get is just a helper method to do but with a GET verb func (c *Client) Get(endpoint string) (*http.Response, error) { - return c.Do("GET", endpoint, nil, true) + return c.Do("GET", endpoint, nil, "application/json") } // Post is just a helper method to do but with a POST verb func (c *Client) Post(endpoint string, jsonpayload *bytes.Buffer) (*http.Response, error) { - return c.Do("POST", endpoint, jsonpayload, true) + return c.Do("POST", endpoint, jsonpayload, "application/json") } // PostNonJson is just a helper method to do but with a POST verb without Json Header -func (c *Client) PostNonJson(endpoint string, jsonpayload *bytes.Buffer) (*http.Response, error) { - return c.Do("POST", endpoint, jsonpayload, false) +func (c *Client) PostNonJson(endpoint string, payload *bytes.Buffer) (*http.Response, error) { + return c.Do("POST", endpoint, payload, "") +} + +// PostWithContentType is just a helper method to do but with a POST verb and a provided content type +func (c *Client) PostWithContentType(endpoint, contentType string, payload *bytes.Buffer) (*http.Response, error) { + return c.Do("POST", endpoint, payload, contentType) } // Put is just a helper method to do but with a PUT verb func (c *Client) Put(endpoint string, jsonpayload *bytes.Buffer) (*http.Response, error) { - return c.Do("PUT", endpoint, jsonpayload, true) + return c.Do("PUT", endpoint, jsonpayload, "application/json") } // PutOnly is just a helper method to do but with a PUT verb and a nil body func (c *Client) PutOnly(endpoint string) (*http.Response, error) { - return c.Do("PUT", endpoint, nil, true) + return c.Do("PUT", endpoint, nil, "application/json") } // Delete is just a helper to Do but with a DELETE verb func (c *Client) Delete(endpoint string) (*http.Response, error) { - return c.Do("DELETE", endpoint, nil, true) + return c.Do("DELETE", endpoint, nil, "application/json") } diff --git a/bitbucket/provider.go b/bitbucket/provider.go index ae1ac408..c63ab95a 100644 --- a/bitbucket/provider.go +++ b/bitbucket/provider.go @@ -70,6 +70,7 @@ func Provider() *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "bitbucket_branch_restriction": resourceBranchRestriction(), "bitbucket_branching_model": resourceBranchingModel(), + "bitbucket_commit_file": resourceCommitFile(), "bitbucket_default_reviewers": resourceDefaultReviewers(), "bitbucket_deploy_key": resourceDeployKey(), "bitbucket_deployment": resourceDeployment(), diff --git a/bitbucket/resource_commit_file.go b/bitbucket/resource_commit_file.go new file mode 100644 index 00000000..0a0565a5 --- /dev/null +++ b/bitbucket/resource_commit_file.go @@ -0,0 +1,160 @@ +package bitbucket + +import ( + "bytes" + "context" + "fmt" + "mime/multipart" + "net/http" + "strings" + + "github.com/DrFaust92/bitbucket-go-client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCommitFile() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceCommitFilePut, + ReadWithoutTimeout: resourceCommitFileRead, + DeleteWithoutTimeout: resourceCommitFileDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "workspace": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "repo_slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "content": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "filename": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "branch": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "commit_message": { + Type: schema.TypeString, + Required: true, + Description: "The SHA of the commit that modified the file", + ForceNew: true, + }, + "commit_author": { + Type: schema.TypeString, + Required: true, + Description: "The SHA of the commit that modified the file", + ForceNew: true, + }, + "commit_sha": { + Type: schema.TypeString, + Computed: true, + Description: "The SHA of the commit that modified the file", + }, + }, + } +} + +func resourceCommitFilePut(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(Clients).httpClient + + repoSlug := d.Get("repo_slug").(string) + workspace := d.Get("workspace").(string) + content := d.Get("content").(string) + filename := d.Get("filename").(string) + branch := d.Get("branch").(string) + commitMessage := d.Get("commit_message").(string) + commitAuthor := d.Get("commit_author").(string) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, _ := writer.CreateFormFile(filename, filename) + _, err := part.Write([]byte(content)) + if err != nil { + return diag.FromErr(err) + } + defer writer.Close() + + messageFormField, err := writer.CreateFormField("message") + if err != nil { + return diag.FromErr(err) + } + _, err = messageFormField.Write([]byte(commitMessage)) + if err != nil { + return diag.FromErr(err) + } + authorFormField, err := writer.CreateFormField("author") + if err != nil { + return diag.FromErr(err) + } + _, err = authorFormField.Write([]byte(commitAuthor)) + if err != nil { + return diag.FromErr(err) + } + + branchFormField, err := writer.CreateFormField("branch") + if err != nil { + return diag.FromErr(err) + } + _, err = branchFormField.Write([]byte(branch)) + if err != nil { + return diag.FromErr(err) + } + + response, err := client.PostWithContentType(fmt.Sprintf("2.0/repositories/%s/%s/src", + workspace, + repoSlug, + ), writer.FormDataContentType(), body) + + if err := handleClientError(err); err != nil { + return diag.FromErr(err) + } + + if response.StatusCode != http.StatusCreated { + return diag.FromErr(fmt.Errorf("")) + } + + d.SetId(string(fmt.Sprintf("%s/%s/%s/%s", workspace, repoSlug, branch, filename))) + + location, _ := response.Location() + splitPath := strings.Split(location.Path, "/") + d.Set("commit_sha", splitPath[len(splitPath)-1]) + + return resourceCommitFileRead(ctx, d, m) +} + +func resourceCommitFileRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(Clients).genClient + sourceApi := c.ApiClient.SourceApi + + repoSlug := d.Get("repo_slug").(string) + workspace := d.Get("workspace").(string) + filename := d.Get("filename").(string) + commit := d.Get("commit_sha").(string) + + _, _, err := sourceApi.RepositoriesWorkspaceRepoSlugSrcCommitPathGet(c.AuthContext, commit, filename, repoSlug, workspace, &bitbucket.SourceApiRepositoriesWorkspaceRepoSlugSrcCommitPathGetOpts{}) + + if err := handleClientError(err); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceCommitFileDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return nil +} diff --git a/bitbucket/resource_commit_file_test.go b/bitbucket/resource_commit_file_test.go new file mode 100644 index 00000000..d02023ad --- /dev/null +++ b/bitbucket/resource_commit_file_test.go @@ -0,0 +1,52 @@ +package bitbucket + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func testAccBitbucketCommitFileConfig(owner, rName string) string { + return fmt.Sprintf(` +resource "bitbucket_repository" "test" { + owner = %[1]q + name = %[2]q +} + +resource "bitbucket_commit_file" "test" { + filename = "README.md" + content = "abc" + repo_slug = bitbucket_repository.test.name + workspace = bitbucket_repository.test.owner + commit_author = "Unit test " + branch = "main" + commit_message = "test" + } +`, owner, rName) +} + +func TestAccBitbucketCommitFile_basic(t *testing.T) { + rName := acctest.RandomWithPrefix("tf-test") + owner := os.Getenv("BITBUCKET_TEAM") + resourceName := "bitbucket_commit_file.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBitbucketDefaultReviewersDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBitbucketCommitFileConfig(owner, rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckBitbucketDefaultReviewersExists(resourceName), + resource.TestCheckResourceAttrPair(resourceName, "repository", "bitbucket_commit_file.test", "name"), + resource.TestCheckResourceAttr(resourceName, "content", "abc"), + resource.TestCheckResourceAttr(resourceName, "branch", "main"), + ), + }, + }, + }) +} diff --git a/docs/resources/commit_file.md b/docs/resources/commit_file.md new file mode 100644 index 00000000..000faaa7 --- /dev/null +++ b/docs/resources/commit_file.md @@ -0,0 +1,41 @@ +--- +layout: "bitbucket" +page_title: "Bitbucket: bitbucket_commit_file" +sidebar_current: "docs-bitbucket-resource-commit-file" +description: |- + Commit a file +--- + +# bitbucket\_commit\_file + +Commit a file. + +This resource allows you to create a commit within a Bitbucket repository. + +OAuth2 Scopes: `repository:write` + +## Example Usage + +```hcl +resource "bitbucket_commit_file" "test" { + filename = "README.md" + content = "abc" + repo_slug = "test" + workspace = "test" + commit_author = "Test " + branch = "main" + commit_message = "test" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `workspace` - (Required) The workspace id. +* `repo_slug` - (Required) The repository slug. +* `filename` - (Required) The path of the file to manage. +* `content` - (Required) The file content. +* `commit_author` - (Required) Committer author to use. +* `branch` - (Required) Git branch. +* `commit_message` - (Required) The message of the commit.