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

Add new resource to support IAM custom organization roles #735

Merged
merged 3 commits into from
Nov 14, 2017
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
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