Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add GitHub Organization Custom Role Resource and Data Source #1700

Merged
merged 8 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions github/data_source_github_organization_custom_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package github

import (
"context"
"fmt"
"log"

"github.com/google/go-github/v53/github"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func dataSourceGithubOrganizationCustomRole() *schema.Resource {
return &schema.Resource{
Read: dataSourceGithubOrganizationCustomRoleRead,

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"base_role": {
Type: schema.TypeString,
Computed: true,
},
"permissions": {
Type: schema.TypeSet,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"description": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func dataSourceGithubOrganizationCustomRoleRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client
ctx := context.Background()
orgName := meta.(*Owner).name

err := checkOrganization(meta)
if err != nil {
return err
}

// ListCustomRepoRoles returns a list of all custom repository roles for an organization.
// There is an API endpoint for getting a single custom repository role, but is not
// implemented in the go-github library.
roleList, _, err := client.Organizations.ListCustomRepoRoles(ctx, orgName)
if err != nil {
return fmt.Errorf("error querying GitHub custom repository roles %s: %s", orgName, err)
}

var role *github.CustomRepoRoles
for _, r := range roleList.CustomRepoRoles {
if fmt.Sprint(*r.Name) == d.Get("name").(string) {
role = r
break
}
}

if role == nil {
log.Printf("[WARN] GitHub custom repository role (%s) not found.", d.Get("name").(string))
d.SetId("")
return nil
}

d.SetId(fmt.Sprint(*role.ID))
d.Set("name", role.Name)
d.Set("description", role.Description)
d.Set("base_role", role.BaseRole)
d.Set("permissions", role.Permissions)

return nil
}
85 changes: 85 additions & 0 deletions github/data_source_github_organization_custom_role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package github

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

func TestAccGithubOrganizationCustomRoleDataSource(t *testing.T) {

t.Run("queries a custom repo role", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)

config := fmt.Sprintf(`
resource "github_organization_custom_role" "test" {
name = "tf-acc-test-%s"
description = "Test role description"
base_role = "read"
permissions = [
"reopen_issue",
"reopen_pull_request",
]
}
`, randomID)

config2 := config + `
data "github_organization_custom_role" "test" {
name = github_organization_custom_role.test.name
}
`

check := resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(
"data.github_organization_custom_role.test", "name",
),
resource.TestCheckResourceAttr(
"data.github_organization_custom_role.test", "name",
fmt.Sprintf(`tf-acc-test-%s`, randomID),
),
resource.TestCheckResourceAttr(
"data.github_organization_custom_role.test", "description",
"Test role description",
),
resource.TestCheckResourceAttr(
"data.github_organization_custom_role.test", "base_role",
"read",
),
resource.TestCheckResourceAttr(
"data.github_organization_custom_role.test", "permissions.#",
"2",
),
)

testCase := func(t *testing.T, mode string) {
resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessMode(t, mode) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(),
},
{
Config: config2,
Check: check,
},
},
})
}

t.Run("with an anonymous account", func(t *testing.T) {
t.Skip("anonymous account not supported for this operation")
})

t.Run("with an individual account", func(t *testing.T) {
t.Skip("individual account not supported for this operation")
})

t.Run("with an organization account", func(t *testing.T) {
testCase(t, organization)
})
})
}
2 changes: 2 additions & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func Provider() terraform.ResourceProvider {
"github_issue_label": resourceGithubIssueLabel(),
"github_membership": resourceGithubMembership(),
"github_organization_block": resourceOrganizationBlock(),
"github_organization_custom_role": resourceGithubOrganizationCustomRole(),
"github_organization_project": resourceGithubOrganizationProject(),
"github_organization_security_manager": resourceGithubOrganizationSecurityManager(),
"github_organization_settings": resourceGithubOrganizationSettings(),
Expand Down Expand Up @@ -181,6 +182,7 @@ func Provider() terraform.ResourceProvider {
"github_issue_labels": dataSourceGithubIssueLabels(),
"github_membership": dataSourceGithubMembership(),
"github_organization": dataSourceGithubOrganization(),
"github_organization_custom_role": dataSourceGithubOrganizationCustomRole(),
"github_organization_ip_allow_list": dataSourceGithubOrganizationIpAllowList(),
"github_organization_team_sync_groups": dataSourceGithubOrganizationTeamSyncGroups(),
"github_organization_teams": dataSourceGithubOrganizationTeams(),
Expand Down
171 changes: 171 additions & 0 deletions github/resource_github_organization_custom_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package github

import (
"context"
"fmt"
"log"

"github.com/google/go-github/v53/github"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func resourceGithubOrganizationCustomRole() *schema.Resource {
return &schema.Resource{
Create: resourceGithubOrganizationCustomRoleCreate,
Read: resourceGithubOrganizationCustomRoleRead,
Update: resourceGithubOrganizationCustomRoleUpdate,
Delete: resourceGithubOrganizationCustomRoleDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
Description: "The organization custom repository role to create.",
},
"base_role": {
Type: schema.TypeString,
Required: true,
Description: "The base role for the custom repository role.",
ValidateFunc: validateValueFunc([]string{"read", "triage", "write", "maintain"}),
},
"permissions": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
MinItems: 1, // At least one permission should be passed.
Copy link
Contributor

@usmonster usmonster Nov 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to comment on an old/merged PR, but is this a necessary constraint? The API permits creation of a repository role without additional permissions, and it makes perfect sense if the role's only purpose is to designate who can bypass a repository ruleset, for example:

gh api \
  --method POST \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  /orgs/$ORG/custom-repository-roles \
  -f name='rule-breaker' \
  -f description='Can bypass repository rulesets' \
  -f base_role='read' \
  -f "permissions[]" 

If you can confirm, shall I open another issue (or PR) to correct this? Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you may be right about this. I was however unable to get it to post to the API at the time, so I was thinking it did require something, but I may have just passed in the wrong blank value.

Description: "The permissions for the custom repository role.",
},
"description": {
Type: schema.TypeString,
Optional: true,
Description: "The description of the custom repository role.",
},
},
}
}

func resourceGithubOrganizationCustomRoleCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client
orgName := meta.(*Owner).name
ctx := context.Background()

err := checkOrganization(meta)
if err != nil {
return err
}

permissions := d.Get("permissions").(*schema.Set).List()
permissionsStr := make([]string, len(permissions))
for i, v := range permissions {
permissionsStr[i] = v.(string)
}

role, _, err := client.Organizations.CreateCustomRepoRole(ctx, orgName, &github.CreateOrUpdateCustomRoleOptions{
Name: github.String(d.Get("name").(string)),
Description: github.String(d.Get("description").(string)),
BaseRole: github.String(d.Get("base_role").(string)),
Permissions: permissionsStr,
})

if err != nil {
return fmt.Errorf("error creating GitHub custom repository role %s (%s): %s", orgName, d.Get("name").(string), err)
}

d.SetId(fmt.Sprint(*role.ID))
return resourceGithubOrganizationCustomRoleRead(d, meta)
}

func resourceGithubOrganizationCustomRoleRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client
ctx := context.Background()
orgName := meta.(*Owner).name

err := checkOrganization(meta)
if err != nil {
return err
}

roleID := d.Id()

// ListCustomRepoRoles returns a list of all custom repository roles for an organization.
// There is an API endpoint for getting a single custom repository role, but is not
// implemented in the go-github library.
roleList, _, err := client.Organizations.ListCustomRepoRoles(ctx, orgName)
if err != nil {
return fmt.Errorf("error querying GitHub custom repository roles %s: %s", orgName, err)
}

var role *github.CustomRepoRoles
for _, r := range roleList.CustomRepoRoles {
if fmt.Sprint(*r.ID) == roleID {
role = r
break
}
}

if role == nil {
log.Printf("[WARN] GitHub custom repository role (%s/%s) not found, removing from state", orgName, roleID)
d.SetId("")
return nil
}

d.Set("name", role.Name)
d.Set("description", role.Description)
d.Set("base_role", role.BaseRole)
d.Set("permissions", role.Permissions)

return nil
}

func resourceGithubOrganizationCustomRoleUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client
ctx := context.Background()
orgName := meta.(*Owner).name

err := checkOrganization(meta)
if err != nil {
return err
}

roleID := d.Id()
permissions := d.Get("permissions").(*schema.Set).List()
permissionsStr := make([]string, len(permissions))
for i, v := range permissions {
permissionsStr[i] = v.(string)
}

update := &github.CreateOrUpdateCustomRoleOptions{
Name: github.String(d.Get("name").(string)),
Description: github.String(d.Get("description").(string)),
BaseRole: github.String(d.Get("base_role").(string)),
Permissions: permissionsStr,
}

if _, _, err := client.Organizations.UpdateCustomRepoRole(ctx, orgName, roleID, update); err != nil {
return fmt.Errorf("error updating GitHub custom repository role %s (%s): %s", orgName, roleID, err)
}

return resourceGithubOrganizationCustomRoleRead(d, meta)
}

func resourceGithubOrganizationCustomRoleDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Owner).v3client
ctx := context.Background()
orgName := meta.(*Owner).name

err := checkOrganization(meta)
if err != nil {
return err
}
roleID := d.Id()

_, err = client.Organizations.DeleteCustomRepoRole(ctx, orgName, roleID)
if err != nil {
return fmt.Errorf("Error deleting GitHub custom repository role %s (%s): %s", orgName, roleID, err)
}

return nil
}
Loading