From c1b373ad5f6cbdf1eb40e11278cec5002864ff69 Mon Sep 17 00:00:00 2001 From: Jacob Severson Date: Tue, 8 Mar 2016 23:05:22 +0100 Subject: [PATCH] Add Github Organization provider. Allows for managing organization membership, teams, team membership, and team repositories. --- .gitignore | 1 + builtin/bins/provider-github/main.go | 12 ++ builtin/bins/provider-github/main_test.go | 1 + builtin/providers/github/config.go | 29 ++++ builtin/providers/github/provider.go | 56 +++++++ builtin/providers/github/provider_test.go | 38 +++++ .../github/resource_github_membership.go | 85 ++++++++++ .../github/resource_github_membership_test.go | 113 +++++++++++++ .../providers/github/resource_github_team.go | 90 ++++++++++ .../github/resource_github_team_membership.go | 100 ++++++++++++ .../resource_github_team_membership_test.go | 152 +++++++++++++++++ .../github/resource_github_team_repository.go | 129 +++++++++++++++ .../resource_github_team_repository_test.go | 154 ++++++++++++++++++ .../github/resource_github_team_test.go | 108 ++++++++++++ builtin/providers/github/util.go | 47 ++++++ builtin/providers/github/util_test.go | 55 +++++++ website/source/assets/stylesheets/_docs.scss | 1 + .../docs/providers/github/index.html.markdown | 42 +++++ .../github/r/membership.html.markdown | 33 ++++ .../providers/github/r/team.html.markdown | 37 +++++ .../github/r/team_membership.html.markdown | 46 ++++++ .../github/r/team_repository.html.markdown | 39 +++++ website/source/layouts/docs.erb | 14 +- website/source/layouts/github.erb | 35 ++++ 24 files changed, 1412 insertions(+), 5 deletions(-) create mode 100644 builtin/bins/provider-github/main.go create mode 100644 builtin/bins/provider-github/main_test.go create mode 100644 builtin/providers/github/config.go create mode 100644 builtin/providers/github/provider.go create mode 100644 builtin/providers/github/provider_test.go create mode 100644 builtin/providers/github/resource_github_membership.go create mode 100644 builtin/providers/github/resource_github_membership_test.go create mode 100644 builtin/providers/github/resource_github_team.go create mode 100644 builtin/providers/github/resource_github_team_membership.go create mode 100644 builtin/providers/github/resource_github_team_membership_test.go create mode 100644 builtin/providers/github/resource_github_team_repository.go create mode 100644 builtin/providers/github/resource_github_team_repository_test.go create mode 100644 builtin/providers/github/resource_github_team_test.go create mode 100644 builtin/providers/github/util.go create mode 100644 builtin/providers/github/util_test.go create mode 100644 website/source/docs/providers/github/index.html.markdown create mode 100644 website/source/docs/providers/github/r/membership.html.markdown create mode 100644 website/source/docs/providers/github/r/team.html.markdown create mode 100644 website/source/docs/providers/github/r/team_membership.html.markdown create mode 100644 website/source/docs/providers/github/r/team_repository.html.markdown create mode 100644 website/source/layouts/github.erb diff --git a/.gitignore b/.gitignore index 749856427..911732441 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,6 @@ website/node_modules *~ .*.swp .idea +*.iml *.test *.iml diff --git a/builtin/bins/provider-github/main.go b/builtin/bins/provider-github/main.go new file mode 100644 index 000000000..bb8876d25 --- /dev/null +++ b/builtin/bins/provider-github/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/github" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: github.Provider, + }) +} diff --git a/builtin/bins/provider-github/main_test.go b/builtin/bins/provider-github/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-github/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/github/config.go b/builtin/providers/github/config.go new file mode 100644 index 000000000..76c1bb865 --- /dev/null +++ b/builtin/providers/github/config.go @@ -0,0 +1,29 @@ +package github + +import ( + "github.com/google/go-github/github" + "golang.org/x/oauth2" +) + +type Config struct { + Token string + Organization string +} + +type Organization struct { + name string + client *github.Client +} + +// Client configures and returns a fully initialized GithubClient +func (c *Config) Client() (interface{}, error) { + var org Organization + org.name = c.Organization + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: c.Token}, + ) + tc := oauth2.NewClient(oauth2.NoContext, ts) + + org.client = github.NewClient(tc) + return &org, nil +} diff --git a/builtin/providers/github/provider.go b/builtin/providers/github/provider.go new file mode 100644 index 000000000..47c7c4766 --- /dev/null +++ b/builtin/providers/github/provider.go @@ -0,0 +1,56 @@ +package github + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + + // The actual provider + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "token": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("GITHUB_TOKEN", nil), + Description: descriptions["token"], + }, + "organization": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("GITHUB_ORGANIZATION", nil), + Description: descriptions["organization"], + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "github_team": resourceGithubTeam(), + "github_team_membership": resourceGithubTeamMembership(), + "github_team_repository": resourceGithubTeamRepository(), + "github_membership": resourceGithubMembership(), + }, + + ConfigureFunc: providerConfigure, + } +} + +var descriptions map[string]string + +func init() { + descriptions = map[string]string{ + "token": "The OAuth token used to connect to GitHub.", + + "organization": "The GitHub organization name to manage.", + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := Config{ + Token: d.Get("token").(string), + Organization: d.Get("organization").(string), + } + + return config.Client() +} diff --git a/builtin/providers/github/provider_test.go b/builtin/providers/github/provider_test.go new file mode 100644 index 000000000..b7e7be59e --- /dev/null +++ b/builtin/providers/github/provider_test.go @@ -0,0 +1,38 @@ +package github + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "github": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("GITHUB_TOKEN"); v == "" { + t.Fatal("GITHUB_TOKEN must be set for acceptance tests") + } + if v := os.Getenv("GITHUB_ORGANIZATION"); v == "" { + t.Fatal("GITHUB_ORGANIZATION must be set for acceptance tests") + } +} diff --git a/builtin/providers/github/resource_github_membership.go b/builtin/providers/github/resource_github_membership.go new file mode 100644 index 000000000..2c0ec27f2 --- /dev/null +++ b/builtin/providers/github/resource_github_membership.go @@ -0,0 +1,85 @@ +package github + +import ( + "github.com/google/go-github/github" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceGithubMembership() *schema.Resource { + + return &schema.Resource{ + Create: resourceGithubMembershipCreate, + Read: resourceGithubMembershipRead, + Update: resourceGithubMembershipUpdate, + Delete: resourceGithubMembershipDelete, + + Schema: map[string]*schema.Schema{ + "username": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "role": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateRoleValueFunc([]string{"member", "admin"}), + Default: "member", + }, + }, + } +} + +func resourceGithubMembershipCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + n := d.Get("username").(string) + r := d.Get("role").(string) + + membership, _, err := client.Organizations.EditOrgMembership(n, meta.(*Organization).name, + &github.Membership{Role: &r}) + if err != nil { + return err + } + + d.SetId(buildTwoPartID(membership.Organization.Login, membership.User.Login)) + + return resourceGithubMembershipRead(d, meta) +} + +func resourceGithubMembershipRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + + membership, _, err := client.Organizations.GetOrgMembership(d.Get("username").(string), meta.(*Organization).name) + if err != nil { + d.SetId("") + return nil + } + username := membership.User.Login + roleName := membership.Role + + d.Set("username", *username) + d.Set("role", *roleName) + return nil +} + +func resourceGithubMembershipUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + n := d.Get("username").(string) + r := d.Get("role").(string) + + _, _, err := client.Organizations.EditOrgMembership(n, meta.(*Organization).name, &github.Membership{ + Role: &r, + }) + if err != nil { + return err + } + return nil +} + +func resourceGithubMembershipDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + n := d.Get("username").(string) + + _, err := client.Organizations.RemoveOrgMembership(n, meta.(*Organization).name) + + return err +} diff --git a/builtin/providers/github/resource_github_membership_test.go b/builtin/providers/github/resource_github_membership_test.go new file mode 100644 index 000000000..0255ed925 --- /dev/null +++ b/builtin/providers/github/resource_github_membership_test.go @@ -0,0 +1,113 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/google/go-github/github" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccGithubMembership_basic(t *testing.T) { + var membership github.Membership + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGithubMembershipDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccGithubMembershipConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubMembershipExists("github_membership.test_org_membership", &membership), + testAccCheckGithubMembershipRoleState("github_membership.test_org_membership", &membership), + ), + }, + }, + }) +} + +func testAccCheckGithubMembershipDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*Organization).client + + for _, rs := range s.RootModule().Resources { + if rs.Type != "github_membership" { + continue + } + o, u := parseTwoPartID(rs.Primary.ID) + + membership, resp, err := conn.Organizations.GetOrgMembership(u, o) + + if err == nil { + if membership != nil && + buildTwoPartID(membership.Organization.Login, membership.User.Login) == rs.Primary.ID { + return fmt.Errorf("Organization membership still exists") + } + } + if resp.StatusCode != 404 { + return err + } + return nil + } + return nil +} + +func testAccCheckGithubMembershipExists(n string, membership *github.Membership) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not Found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No membership ID is set") + } + + conn := testAccProvider.Meta().(*Organization).client + o, u := parseTwoPartID(rs.Primary.ID) + + githubMembership, _, err := conn.Organizations.GetOrgMembership(u, o) + if err != nil { + return err + } + *membership = *githubMembership + return nil + } +} + +func testAccCheckGithubMembershipRoleState(n string, membership *github.Membership) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not Found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No membership ID is set") + } + + conn := testAccProvider.Meta().(*Organization).client + o, u := parseTwoPartID(rs.Primary.ID) + + githubMembership, _, err := conn.Organizations.GetOrgMembership(u, o) + if err != nil { + return err + } + + resourceRole := membership.Role + actualRole := githubMembership.Role + + if *resourceRole != *actualRole { + return fmt.Errorf("Membership role %v in resource does match actual state of %v", *resourceRole, *actualRole) + } + return nil + } +} + +const testAccGithubMembershipConfig = ` +resource "github_membership" "test_org_membership" { + username = "TerraformDummyUser" + role = "member" +} +` diff --git a/builtin/providers/github/resource_github_team.go b/builtin/providers/github/resource_github_team.go new file mode 100644 index 000000000..b38f8d256 --- /dev/null +++ b/builtin/providers/github/resource_github_team.go @@ -0,0 +1,90 @@ +package github + +import ( + "github.com/google/go-github/github" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceGithubTeam() *schema.Resource { + + return &schema.Resource{ + Create: resourceGithubTeamCreate, + Read: resourceGithubTeamRead, + Update: resourceGithubTeamUpdate, + Delete: resourceGithubTeamDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceGithubTeamCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + n := d.Get("name").(string) + desc := d.Get("description").(string) + githubTeam, _, err := client.Organizations.CreateTeam(meta.(*Organization).name, &github.Team{ + Name: &n, + Description: &desc, + }) + if err != nil { + return err + } + d.SetId(fromGithubID(githubTeam.ID)) + return resourceGithubTeamRead(d, meta) +} + +func resourceGithubTeamRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + + team, err := getGithubTeam(d, client) + if err != nil { + d.SetId("") + return nil + } + d.Set("description", team.Description) + d.Set("name", team.Name) + return nil +} + +func resourceGithubTeamUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + team, err := getGithubTeam(d, client) + + if err != nil { + d.SetId("") + return nil + } + + name := d.Get("name").(string) + description := d.Get("description").(string) + team.Description = &description + team.Name = &name + + team, _, err = client.Organizations.EditTeam(*team.ID, team) + if err != nil { + return err + } + d.SetId(fromGithubID(team.ID)) + return resourceGithubTeamRead(d, meta) +} + +func resourceGithubTeamDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + id := toGithubID(d.Id()) + _, err := client.Organizations.DeleteTeam(id) + return err +} + +func getGithubTeam(d *schema.ResourceData, github *github.Client) (*github.Team, error) { + id := toGithubID(d.Id()) + team, _, err := github.Organizations.GetTeam(id) + return team, err +} diff --git a/builtin/providers/github/resource_github_team_membership.go b/builtin/providers/github/resource_github_team_membership.go new file mode 100644 index 000000000..6dadfc6f5 --- /dev/null +++ b/builtin/providers/github/resource_github_team_membership.go @@ -0,0 +1,100 @@ +package github + +import ( + "strings" + + "github.com/google/go-github/github" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceGithubTeamMembership() *schema.Resource { + + return &schema.Resource{ + Create: resourceGithubTeamMembershipCreate, + Read: resourceGithubTeamMembershipRead, + // editing team memberships are not supported by github api so forcing new on any changes + Delete: resourceGithubTeamMembershipDelete, + + Schema: map[string]*schema.Schema{ + "team_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "username": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "role": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "member", + ValidateFunc: validateRoleValueFunc([]string{"member", "maintainer"}), + }, + }, + } +} + +func resourceGithubTeamMembershipCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + t := d.Get("team_id").(string) + n := d.Get("username").(string) + r := d.Get("role").(string) + + _, _, err := client.Organizations.AddTeamMembership(toGithubID(t), n, + &github.OrganizationAddTeamMembershipOptions{Role: r}) + + if err != nil { + return err + } + + d.SetId(buildTwoPartID(&t, &n)) + + return resourceGithubTeamMembershipRead(d, meta) +} + +func resourceGithubTeamMembershipRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + t := d.Get("team_id").(string) + n := d.Get("username").(string) + + membership, _, err := client.Organizations.GetTeamMembership(toGithubID(t), n) + + if err != nil { + d.SetId("") + return nil + } + team, user := getTeamAndUserFromURL(membership.URL) + + d.Set("username", user) + d.Set("role", membership.Role) + d.Set("team_id", team) + return nil +} + +func resourceGithubTeamMembershipDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + t := d.Get("team_id").(string) + n := d.Get("username").(string) + + _, err := client.Organizations.RemoveTeamMembership(toGithubID(t), n) + + return err +} + +func getTeamAndUserFromURL(url *string) (string, string) { + var team, user string + + urlSlice := strings.Split(*url, "/") + for v := range urlSlice { + if urlSlice[v] == "teams" { + team = urlSlice[v+1] + } + if urlSlice[v] == "memberships" { + user = urlSlice[v+1] + } + } + return team, user +} diff --git a/builtin/providers/github/resource_github_team_membership_test.go b/builtin/providers/github/resource_github_team_membership_test.go new file mode 100644 index 000000000..39a74b8be --- /dev/null +++ b/builtin/providers/github/resource_github_team_membership_test.go @@ -0,0 +1,152 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/google/go-github/github" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccGithubTeamMembership_basic(t *testing.T) { + var membership github.Membership + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGithubTeamMembershipDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccGithubTeamMembershipConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubTeamMembershipExists("github_team_membership.test_team_membership", &membership), + testAccCheckGithubTeamMembershipRoleState("github_team_membership.test_team_membership", "member", &membership), + ), + }, + resource.TestStep{ + Config: testAccGithubTeamMembershipUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubTeamMembershipExists("github_team_membership.test_team_membership", &membership), + testAccCheckGithubTeamMembershipRoleState("github_team_membership.test_team_membership", "maintainer", &membership), + ), + }, + }, + }) +} + +func testAccCheckGithubTeamMembershipDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*Organization).client + + for _, rs := range s.RootModule().Resources { + if rs.Type != "github_team_membership" { + continue + } + + t, u := parseTwoPartID(rs.Primary.ID) + membership, resp, err := conn.Organizations.GetTeamMembership(toGithubID(t), u) + if err == nil { + if membership != nil { + return fmt.Errorf("Team membership still exists") + } + } + if resp.StatusCode != 404 { + return err + } + return nil + } + return nil +} + +func testAccCheckGithubTeamMembershipExists(n string, membership *github.Membership) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not Found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No team membership ID is set") + } + + conn := testAccProvider.Meta().(*Organization).client + t, u := parseTwoPartID(rs.Primary.ID) + + teamMembership, _, err := conn.Organizations.GetTeamMembership(toGithubID(t), u) + + if err != nil { + return err + } + *membership = *teamMembership + return nil + } +} + +func testAccCheckGithubTeamMembershipRoleState(n, expected string, membership *github.Membership) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not Found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No team membership ID is set") + } + + conn := testAccProvider.Meta().(*Organization).client + t, u := parseTwoPartID(rs.Primary.ID) + + teamMembership, _, err := conn.Organizations.GetTeamMembership(toGithubID(t), u) + if err != nil { + return err + } + + resourceRole := membership.Role + actualRole := teamMembership.Role + + if *resourceRole != expected { + return fmt.Errorf("Team membership role %v in resource does match expected state of %v", *resourceRole, expected) + } + + if *resourceRole != *actualRole { + return fmt.Errorf("Team membership role %v in resource does match actual state of %v", *resourceRole, *actualRole) + } + return nil + } +} + +const testAccGithubTeamMembershipConfig = ` +resource "github_membership" "test_org_membership" { + username = "TerraformDummyUser" + role = "member" +} + +resource "github_team" "test_team" { + name = "foo" + description = "Terraform acc test group" +} + +resource "github_team_membership" "test_team_membership" { + team_id = "${github_team.test_team.id}" + username = "TerraformDummyUser" + role = "member" +} +` + +const testAccGithubTeamMembershipUpdateConfig = ` +resource "github_membership" "test_org_membership" { + username = "TerraformDummyUser" + role = "member" +} + +resource "github_team" "test_team" { + name = "foo" + description = "Terraform acc test group" +} + +resource "github_team_membership" "test_team_membership" { + team_id = "${github_team.test_team.id}" + username = "TerraformDummyUser" + role = "maintainer" +} +` diff --git a/builtin/providers/github/resource_github_team_repository.go b/builtin/providers/github/resource_github_team_repository.go new file mode 100644 index 000000000..d4ef1ed08 --- /dev/null +++ b/builtin/providers/github/resource_github_team_repository.go @@ -0,0 +1,129 @@ +package github + +import ( + "errors" + + "github.com/google/go-github/github" + "github.com/hashicorp/terraform/helper/schema" +) + +const pullPermission string = "pull" +const pushPermission string = "push" +const adminPermission string = "admin" + +func resourceGithubTeamRepository() *schema.Resource { + return &schema.Resource{ + Create: resourceGithubTeamRepositoryCreate, + Read: resourceGithubTeamRepositoryRead, + Update: resourceGithubTeamRepositoryUpdate, + Delete: resourceGithubTeamRepositoryDelete, + + Schema: map[string]*schema.Schema{ + "team_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "repository": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "permission": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "pull", + ValidateFunc: validateRoleValueFunc([]string{"pull", "push", "admin"}), + }, + }, + } +} + +func resourceGithubTeamRepositoryCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + t := d.Get("team_id").(string) + r := d.Get("repository").(string) + p := d.Get("permission").(string) + + _, err := client.Organizations.AddTeamRepo(toGithubID(t), meta.(*Organization).name, r, + &github.OrganizationAddTeamRepoOptions{Permission: p}) + + if err != nil { + return err + } + + d.SetId(buildTwoPartID(&t, &r)) + + return resourceGithubTeamRepositoryRead(d, meta) +} + +func resourceGithubTeamRepositoryRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + t := d.Get("team_id").(string) + r := d.Get("repository").(string) + + repo, _, repoErr := client.Organizations.IsTeamRepo(toGithubID(t), meta.(*Organization).name, r) + + if repoErr != nil { + d.SetId("") + return nil + } + + repositoryName := repo.Name + + d.Set("team_id", t) + d.Set("repository", repositoryName) + + permName, permErr := getRepoPermission(repo.Permissions) + + if permErr != nil { + return permErr + } + + d.Set("permission", permName) + + return nil +} + +func resourceGithubTeamRepositoryUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + t := d.Get("team_id").(string) + r := d.Get("repository").(string) + p := d.Get("permission").(string) + + // the go-github library's AddTeamRepo method uses the add/update endpoint from Github API + _, err := client.Organizations.AddTeamRepo(toGithubID(t), meta.(*Organization).name, r, + &github.OrganizationAddTeamRepoOptions{Permission: p}) + + if err != nil { + return err + } + return resourceGithubTeamRepositoryRead(d, meta) +} + +func resourceGithubTeamRepositoryDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Organization).client + t := d.Get("team_id").(string) + r := d.Get("repository").(string) + + _, err := client.Organizations.RemoveTeamRepo(toGithubID(t), meta.(*Organization).name, r) + + return err +} + +func getRepoPermission(p *map[string]bool) (string, error) { + + // Permissions are returned in this map format such that if you have a certain level + // of permission, all levels below are also true. For example, if a team has push + // permission, the map will be: {"pull": true, "push": true, "admin": false} + if (*p)[adminPermission] { + return adminPermission, nil + } else if (*p)[pushPermission] { + return pushPermission, nil + } else { + if (*p)[pullPermission] { + return pullPermission, nil + } + return "", errors.New("At least one permission expected from permissions map.") + } +} diff --git a/builtin/providers/github/resource_github_team_repository_test.go b/builtin/providers/github/resource_github_team_repository_test.go new file mode 100644 index 000000000..c7a9b2c20 --- /dev/null +++ b/builtin/providers/github/resource_github_team_repository_test.go @@ -0,0 +1,154 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/google/go-github/github" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccGithubTeamRepository_basic(t *testing.T) { + var repository github.Repository + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGithubTeamRepositoryDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccGithubTeamRepositoryConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubTeamRepositoryExists("github_team_repository.test_team_test_repo", &repository), + testAccCheckGithubTeamRepositoryRoleState("pull", &repository), + ), + }, + resource.TestStep{ + Config: testAccGithubTeamRepositoryUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubTeamRepositoryExists("github_team_repository.test_team_test_repo", &repository), + testAccCheckGithubTeamRepositoryRoleState("push", &repository), + ), + }, + }, + }) +} + +func TestAccCheckGetPermissions(t *testing.T) { + pullMap := map[string]bool{"pull": true, "push": false, "admin": false} + pushMap := map[string]bool{"pull": true, "push": true, "admin": false} + adminMap := map[string]bool{"pull": true, "push": true, "admin": true} + errorMap := map[string]bool{"pull": false, "push": false, "admin": false} + + pull, _ := getRepoPermission(&pullMap) + if pull != "pull" { + t.Fatalf("Expected pull permission, actual: %s", pull) + } + + push, _ := getRepoPermission(&pushMap) + if push != "push" { + t.Fatalf("Expected push permission, actual: %s", push) + } + + admin, _ := getRepoPermission(&adminMap) + if admin != "admin" { + t.Fatalf("Expected admin permission, actual: %s", admin) + } + + errPerm, err := getRepoPermission(&errorMap) + if err == nil { + t.Fatalf("Expected an error getting permissions, actual: %v", errPerm) + } +} + +func testAccCheckGithubTeamRepositoryRoleState(role string, repository *github.Repository) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceRole, err := getRepoPermission(repository.Permissions) + if err != nil { + return err + } + + if resourceRole != role { + return fmt.Errorf("Team repository role %v in resource does match expected state of %v", resourceRole, role) + } + return nil + } +} + +func testAccCheckGithubTeamRepositoryExists(n string, repository *github.Repository) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not Found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No team repository ID is set") + } + + conn := testAccProvider.Meta().(*Organization).client + t, r := parseTwoPartID(rs.Primary.ID) + + repo, _, err := conn.Organizations.IsTeamRepo(toGithubID(t), + testAccProvider.Meta().(*Organization).name, r) + + if err != nil { + return err + } + *repository = *repo + return nil + } +} + +func testAccCheckGithubTeamRepositoryDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*Organization).client + + for _, rs := range s.RootModule().Resources { + if rs.Type != "github_team_repository" { + continue + } + t, r := parseTwoPartID(rs.Primary.ID) + + repo, resp, err := conn.Organizations.IsTeamRepo(toGithubID(t), + testAccProvider.Meta().(*Organization).name, r) + + if err == nil { + if repo != nil && + buildTwoPartID(&t, repo.Name) == rs.Primary.ID { + return fmt.Errorf("Team repository still exists") + } + } + if resp.StatusCode != 404 { + return err + } + return nil + } + return nil +} + +const testAccGithubTeamRepositoryConfig = ` +resource "github_team" "test_team" { + name = "foo" + description = "Terraform acc test group" +} + +resource "github_team_repository" "test_team_test_repo" { + team_id = "${github_team.test_team.id}" + repository = "test-repo" + permission = "pull" +} +` + +const testAccGithubTeamRepositoryUpdateConfig = ` +resource "github_team" "test_team" { + name = "foo" + description = "Terraform acc test group" +} + +resource "github_team_repository" "test_team_test_repo" { + team_id = "${github_team.test_team.id}" + repository = "test-repo" + permission = "push" +} +` diff --git a/builtin/providers/github/resource_github_team_test.go b/builtin/providers/github/resource_github_team_test.go new file mode 100644 index 000000000..e5429fe63 --- /dev/null +++ b/builtin/providers/github/resource_github_team_test.go @@ -0,0 +1,108 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/google/go-github/github" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccGithubTeam_basic(t *testing.T) { + var team github.Team + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGithubTeamDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccGithubTeamConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubTeamExists("github_team.foo", &team), + testAccCheckGithubTeamAttributes(&team, "foo", "Terraform acc test group"), + ), + }, + resource.TestStep{ + Config: testAccGithubTeamUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckGithubTeamExists("github_team.foo", &team), + testAccCheckGithubTeamAttributes(&team, "foo2", "Terraform acc test group - updated"), + ), + }, + }, + }) +} + +func testAccCheckGithubTeamExists(n string, team *github.Team) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not Found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Team ID is set") + } + + conn := testAccProvider.Meta().(*Organization).client + githubTeam, _, err := conn.Organizations.GetTeam(toGithubID(rs.Primary.ID)) + if err != nil { + return err + } + *team = *githubTeam + return nil + } +} + +func testAccCheckGithubTeamAttributes(team *github.Team, name, description string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *team.Name != name { + return fmt.Errorf("Team name does not match: %s, %s", *team.Name, name) + } + + if *team.Description != description { + return fmt.Errorf("Team description does not match: %s, %s", *team.Description, description) + } + + return nil + } +} + +func testAccCheckGithubTeamDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*Organization).client + + for _, rs := range s.RootModule().Resources { + if rs.Type != "github_team" { + continue + } + + team, resp, err := conn.Organizations.GetTeam(toGithubID(rs.Primary.ID)) + if err == nil { + if team != nil && + fromGithubID(team.ID) == rs.Primary.ID { + return fmt.Errorf("Team still exists") + } + } + if resp.StatusCode != 404 { + return err + } + return nil + } + return nil +} + +const testAccGithubTeamConfig = ` +resource "github_team" "foo" { + name = "foo" + description = "Terraform acc test group" +} +` + +const testAccGithubTeamUpdateConfig = ` +resource "github_team" "foo" { + name = "foo2" + description = "Terraform acc test group - updated" +} +` diff --git a/builtin/providers/github/util.go b/builtin/providers/github/util.go new file mode 100644 index 000000000..3f5e2074a --- /dev/null +++ b/builtin/providers/github/util.go @@ -0,0 +1,47 @@ +package github + +import ( + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/terraform/helper/schema" +) + +func toGithubID(id string) int { + githubID, _ := strconv.Atoi(id) + return githubID +} + +func fromGithubID(id *int) string { + return strconv.Itoa(*id) +} + +func validateRoleValueFunc(roles []string) schema.SchemaValidateFunc { + return func(v interface{}, k string) (we []string, errors []error) { + value := v.(string) + valid := false + for _, role := range roles { + if value == role { + valid = true + break + } + } + + if !valid { + errors = append(errors, fmt.Errorf("%s is an invalid Github role type for %s", value, k)) + } + return + } +} + +// return the pieces of id `a:b` as a, b +func parseTwoPartID(id string) (string, string) { + parts := strings.SplitN(id, ":", 2) + return parts[0], parts[1] +} + +// format the strings into an id `a:b` +func buildTwoPartID(a, b *string) string { + return fmt.Sprintf("%s:%s", *a, *b) +} diff --git a/builtin/providers/github/util_test.go b/builtin/providers/github/util_test.go new file mode 100644 index 000000000..9efd81f73 --- /dev/null +++ b/builtin/providers/github/util_test.go @@ -0,0 +1,55 @@ +package github + +import ( + "testing" +) + +func TestAccGithubUtilRole_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "invalid", + ErrCount: 1, + }, + { + Value: "valid_one", + ErrCount: 0, + }, + { + Value: "valid_two", + ErrCount: 0, + }, + } + + validationFunc := validateRoleValueFunc([]string{"valid_one", "valid_two"}) + + for _, tc := range cases { + _, errors := validationFunc(tc.Value, "github_membership") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected github_membership to trigger a validation error") + } + } +} + +func TestAccGithubUtilTwoPartID(t *testing.T) { + partOne, partTwo := "foo", "bar" + + id := buildTwoPartID(&partOne, &partTwo) + + if id != "foo:bar" { + t.Fatalf("Expected two part id to be foo:bar, actual: %s", id) + } + + parsedPartOne, parsedPartTwo := parseTwoPartID(id) + + if parsedPartOne != "foo" { + t.Fatalf("Expected parsed part one foo, actual: %s", parsedPartOne) + } + + if parsedPartTwo != "bar" { + t.Fatalf("Expected parsed part two bar, actual: %s", parsedPartTwo) + } +} diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index c1a6e629d..83c8f2314 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -20,6 +20,7 @@ body.layout-dme, body.layout-dnsimple, body.layout-docker, body.layout-dyn, +body.layout-github, body.layout-google, body.layout-heroku, body.layout-mailgun, diff --git a/website/source/docs/providers/github/index.html.markdown b/website/source/docs/providers/github/index.html.markdown new file mode 100644 index 000000000..e7d11056f --- /dev/null +++ b/website/source/docs/providers/github/index.html.markdown @@ -0,0 +1,42 @@ +--- +layout: "github" +page_title: "Provider: Github" +sidebar_current: "docs-github-index" +description: |- + The Github provider is used to interact with Github organization resources. +--- + +# Github Provider + +The Github provider is used to interact with Github organization resources. + +The provider allows you to manage your Github organization's members and teams easily. +It needs to be configured with the proper credentials before it can be used. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the Github Provider +provider "github" { + token = "${var.github_token}" + organization = "${var.github_organization}" +} + +# Add a user to the organization +resource "github_membership" "membership_for_user_x" { + ... +} +``` + +## Argument Reference + +The following arguments are supported in the `provider` block: + +* `token` - (Optional) This is the Github personal access token. It must be provided, but + it can also be sourced from the `GITHUB_TOKEN` environment variable. + +* `organization` - (Optional) This is the target Github organization to manage. The account + corresponding to the token will need "owner" privileges for this organization. It must be provided, but + it can also be sourced from the `GITHUB_ORGANIZATION` environment variable. diff --git a/website/source/docs/providers/github/r/membership.html.markdown b/website/source/docs/providers/github/r/membership.html.markdown new file mode 100644 index 000000000..5f73e2132 --- /dev/null +++ b/website/source/docs/providers/github/r/membership.html.markdown @@ -0,0 +1,33 @@ +--- +layout: "github" +page_title: "Github: github_membership" +sidebar_current: "docs-github-resource-membership" +description: |- + Provides a Github membership resource. +--- + +# github\_membership + +Provides a Github membership resource. + +This resource allows you to add/remove users from your organization. When applied, +an invitation will be sent to the user to become part of the organization. When +destroyed, either the invitation will be cancelled or the user will be removed. + +## Example Usage + +``` +# Add a user to the organization +resource "github_membership" "membership_for_some_user" { + username = "SomeUser" + role = "member" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `username` - (Required) The user to add to the organization. +* `role` - (Optional) The role of the user within the organization. + Must be one of `member` or `admin`. Defaults to `member`. diff --git a/website/source/docs/providers/github/r/team.html.markdown b/website/source/docs/providers/github/r/team.html.markdown new file mode 100644 index 000000000..d81533a73 --- /dev/null +++ b/website/source/docs/providers/github/r/team.html.markdown @@ -0,0 +1,37 @@ +--- +layout: "github" +page_title: "Github: github_team" +sidebar_current: "docs-github-resource-team" +description: |- + Provides a Github team resource. +--- + +# github\_team + +Provides a Github team resource. + +This resource allows you to add/remove teams from your organization. When applied, +a new team will be created. When destroyed, that team will be removed. + +## Example Usage + +``` +# Add a team to the organization +resource "github_team" "some_team" { + name = "some-team" + description = "Some cool team" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the team. +* `description` - (Optional) A description of the team. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the created team. diff --git a/website/source/docs/providers/github/r/team_membership.html.markdown b/website/source/docs/providers/github/r/team_membership.html.markdown new file mode 100644 index 000000000..6161068a6 --- /dev/null +++ b/website/source/docs/providers/github/r/team_membership.html.markdown @@ -0,0 +1,46 @@ +--- +layout: "github" +page_title: "Github: github_team_membership" +sidebar_current: "docs-github-resource-team-membership" +description: |- + Provides a Github team membership resource. +--- + +# github\_team_membership + +Provides a Github team membership resource. + +This resource allows you to add/remove users from teams in your organization. When applied, +the user will be added to the team. If the user hasn't accepted their invitation to the +organization, they won't be part of the team until they do. When +destroyed, the user will be removed from the team. + +## Example Usage + +``` +# Add a user to the organization +resource "github_membership" "membership_for_some_user" { + username = "SomeUser" + role = "member" +} + +resource "github_team" "some_team" { + name = "SomeTeam" + description = "Some cool team" +} + +resource "github_team_membership" "some_team_membership" { + team_id = "${github_team.some_team.id}" + username = "SomeUser" + role = "member" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `team_id` - (Required) The Github team id +* `username` - (Required) The user to add to the team. +* `role` - (Optional) The role of the user within the team. + Must be one of `member` or `maintainer`. Defaults to `member`. diff --git a/website/source/docs/providers/github/r/team_repository.html.markdown b/website/source/docs/providers/github/r/team_repository.html.markdown new file mode 100644 index 000000000..c62b58a34 --- /dev/null +++ b/website/source/docs/providers/github/r/team_repository.html.markdown @@ -0,0 +1,39 @@ +--- +layout: "github" +page_title: "Github: github_team_repository" +sidebar_current: "docs-github-resource-team-repository" +description: |- + Provides a Github team repository resource. +--- + +# github\_team_repository + +Provides a Github team repository resource. + +This resource allows you to add/remove repositories from teams in your organization. When applied, +the repository will be added to the team. When destroyed, the repository will be removed from the team. + +## Example Usage + +``` +# Add a repository to the team +resource "github_team" "some_team" { + name = "SomeTeam" + description = "Some cool team" +} + +resource "github_team_repository" "some_team_repo" { + team_id = "${github_team.some_team.id}" + repository = "our-repo" + permission = "pull" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `team_id` - (Required) The Github team id +* `repository` - (Required) The repository to add to the team. +* `permission` - (Optional) The permissions of team members regarding the repository. + Must be one of `pull`, `push`, or `admin`. Defaults to `pull`. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index a000ec6c8..003897f53 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -182,6 +182,10 @@ Dyn + > + Github + + > Google Cloud @@ -218,9 +222,9 @@ Rundeck - > - StatusCake - + > + StatusCake + > Template @@ -238,9 +242,9 @@ VMware vCloud Director - > + > VMware vSphere - + diff --git a/website/source/layouts/github.erb b/website/source/layouts/github.erb new file mode 100644 index 000000000..a49aecfe4 --- /dev/null +++ b/website/source/layouts/github.erb @@ -0,0 +1,35 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> + <% end %>