diff --git a/google/provider.go b/google/provider.go index 0e89fe49bb9..51cd30ad9ff 100644 --- a/google/provider.go +++ b/google/provider.go @@ -179,6 +179,7 @@ func Provider() terraform.ResourceProvider { "google_project_iam_member": ResourceIamMemberWithImport(IamProjectSchema, NewProjectIamUpdater, ProjectIdParseFunc), "google_project_service": resourceGoogleProjectService(), "google_project_iam_custom_role": resourceGoogleProjectIamCustomRole(), + "google_project_organization_policy": resourceGoogleProjectOrganizationPolicy(), "google_project_usage_export_bucket": resourceProjectUsageBucket(), "google_project_services": resourceGoogleProjectServices(), "google_pubsub_topic": resourcePubsubTopic(), diff --git a/google/resource_google_project_organization_policy.go b/google/resource_google_project_organization_policy.go new file mode 100644 index 00000000000..2f986cf0bda --- /dev/null +++ b/google/resource_google_project_organization_policy.go @@ -0,0 +1,104 @@ +package google + +import ( + "fmt" + + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" +) + +func resourceGoogleProjectOrganizationPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceGoogleProjectOrganizationPolicyCreate, + Read: resourceGoogleProjectOrganizationPolicyRead, + Update: resourceGoogleProjectOrganizationPolicyUpdate, + Delete: resourceGoogleProjectOrganizationPolicyDelete, + + Schema: mergeSchemas( + schemaOrganizationPolicy, + map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + ), + } +} + +func resourceGoogleProjectOrganizationPolicyCreate(d *schema.ResourceData, meta interface{}) error { + if err := setProjectOrganizationPolicy(d, meta); err != nil { + return err + } + + d.SetId(fmt.Sprintf("%s:%s", d.Get("project"), d.Get("constraint"))) + + return resourceGoogleProjectOrganizationPolicyRead(d, meta) +} + +func resourceGoogleProjectOrganizationPolicyRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + project := prefixedProject(d.Get("project").(string)) + + policy, err := config.clientResourceManager.Projects.GetOrgPolicy(project, &cloudresourcemanager.GetOrgPolicyRequest{ + Constraint: canonicalOrgPolicyConstraint(d.Get("constraint").(string)), + }).Do() + + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Organization policy for %s", project)) + } + + d.Set("constraint", policy.Constraint) + d.Set("boolean_policy", flattenBooleanOrganizationPolicy(policy.BooleanPolicy)) + d.Set("list_policy", flattenListOrganizationPolicy(policy.ListPolicy)) + d.Set("version", policy.Version) + d.Set("etag", policy.Etag) + d.Set("update_time", policy.UpdateTime) + + return nil +} + +func resourceGoogleProjectOrganizationPolicyUpdate(d *schema.ResourceData, meta interface{}) error { + if err := setProjectOrganizationPolicy(d, meta); err != nil { + return err + } + + return resourceGoogleProjectOrganizationPolicyRead(d, meta) +} + +func resourceGoogleProjectOrganizationPolicyDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + project := prefixedProject(d.Get("project").(string)) + + _, err := config.clientResourceManager.Projects.ClearOrgPolicy(project, &cloudresourcemanager.ClearOrgPolicyRequest{ + Constraint: canonicalOrgPolicyConstraint(d.Get("constraint").(string)), + }).Do() + + if err != nil { + return err + } + + return nil +} + +func setProjectOrganizationPolicy(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + project := prefixedProject(d.Get("project").(string)) + listPolicy, err := expandListOrganizationPolicy(d.Get("list_policy").([]interface{})) + if err != nil { + return err + } + + _, err = config.clientResourceManager.Projects.SetOrgPolicy(project, &cloudresourcemanager.SetOrgPolicyRequest{ + Policy: &cloudresourcemanager.OrgPolicy{ + Constraint: canonicalOrgPolicyConstraint(d.Get("constraint").(string)), + BooleanPolicy: expandBooleanOrganizationPolicy(d.Get("boolean_policy").([]interface{})), + ListPolicy: listPolicy, + Version: int64(d.Get("version").(int)), + Etag: d.Get("etag").(string), + }, + }).Do() + + return err +} diff --git a/google/resource_google_project_organization_policy_test.go b/google/resource_google_project_organization_policy_test.go new file mode 100644 index 00000000000..f5a68f6ad9e --- /dev/null +++ b/google/resource_google_project_organization_policy_test.go @@ -0,0 +1,305 @@ +package google + +import ( + "fmt" + "reflect" + "sort" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/cloudresourcemanager/v1" +) + +/* +Tests for `google_project_organization_policy` + +These are *not* run in parallel, as they all use the same project +and I end up with 409 Conflict errors from the API when they are +run in parallel. +*/ + +func TestAccProjectOrganizationPolicy_boolean(t *testing.T) { + projectId := getTestProjectFromEnv() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + // Test creation of an enforced boolean policy + Config: testAccProjectOrganizationPolicy_boolean(projectId, true), + Check: testAccCheckGoogleProjectOrganizationBooleanPolicy("bool", true), + }, + { + // Test update from enforced to not + Config: testAccProjectOrganizationPolicy_boolean(projectId, false), + Check: testAccCheckGoogleProjectOrganizationBooleanPolicy("bool", false), + }, + { + Config: " ", + Destroy: true, + }, + { + // Test creation of a not enforced boolean policy + Config: testAccProjectOrganizationPolicy_boolean(projectId, false), + Check: testAccCheckGoogleProjectOrganizationBooleanPolicy("bool", false), + }, + { + // Test update from not enforced to enforced + Config: testAccProjectOrganizationPolicy_boolean(projectId, true), + Check: testAccCheckGoogleProjectOrganizationBooleanPolicy("bool", true), + }, + }, + }) +} + +func TestAccProjectOrganizationPolicy_list_allowAll(t *testing.T) { + projectId := getTestProjectFromEnv() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectOrganizationPolicy_list_allowAll(projectId), + Check: testAccCheckGoogleProjectOrganizationListPolicyAll("list", "ALLOW"), + }, + }, + }) +} + +func TestAccProjectOrganizationPolicy_list_allowSome(t *testing.T) { + project := getTestProjectFromEnv() + canonicalProject := canonicalProjectId(project) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectOrganizationPolicy_list_allowSome(project), + Check: testAccCheckGoogleProjectOrganizationListPolicyAllowedValues("list", []string{canonicalProject}), + }, + }, + }) +} + +func TestAccProjectOrganizationPolicy_list_denySome(t *testing.T) { + projectId := getTestProjectFromEnv() + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectOrganizationPolicy_list_denySome(projectId), + Check: testAccCheckGoogleProjectOrganizationListPolicyDeniedValues("list", DENIED_ORG_POLICIES), + }, + }, + }) +} + +func TestAccProjectOrganizationPolicy_list_update(t *testing.T) { + projectId := getTestProjectFromEnv() + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckGoogleProjectOrganizationPolicyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccProjectOrganizationPolicy_list_allowAll(projectId), + Check: testAccCheckGoogleProjectOrganizationListPolicyAll("list", "ALLOW"), + }, + { + Config: testAccProjectOrganizationPolicy_list_denySome(projectId), + Check: testAccCheckGoogleProjectOrganizationListPolicyDeniedValues("list", DENIED_ORG_POLICIES), + }, + }, + }) +} + +func testAccCheckGoogleProjectOrganizationPolicyDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_project_organization_policy" { + continue + } + + projectId := canonicalProjectId(rs.Primary.Attributes["project"]) + constraint := canonicalOrgPolicyConstraint(rs.Primary.Attributes["constraint"]) + policy, err := config.clientResourceManager.Projects.GetOrgPolicy(projectId, &cloudresourcemanager.GetOrgPolicyRequest{ + Constraint: constraint, + }).Do() + + if err != nil { + return err + } + + if policy.ListPolicy != nil || policy.BooleanPolicy != nil { + return fmt.Errorf("Org policy with constraint '%s' hasn't been cleared", constraint) + } + } + return nil +} + +func testAccCheckGoogleProjectOrganizationBooleanPolicy(n string, enforced bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleProjectOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + if policy.BooleanPolicy.Enforced != enforced { + return fmt.Errorf("Expected boolean policy enforcement to be '%t', got '%t'", enforced, policy.BooleanPolicy.Enforced) + } + + return nil + } +} + +func testAccCheckGoogleProjectOrganizationListPolicyAll(n, policyType string) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleProjectOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + if len(policy.ListPolicy.AllowedValues) > 0 || len(policy.ListPolicy.DeniedValues) > 0 { + return fmt.Errorf("The `values` field shouldn't be set") + } + + if policy.ListPolicy.AllValues != policyType { + return fmt.Errorf("The list policy should %s all values", policyType) + } + + return nil + } +} + +func testAccCheckGoogleProjectOrganizationListPolicyAllowedValues(n string, values []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleProjectOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + sort.Strings(policy.ListPolicy.AllowedValues) + sort.Strings(values) + if !reflect.DeepEqual(policy.ListPolicy.AllowedValues, values) { + return fmt.Errorf("Expected the list policy to allow '%s', instead allowed '%s'", values, policy.ListPolicy.AllowedValues) + } + + return nil + } +} + +func testAccCheckGoogleProjectOrganizationListPolicyDeniedValues(n string, values []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + policy, err := getGoogleProjectOrganizationPolicyTestResource(s, n) + if err != nil { + return err + } + + sort.Strings(policy.ListPolicy.DeniedValues) + sort.Strings(values) + if !reflect.DeepEqual(policy.ListPolicy.DeniedValues, values) { + return fmt.Errorf("Expected the list policy to deny '%s', instead denied '%s'", values, policy.ListPolicy.DeniedValues) + } + + return nil + } +} + +func getGoogleProjectOrganizationPolicyTestResource(s *terraform.State, n string) (*cloudresourcemanager.OrgPolicy, error) { + rn := "google_project_organization_policy." + n + rs, ok := s.RootModule().Resources[rn] + if !ok { + return nil, fmt.Errorf("Not found: %s", rn) + } + + if rs.Primary.ID == "" { + return nil, fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + projectId := canonicalProjectId(rs.Primary.Attributes["project"]) + + return config.clientResourceManager.Projects.GetOrgPolicy(projectId, &cloudresourcemanager.GetOrgPolicyRequest{ + Constraint: rs.Primary.Attributes["constraint"], + }).Do() +} + +func testAccProjectOrganizationPolicy_boolean(pid string, enforced bool) string { + return fmt.Sprintf(` +resource "google_project_organization_policy" "bool" { + project = "%s" + constraint = "constraints/compute.disableSerialPortAccess" + + boolean_policy { + enforced = %t + } +} +`, pid, enforced) +} + +func testAccProjectOrganizationPolicy_list_allowAll(pid string) string { + return fmt.Sprintf(` +resource "google_project_organization_policy" "list" { + project = "%s" + constraint = "constraints/serviceuser.services" + + list_policy { + allow { + all = true + } + } +} +`, pid) +} + +func testAccProjectOrganizationPolicy_list_allowSome(pid string) string { + return fmt.Sprintf(` + +resource "google_project_organization_policy" "list" { + project = "%s" + constraint = "constraints/compute.trustedImageProjects" + + list_policy { + allow { + values = ["projects/%s"] + } + } +} +`, pid, pid) +} + +func testAccProjectOrganizationPolicy_list_denySome(pid string) string { + return fmt.Sprintf(` + +resource "google_project_organization_policy" "list" { + project = "%s" + constraint = "constraints/serviceuser.services" + + list_policy { + deny { + values = [ + "doubleclicksearch.googleapis.com", + "replicapoolupdater.googleapis.com", + ] + } + } +} +`, pid) +} + +func canonicalProjectId(project string) string { + if strings.HasPrefix(project, "projects/") { + return project + } + return fmt.Sprintf("projects/%s", project) +} diff --git a/website/docs/r/google_project_organization_policy.html.markdown b/website/docs/r/google_project_organization_policy.html.markdown new file mode 100644 index 00000000000..d210ba750db --- /dev/null +++ b/website/docs/r/google_project_organization_policy.html.markdown @@ -0,0 +1,106 @@ +--- +layout: "google" +page_title: "Google: google_project_organization_policy" +sidebar_current: "docs-google-project-organization-policy" +description: |- + Allows management of Organization policies for a Google Project. +--- + +# google\_project\_organization\_policy + +Allows management of Organization policies for a Google Project. For more information see +[the official +documentation](https://cloud.google.com/resource-manager/docs/organization-policy/overview) and +[API](https://cloud.google.com/resource-manager/reference/rest/v1/projects/setOrgPolicy). + +## Example Usage + +To set policy with a [boolean constraint](https://cloud.google.com/resource-manager/docs/organization-policy/quickstart-boolean-constraints): + +```hcl +resource "google_project_organization_policy" "serial_port_policy" { + project = "your-project-id" + constraint = "compute.disableSerialPortAccess" + + boolean_policy { + enforced = true + } +} +``` + + +To set a policy with a [list contraint](https://cloud.google.com/resource-manager/docs/organization-policy/quickstart-list-constraints): + +```hcl +resource "google_project_organization_policy" "services_policy" { + project = "your-project-id" + constraint = "serviceuser.services" + + list_policy { + allow { + all = true + } + } +} +``` + + +Or to deny some services, use the following instead: + +```hcl +resource "google_project_organization_policy" "services_policy" { + project = "your-project-id" + constraint = "serviceuser.services" + + list_policy { + suggested_values = "compute.googleapis.com" + + deny { + values = ["cloudresourcemanager.googleapis.com"] + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `project` - (Required) The project id of the project to set the policy for. + +* `constraint` - (Required) The name of the Constraint the Policy is configuring, for example, `serviceuser.services`. Check out the [complete list of available constraints](https://cloud.google.com/resource-manager/docs/organization-policy/understanding-constraints#available_constraints). + +- - - + +* `version` - (Optional) Version of the Policy. Default version is 0. + +* `boolean_policy` - (Optional) A boolean policy is a constraint that is either enforced or not. Structure is documented below. + +* `list_policy` - (Optional) A policy that can define specific values that are allowed or denied for the given constraint. It can also be used to allow or deny all values. Structure is documented below. + +- - - + +The `boolean_policy` block supports: + +* `enforced` - (Required) If true, then the Policy is enforced. If false, then any configuration is acceptable. + +The `list_policy` block supports: + +* `allow` or `deny` - (Optional) One or the other must be set. + +* `suggested_values` - (Optional) The Google Cloud Console will try to default to a configuration that matches the value specified in this field. + +The `allow` or `deny` blocks support: + +* `all` - (Optional) The policy allows or denies all values. + +* `values` - (Optional) The policy can define specific values that are allowed or denied. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are +exported: + +* `etag` - (Computed) The etag of the organization policy. `etag` is used for optimistic concurrency control as a way to help prevent simultaneous updates of a policy from overwriting each other. + +* `update_time` - (Computed) The timestamp in RFC3339 UTC "Zulu" format, accurate to nanoseconds, representing when the variable was last updated. Example: "2016-10-09T12:33:37.578138407Z". diff --git a/website/google.erb b/website/google.erb index c3a98503c67..176f0057c0a 100644 --- a/website/google.erb +++ b/website/google.erb @@ -180,6 +180,9 @@ > google_project_iam_custom_role + > + google_project_organization_policy + > google_project_service