From bd9ddff0cc587aa12afe7a823da8929143f75681 Mon Sep 17 00:00:00 2001 From: Colin Wood Date: Tue, 28 Jun 2016 19:28:49 -0700 Subject: [PATCH] Bitbucket provider for terraform --- builtin/bins/provider-bitbucket/main.go | 12 + builtin/providers/bitbucket/client.go | 65 +++++ builtin/providers/bitbucket/provider.go | 38 +++ .../bitbucket/resource_default_reviewers.go | 114 ++++++++ builtin/providers/bitbucket/resource_hook.go | 206 +++++++++++++++ .../bitbucket/resource_repository.go | 247 ++++++++++++++++++ command/internal_plugin_list.go | 4 +- 7 files changed, 685 insertions(+), 1 deletion(-) create mode 100644 builtin/bins/provider-bitbucket/main.go create mode 100644 builtin/providers/bitbucket/client.go create mode 100644 builtin/providers/bitbucket/provider.go create mode 100644 builtin/providers/bitbucket/resource_default_reviewers.go create mode 100644 builtin/providers/bitbucket/resource_hook.go create mode 100644 builtin/providers/bitbucket/resource_repository.go diff --git a/builtin/bins/provider-bitbucket/main.go b/builtin/bins/provider-bitbucket/main.go new file mode 100644 index 000000000..222d12e4e --- /dev/null +++ b/builtin/bins/provider-bitbucket/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/bitbucket" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: bitbucket.Provider, + }) +} diff --git a/builtin/providers/bitbucket/client.go b/builtin/providers/bitbucket/client.go new file mode 100644 index 000000000..02b9ed0db --- /dev/null +++ b/builtin/providers/bitbucket/client.go @@ -0,0 +1,65 @@ +package bitbucket + +import ( + "bytes" + "net/http" +) + +type BitbucketClient struct { + Username string + Password string +} + +func (c *BitbucketClient) Get(endpoint string) (*http.Response, error) { + client := &http.Client{} + req, err := http.NewRequest("GET", "https://api.bitbucket.org/"+endpoint, nil) + if err != nil { + return nil, err + } + + req.SetBasicAuth(c.Username, c.Password) + return client.Do(req) + +} + +func (c *BitbucketClient) Post(endpoint string, jsonpayload *bytes.Buffer) (*http.Response, error) { + client := &http.Client{} + req, err := http.NewRequest("POST", "https://api.bitbucket.org/"+endpoint, jsonpayload) + if err != nil { + return nil, err + } + req.SetBasicAuth(c.Username, c.Password) + req.Header.Add("content-type", "application/json") + return client.Do(req) +} + +func (c *BitbucketClient) Put(endpoint string, jsonpayload *bytes.Buffer) (*http.Response, error) { + client := &http.Client{} + req, err := http.NewRequest("PUT", "https://api.bitbucket.org/"+endpoint, jsonpayload) + if err != nil { + return nil, err + } + req.SetBasicAuth(c.Username, c.Password) + req.Header.Add("content-type", "application/json") + return client.Do(req) +} + +func (c *BitbucketClient) PutOnly(endpoint string) (*http.Response, error) { + client := &http.Client{} + req, err := http.NewRequest("PUT", "https://api.bitbucket.org/"+endpoint, nil) + if err != nil { + return nil, err + } + req.SetBasicAuth(c.Username, c.Password) + return client.Do(req) +} + +func (c *BitbucketClient) Delete(endpoint string) (*http.Response, error) { + client := &http.Client{} + req, err := http.NewRequest("DELETE", "https://api.bitbucket.org/"+endpoint, nil) + if err != nil { + return nil, err + } + req.SetBasicAuth(c.Username, c.Password) + return client.Do(req) +} diff --git a/builtin/providers/bitbucket/provider.go b/builtin/providers/bitbucket/provider.go new file mode 100644 index 000000000..afeb71249 --- /dev/null +++ b/builtin/providers/bitbucket/provider.go @@ -0,0 +1,38 @@ +package bitbucket + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "username": { + Required: true, + Type: schema.TypeString, + DefaultFunc: schema.EnvDefaultFunc("BITBUCKET_USERNAME", nil), + }, + "password": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("BITBUCKET_PASSWORD", nil), + }, + }, + ConfigureFunc: providerConfigure, + ResourcesMap: map[string]*schema.Resource{ + "bitbucket_hook": resourceHook(), + "bitbucket_default_reviewers": resourceDefaultReviewers(), + "bitbucket_repository": resourceRepository(), + }, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + client := &BitbucketClient{ + Username: d.Get("username").(string), + Password: d.Get("password").(string), + } + + return client, nil +} diff --git a/builtin/providers/bitbucket/resource_default_reviewers.go b/builtin/providers/bitbucket/resource_default_reviewers.go new file mode 100644 index 000000000..c2522b8fd --- /dev/null +++ b/builtin/providers/bitbucket/resource_default_reviewers.go @@ -0,0 +1,114 @@ +package bitbucket + +import ( + "encoding/json" + "fmt" + "github.com/hashicorp/terraform/helper/schema" +) + +type Reviewer struct { + DisplayName string `json:"display_name,omitempty"` + UUID string `json:"uuid,omitempty"` + Username string `json:"username,omitempty"` + Type string `json:"type,omitempty"` +} + +type PaginatedReviewers struct { + Values []Reviewer `json:"values,omitempty"` +} + +func resourceDefaultReviewers() *schema.Resource { + return &schema.Resource{ + Create: resourceDefaultReviewersCreate, + Read: resourceDefaultReviewersRead, + Update: resourceDefaultReviewersUpdate, + Delete: resourceDefaultReviewersDelete, + + Schema: map[string]*schema.Schema{ + "username": { + Type: schema.TypeString, + Required: true, + }, + "repository": { + Type: schema.TypeString, + Required: true, + }, + "reviewers": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Set: schema.HashString, + ForceNew: true, + }, + }, + } +} + +func resourceDefaultReviewersCreate(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketClient) + + for _, user := range d.Get("reviewers").(*schema.Set).List() { + reviewer_resp, err := client.PutOnly(fmt.Sprintf("2.0/repositories/%s/%s/default-reviewers/%s", + d.Get("username").(string), + d.Get("repository").(string), + user, + )) + + if err != nil { + return err + } + + if reviewer_resp.StatusCode != 201 { + return fmt.Errorf("Failed to create reviewer %s got code %d", user.(string), reviewer_resp.StatusCode) + } + + defer reviewer_resp.Body.Close() + } + + d.SetId(fmt.Sprintf("%s/%s/reviewers", d.Get("username").(string), d.Get("repository").(string))) + return resourceDefaultReviewersRead(d, m) +} +func resourceDefaultReviewersRead(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketClient) + + reviewers_response, err := client.Get(fmt.Sprintf("2.0/repositories/%s/%s/default-reviewers", + d.Get("username").(string), + d.Get("repository").(string), + )) + + var reviewers PaginatedReviewers + decoder := json.NewDecoder(reviewers_response.Body) + err = decoder.Decode(&reviewers) + if err != nil { + return err + } + + terraform_reviewers := make([]string, 0, len(reviewers.Values)) + + for _, reviewer := range reviewers.Values { + terraform_reviewers = append(terraform_reviewers, reviewer.Username) + } + + d.Set("reviewers", terraform_reviewers) + + return nil +} +func resourceDefaultReviewersUpdate(d *schema.ResourceData, m interface{}) error { + return nil +} +func resourceDefaultReviewersDelete(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketClient) + + for _, user := range d.Get("reviewers").(*schema.Set).List() { + _, err := client.Delete(fmt.Sprintf("2.0/repositories/%s/%s/default-reviewers/%s", + d.Get("username").(string), + d.Get("repository").(string), + user, + )) + + if err != nil { + return err + } + } + return nil +} diff --git a/builtin/providers/bitbucket/resource_hook.go b/builtin/providers/bitbucket/resource_hook.go new file mode 100644 index 000000000..d1f439782 --- /dev/null +++ b/builtin/providers/bitbucket/resource_hook.go @@ -0,0 +1,206 @@ +package bitbucket + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/hashicorp/terraform/helper/schema" +) + +type Hook struct { + Uuid string `json:"uuid,omitempty"` + Url string `json:"url,omitempty"` + Description string `json:"description,omitempty"` + Active bool `json:"active,omitempty"` + Events []string `json:"events,omitempty"` +} + +func resourceHook() *schema.Resource { + return &schema.Resource{ + Create: resourceHookCreate, + Read: resourceHookRead, + Update: resourceHookUpdate, + Delete: resourceHookDelete, + Exists: resourceHookExists, + + Schema: map[string]*schema.Schema{ + "username": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "repository": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "active": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "url": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "uuid": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "events": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func createHook(d *schema.ResourceData) *Hook { + + events := make([]string, 0, len(d.Get("events").(*schema.Set).List())) + + for _, item := range d.Get("events").(*schema.Set).List() { + events = append(events, item.(string)) + } + + return &Hook{ + Url: d.Get("url").(string), + Description: d.Get("description").(string), + Active: d.Get("active").(bool), + Events: events, + } +} + +func resourceHookCreate(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketClient) + hook := createHook(d) + + var jsonbuffer []byte + + jsonpayload := bytes.NewBuffer(jsonbuffer) + enc := json.NewEncoder(jsonpayload) + enc.Encode(hook) + + hook_req, err := client.Post(fmt.Sprintf("2.0/repositories/%s/%s/hooks", + d.Get("username").(string), + d.Get("repository").(string), + ), jsonpayload) + + decoder := json.NewDecoder(hook_req.Body) + err = decoder.Decode(&hook) + if err != nil { + return err + } + + d.SetId(string(hook.Uuid)) + d.Set("uuid", string(hook.Uuid)) + + return resourceHookRead(d, m) +} +func resourceHookRead(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketClient) + hook_req, err := client.Get(fmt.Sprintf("2.0/repositories/%s/%s/hooks/%s", + d.Get("username").(string), + d.Get("repository").(string), + d.Get("uuid").(string), + )) + + if err != nil { + return err + } + + var hook Hook + + decoder := json.NewDecoder(hook_req.Body) + err = decoder.Decode(&hook) + if err != nil { + return err + } + + d.Set("uuid", string(hook.Uuid)) + d.Set("description", string(hook.Description)) + d.Set("active", bool(hook.Active)) + d.Set("url", string(hook.Url)) + + eventsList := make([]string, 0, len(hook.Events)) + + for _, event := range hook.Events { + eventsList = append(eventsList, string(event)) + } + + d.Set("events", eventsList) + + return nil +} + +func resourceHookUpdate(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketClient) + hook := createHook(d) + + var jsonbuffer []byte + + jsonpayload := bytes.NewBuffer(jsonbuffer) + enc := json.NewEncoder(jsonpayload) + enc.Encode(hook) + + hook_req, err := client.Put(fmt.Sprintf("2.0/repositories/%s/%s/hooks/%s", + d.Get("username").(string), + d.Get("repository").(string), + d.Get("uuid").(string), + ), jsonpayload) + + if err != nil { + return err + } + + decoder := json.NewDecoder(hook_req.Body) + err = decoder.Decode(&hook) + if err != nil { + return err + } + + return resourceHookRead(d, m) +} + +func resourceHookExists(d *schema.ResourceData, m interface{}) (bool, error) { + client := m.(*BitbucketClient) + if _, okay := d.GetOk("uuid"); okay { + hook_req, err := client.Get(fmt.Sprintf("2.0/repositories/%s/%s/hooks/%s", + d.Get("username").(string), + d.Get("repository").(string), + d.Get("uuid").(string), + )) + + if err != nil { + panic(err) + } + + if hook_req.StatusCode != 200 { + d.SetId("") + return false, nil + } + + return true, nil + } else { + return false, nil + } + +} + +func resourceHookDelete(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketClient) + _, err := client.Delete(fmt.Sprintf("2.0/repositories/%s/%s/hooks/%s", + d.Get("username").(string), + d.Get("repository").(string), + d.Get("uuid").(string), + )) + + if err != nil { + return err + } + return nil +} diff --git a/builtin/providers/bitbucket/resource_repository.go b/builtin/providers/bitbucket/resource_repository.go new file mode 100644 index 000000000..13815d6fd --- /dev/null +++ b/builtin/providers/bitbucket/resource_repository.go @@ -0,0 +1,247 @@ +package bitbucket + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "log" +) + +type CloneUrl struct { + Href string `json:"href,omitempty"` + Name string `json:"name,omitempty"` +} + +type Repository struct { + SCM string `json:"scm,omitempty"` + HasWiki bool `json:"has_wiki,omitempty"` + HasIssues bool `json:"has_issues,omitempty"` + Website string `json:"website,omitempty"` + IsPrivate bool `json:"is_private,omitempty"` + ForkPolicy string `json:"fork_policy,omitempty"` + Language string `json:"language,omitempty"` + Description string `json:"description,omitempty"` + Name string `json:"name,omitempty"` + UUID string `json:"uuid,omitempty"` + Project struct { + Key string `json:"key,omitempty"` + } `json:"project,omitempty"` + Links struct { + Clone []CloneUrl `json:"clone,omitempty"` + } `json:"links,omitempty"` +} + +func resourceRepository() *schema.Resource { + return &schema.Resource{ + Create: resourceRepositoryCreate, + Update: resourceRepositoryUpdate, + Read: resourceRepositoryRead, + Delete: resourceRepositoryDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "scm": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "git", + }, + "has_wiki": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "has_issues": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "website": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "clone_ssh": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "clone_https": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "project_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "is_private": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "fork_policy": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "allow_forks", + }, + "language": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "owner": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func newRepositoryFromResource(d *schema.ResourceData) *Repository { + repo := &Repository{ + Name: d.Get("name").(string), + Language: d.Get("language").(string), + IsPrivate: d.Get("is_private").(bool), + Description: d.Get("description").(string), + ForkPolicy: d.Get("fork_policy").(string), + HasWiki: d.Get("has_wiki").(bool), + HasIssues: d.Get("has_issues").(bool), + SCM: d.Get("scm").(string), + Website: d.Get("website").(string), + } + + repo.Project.Key = d.Get("project_key").(string) + return repo +} + +func resourceRepositoryUpdate(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketClient) + repository := newRepositoryFromResource(d) + + var jsonbuffer []byte + + jsonpayload := bytes.NewBuffer(jsonbuffer) + enc := json.NewEncoder(jsonpayload) + enc.Encode(repository) + + repository_response, err := client.Put(fmt.Sprintf("2.0/repositories/%s/%s", + d.Get("owner").(string), + d.Get("name").(string), + ), jsonpayload) + + if err != nil { + return err + } + + if repository_response.StatusCode == 200 { + decoder := json.NewDecoder(repository_response.Body) + err = decoder.Decode(&repository) + if err != nil { + return err + } + } else { + return fmt.Errorf("Failed to put: %d", repository_response.StatusCode) + } + + return resourceRepositoryRead(d, m) +} + +func resourceRepositoryCreate(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketClient) + repo := newRepositoryFromResource(d) + + var jsonbuffer []byte + + jsonpayload := bytes.NewBuffer(jsonbuffer) + enc := json.NewEncoder(jsonpayload) + enc.Encode(repo) + + log.Printf("Sending %s \n", jsonpayload) + + repo_req, err := client.Post(fmt.Sprintf("2.0/repositories/%s/%s", + d.Get("owner").(string), + d.Get("name").(string), + ), jsonpayload) + + decoder := json.NewDecoder(repo_req.Body) + err = decoder.Decode(&repo) + if err != nil { + return err + } + + log.Printf("Received %s \n", repo_req.Body) + + if repo_req.StatusCode != 200 { + return fmt.Errorf("Failed to create repository got status code %d", repo_req.StatusCode) + } + + d.SetId(string(fmt.Sprintf("%s/%s", d.Get("owner").(string), d.Get("name").(string)))) + + return resourceRepositoryRead(d, m) +} +func resourceRepositoryRead(d *schema.ResourceData, m interface{}) error { + + client := m.(*BitbucketClient) + repo_req, err := client.Get(fmt.Sprintf("2.0/repositories/%s/%s", + d.Get("owner").(string), + d.Get("name").(string), + )) + + if err != nil { + return err + } + + var repo Repository + + decoder := json.NewDecoder(repo_req.Body) + err = decoder.Decode(&repo) + if err != nil { + return err + } + + d.Set("scm", string(repo.SCM)) + d.Set("is_private", bool(repo.IsPrivate)) + d.Set("has_wiki", bool(repo.HasWiki)) + d.Set("has_issues", bool(repo.HasIssues)) + d.Set("name", string(repo.Name)) + d.Set("language", string(repo.Language)) + d.Set("fork_policy", string(repo.ForkPolicy)) + d.Set("website", string(repo.Website)) + d.Set("description", string(repo.Description)) + d.Set("project_key", string(repo.Project.Key)) + + for _, clone_url := range repo.Links.Clone { + if clone_url.Name == "https" { + d.Set("clone_https", string(clone_url.Href)) + } else { + d.Set("clone_ssh", string(clone_url.Href)) + } + } + + return nil +} + +func resourceRepositoryDelete(d *schema.ResourceData, m interface{}) error { + client := m.(*BitbucketClient) + delete_response, err := client.Delete(fmt.Sprintf("2.0/repositories/%s/%s", + d.Get("owner").(string), + d.Get("name").(string), + )) + + if err != nil { + return err + } + + if delete_response.StatusCode != 204 { + return fmt.Errorf("Failed to delete the repository got status code %d", delete_response.StatusCode) + } + + return nil +} diff --git a/command/internal_plugin_list.go b/command/internal_plugin_list.go index e4807b799..d3e4f6b87 100644 --- a/command/internal_plugin_list.go +++ b/command/internal_plugin_list.go @@ -11,6 +11,7 @@ import ( awsprovider "github.com/hashicorp/terraform/builtin/providers/aws" azureprovider "github.com/hashicorp/terraform/builtin/providers/azure" azurermprovider "github.com/hashicorp/terraform/builtin/providers/azurerm" + bitbucketprovider "github.com/hashicorp/terraform/builtin/providers/bitbucket" chefprovider "github.com/hashicorp/terraform/builtin/providers/chef" clcprovider "github.com/hashicorp/terraform/builtin/providers/clc" cloudflareprovider "github.com/hashicorp/terraform/builtin/providers/cloudflare" @@ -61,10 +62,12 @@ import ( ) var InternalProviders = map[string]plugin.ProviderFunc{ + "archive": archiveprovider.Provider, "atlas": atlasprovider.Provider, "aws": awsprovider.Provider, "azure": azureprovider.Provider, "azurerm": azurermprovider.Provider, + "bitbucket": bitbucketprovider.Provider, "chef": chefprovider.Provider, "clc": clcprovider.Provider, "cloudflare": cloudflareprovider.Provider, @@ -105,7 +108,6 @@ var InternalProviders = map[string]plugin.ProviderFunc{ "ultradns": ultradnsprovider.Provider, "vcd": vcdprovider.Provider, "vsphere": vsphereprovider.Provider, - "archive": archiveprovider.Provider, } var InternalProvisioners = map[string]plugin.ProvisionerFunc{