Skip to content

Commit

Permalink
Add new resource to support IAM custom organization roles (#735)
Browse files Browse the repository at this point in the history
* Add new resource to support IAM custom organization roles
* Add documentation
  • Loading branch information
rosbo authored Nov 14, 2017
1 parent 2e8ec6f commit 108971f
Show file tree
Hide file tree
Showing 8 changed files with 547 additions and 0 deletions.
44 changes: 44 additions & 0 deletions google/field_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const (
zonalLinkTemplate = "projects/%s/zones/%s/%s/%s"
zonalLinkBasePattern = "projects/(.+)/zones/(.+)/%s/(.+)"
zonalPartialLinkBasePattern = "zones/(.+)/%s/(.+)"
organizationLinkTemplate = "organizations/%s/%s/%s"
organizationBasePattern = "organizations/(.+)/%s/(.+)"
)

// ------------------------------------------------------------
Expand All @@ -33,6 +35,10 @@ func ParseDiskFieldValue(disk string, d TerraformResourceData, config *Config) (
return parseZonalFieldValue("disks", disk, "project", "zone", d, config, false)
}

func ParseOrganizationCustomRoleName(role string) (*OrganizationFieldValue, error) {
return parseOrganizationFieldValue("roles", role, false)
}

// ------------------------------------------------------------
// Base helpers used to create helpers for specific fields.
// ------------------------------------------------------------
Expand Down Expand Up @@ -176,3 +182,41 @@ func getProjectFromSchema(projectSchemaField string, d TerraformResourceData, co
}
return res.(string), nil
}

type OrganizationFieldValue struct {
OrgId string
Name string

resourceType string
}

func (f OrganizationFieldValue) RelativeLink() string {
if len(f.Name) == 0 {
return ""
}

return fmt.Sprintf(organizationLinkTemplate, f.OrgId, f.resourceType, f.Name)
}

// Parses an organization field with the following formats:
// - organizations/{my_organizations}/{resource_type}/{resource_name}
func parseOrganizationFieldValue(resourceType, fieldValue string, isEmptyValid bool) (*OrganizationFieldValue, error) {
if len(fieldValue) == 0 {
if isEmptyValid {
return &OrganizationFieldValue{resourceType: resourceType}, nil
}
return nil, fmt.Errorf("The organization field for resource %s cannot be empty", resourceType)
}

r := regexp.MustCompile(fmt.Sprintf(organizationBasePattern, resourceType))
if parts := r.FindStringSubmatch(fieldValue); parts != nil {
return &OrganizationFieldValue{
OrgId: parts[1],
Name: parts[2],

resourceType: resourceType,
}, nil
}

return nil, fmt.Errorf("Invalid field format. Got '%s', expected format '%s'", fieldValue, fmt.Sprintf(organizationLinkTemplate, "{org_id}", resourceType, "{name}"))
}
43 changes: 43 additions & 0 deletions google/field_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,46 @@ func TestParseZonalFieldValue(t *testing.T) {
}
}
}

func TestParseOrganizationFieldValue(t *testing.T) {
const resourceType = "roles"
cases := map[string]struct {
FieldValue string
ExpectedRelativeLink string
ExpectedName string
ExpectedOrgId string
ExpectedError bool
IsEmptyValid bool
}{
"role is valid": {
FieldValue: "organizations/123/roles/custom",
ExpectedRelativeLink: "organizations/123/roles/custom",
ExpectedName: "custom",
ExpectedOrgId: "123",
},
"role is empty and it is valid": {
FieldValue: "",
IsEmptyValid: true,
ExpectedRelativeLink: "",
},
"role is empty and it is not valid": {
FieldValue: "",
IsEmptyValid: false,
ExpectedError: true,
},
}

for tn, tc := range cases {
v, err := parseOrganizationFieldValue(resourceType, tc.FieldValue, tc.IsEmptyValid)

if err != nil {
if !tc.ExpectedError {
t.Errorf("bad: %s, did not expect an error. Error: %s", tn, err)
}
} else {
if v.RelativeLink() != tc.ExpectedRelativeLink {
t.Errorf("bad: %s, expected relative link to be '%s' but got '%s'", tn, tc.ExpectedRelativeLink, v.RelativeLink())
}
}
}
}
30 changes: 30 additions & 0 deletions google/import_google_organization_iam_custom_role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package google

import (
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"testing"
)

func TestAccGoogleOrganizationIamCustomRole_import(t *testing.T) {
t.Parallel()

skipIfEnvNotSet(t, "GOOGLE_ORG")
roleId := "tfIamRole" + acctest.RandString(10)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckGoogleOrganizationIamCustomRoleDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckGoogleOrganizationIamCustomRole_update(org, roleId),
},
{
ResourceName: "google_organization_iam_custom_role.foo",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
1 change: 1 addition & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ func Provider() terraform.ResourceProvider {
"google_sql_database": resourceSqlDatabase(),
"google_sql_database_instance": resourceSqlDatabaseInstance(),
"google_sql_user": resourceSqlUser(),
"google_organization_iam_custom_role": resourceGoogleOrganizationIamCustomRole(),
"google_organization_policy": resourceGoogleOrganizationPolicy(),
"google_project": resourceGoogleProject(),
"google_project_iam_policy": resourceGoogleProjectIamPolicy(),
Expand Down
171 changes: 171 additions & 0 deletions google/resource_google_organization_iam_custom_role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package google

import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
"google.golang.org/api/iam/v1"
)

func resourceGoogleOrganizationIamCustomRole() *schema.Resource {
return &schema.Resource{
Create: resourceGoogleOrganizationIamCustomRoleCreate,
Read: resourceGoogleOrganizationIamCustomRoleRead,
Update: resourceGoogleOrganizationIamCustomRoleUpdate,
Delete: resourceGoogleOrganizationIamCustomRoleDelete,

Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"role_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"org_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"title": {
Type: schema.TypeString,
Required: true,
},
"permissions": {
Type: schema.TypeSet,
Required: true,
MinItems: 1,
Elem: &schema.Schema{Type: schema.TypeString},
},
"stage": {
Type: schema.TypeString,
Optional: true,
Default: "GA",
ValidateFunc: validation.StringInSlice([]string{"ALPHA", "BETA", "GA", "DEPRECATED", "DISABLED", "EAP"}, false),
},
"description": {
Type: schema.TypeString,
Optional: true,
},
"deleted": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
}
}

func resourceGoogleOrganizationIamCustomRoleCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

if d.Get("deleted").(bool) {
return fmt.Errorf("Cannot create a custom organization role with a deleted state. `deleted` field should be false.")
}

role, err := config.clientIAM.Organizations.Roles.Create("organizations/"+d.Get("org_id").(string), &iam.CreateRoleRequest{
RoleId: d.Get("role_id").(string),
Role: &iam.Role{
Title: d.Get("title").(string),
Description: d.Get("description").(string),
Stage: d.Get("stage").(string),
IncludedPermissions: convertStringSet(d.Get("permissions").(*schema.Set)),
},
}).Do()

if err != nil {
return fmt.Errorf("Error creating the custom organization role %s: %s", d.Get("title").(string), err)
}

d.SetId(role.Name)

return resourceGoogleOrganizationIamCustomRoleRead(d, meta)
}

func resourceGoogleOrganizationIamCustomRoleRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

role, err := config.clientIAM.Organizations.Roles.Get(d.Id()).Do()
if err != nil {
return handleNotFoundError(err, d, d.Id())
}

parsedRoleName, err := ParseOrganizationCustomRoleName(role.Name)
if err != nil {
return err
}

d.Set("role_id", parsedRoleName.Name)
d.Set("org_id", parsedRoleName.OrgId)
d.Set("title", role.Title)
d.Set("description", role.Description)
d.Set("permissions", role.IncludedPermissions)
d.Set("stage", role.Stage)
d.Set("deleted", role.Deleted)

return nil
}

func resourceGoogleOrganizationIamCustomRoleUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

d.Partial(true)

if d.HasChange("deleted") {
if d.Get("deleted").(bool) {
if err := resourceGoogleOrganizationIamCustomRoleDelete(d, meta); err != nil {
return err
}
} else {
if err := resourceGoogleOrganizationIamCustomRoleUndelete(d, meta); err != nil {
return err
}
}
d.SetPartial("deleted")
}

if d.HasChange("title") || d.HasChange("description") || d.HasChange("stage") || d.HasChange("permissions") {
_, err := config.clientIAM.Organizations.Roles.Patch(d.Id(), &iam.Role{
Title: d.Get("title").(string),
Description: d.Get("description").(string),
Stage: d.Get("stage").(string),
IncludedPermissions: convertStringSet(d.Get("permissions").(*schema.Set)),
}).Do()

if err != nil {
return fmt.Errorf("Error updating the custom organization role %s: %s", d.Get("title").(string), err)
}
d.SetPartial("title")
d.SetPartial("description")
d.SetPartial("stage")
d.SetPartial("permissions")
}

d.Partial(false)

return nil
}

func resourceGoogleOrganizationIamCustomRoleDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

_, err := config.clientIAM.Organizations.Roles.Delete(d.Id()).Do()
if err != nil {
return fmt.Errorf("Error deleting the custom organization role %s: %s", d.Get("title").(string), err)
}

return nil
}

func resourceGoogleOrganizationIamCustomRoleUndelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

_, err := config.clientIAM.Organizations.Roles.Undelete(d.Id(), &iam.UndeleteRoleRequest{}).Do()
if err != nil {
return fmt.Errorf("Error undeleting the custom organization role %s: %s", d.Get("title").(string), err)
}

return nil
}
Loading

0 comments on commit 108971f

Please sign in to comment.