diff --git a/google/resource_google_project.go b/google/resource_google_project.go index ece7172a..fa5c515b 100644 --- a/google/resource_google_project.go +++ b/google/resource_google_project.go @@ -51,9 +51,17 @@ func resourceGoogleProject() *schema.Resource { Required: true, }, "org_id": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"folder_id"}, + }, + "folder_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"org_id"}, + StateFunc: parseFolderId, }, "policy_data": &schema.Schema{ Type: schema.TypeString, @@ -95,12 +103,10 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error project := &cloudresourcemanager.Project{ ProjectId: pid, Name: d.Get("name").(string), - Parent: &cloudresourcemanager.ResourceId{ - Id: d.Get("org_id").(string), - Type: "organization", - }, } + getParentResourceId(d, project) + if _, ok := d.GetOk("labels"); ok { project.Labels = expandLabels(d) } @@ -155,7 +161,14 @@ func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error { d.Set("labels", p.Labels) if p.Parent != nil { - d.Set("org_id", p.Parent.Id) + switch p.Parent.Type { + case "organization": + d.Set("org_id", p.Parent.Id) + d.Set("folder_id", "") + case "folder": + d.Set("folder_id", p.Parent.Id) + d.Set("org_id", "") + } } // Read the billing account @@ -183,9 +196,36 @@ func prefixedProject(pid string) string { return "projects/" + pid } +func getParentResourceId(d *schema.ResourceData, p *cloudresourcemanager.Project) error { + if v, ok := d.GetOk("org_id"); ok { + org_id := v.(string) + p.Parent = &cloudresourcemanager.ResourceId{ + Id: org_id, + Type: "organization", + } + } + + if v, ok := d.GetOk("folder_id"); ok { + p.Parent = &cloudresourcemanager.ResourceId{ + Id: parseFolderId(v), + Type: "folder", + } + } + return nil +} + +func parseFolderId(v interface{}) string { + folderId := v.(string) + if strings.HasPrefix(folderId, "folders/") { + return folderId[8:] + } + return folderId +} + func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) pid := d.Id() + project_name := d.Get("name").(string) // Read the project // we need the project even though refresh has already been called @@ -198,30 +238,46 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error checking project %q: %s", pid, err) } - // Project name has changed + d.Partial(true) + + // Project display name has changed if ok := d.HasChange("name"); ok { - p.Name = d.Get("name").(string) + p.Name = project_name // Do update on project p, err = config.clientResourceManager.Projects.Update(p.ProjectId, p).Do() if err != nil { - return fmt.Errorf("Error updating project %q: %s", p.Name, err) + return fmt.Errorf("Error updating project %q: %s", project_name, err) } + d.SetPartial("name") + } + + // Project parent has changed + if d.HasChange("org_id") || d.HasChange("folder_id") { + getParentResourceId(d, p) + + // Do update on project + p, err = config.clientResourceManager.Projects.Update(p.ProjectId, p).Do() + if err != nil { + return fmt.Errorf("Error updating project %q: %s", project_name, err) + } + d.SetPartial("org_id") + d.SetPartial("folder_id") } // Billing account has changed if ok := d.HasChange("billing_account"); ok { - name := d.Get("billing_account").(string) + billing_name := d.Get("billing_account").(string) ba := cloudbilling.ProjectBillingInfo{} - if name != "" { - ba.BillingAccountName = "billingAccounts/" + name + if billing_name != "" { + ba.BillingAccountName = "billingAccounts/" + billing_name } _, err = config.clientBilling.Projects.UpdateBillingInfo(prefixedProject(pid), &ba).Do() if err != nil { d.Set("billing_account", "") if _err, ok := err.(*googleapi.Error); ok { - return fmt.Errorf("Error updating billing account %q for project %q: %v", name, prefixedProject(pid), _err) + return fmt.Errorf("Error updating billing account %q for project %q: %v", billing_name, prefixedProject(pid), _err) } - return fmt.Errorf("Error updating billing account %q for project %q: %v", name, prefixedProject(pid), err) + return fmt.Errorf("Error updating billing account %q for project %q: %v", billing_name, prefixedProject(pid), err) } } @@ -235,6 +291,8 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error updating project %q: %s", p.Name, err) } } + d.Partial(false) + return nil } diff --git a/google/resource_google_project_iam_policy_test.go b/google/resource_google_project_iam_policy_test.go index 24052c96..0d917ecc 100644 --- a/google/resource_google_project_iam_policy_test.go +++ b/google/resource_google_project_iam_policy_test.go @@ -658,6 +658,14 @@ data "google_iam_policy" "admin" { `, pid, name, org) } +func testAccGoogleProject_createWithoutOrg(pid, name string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" +}`, pid, name) +} + func testAccGoogleProject_create(pid, name, org string) string { return fmt.Sprintf(` resource "google_project" "acceptance" { diff --git a/google/resource_google_project_test.go b/google/resource_google_project_test.go index 533f0936..0c6ce983 100644 --- a/google/resource_google_project_test.go +++ b/google/resource_google_project_test.go @@ -24,15 +24,44 @@ var ( originalPolicy *cloudresourcemanager.Policy ) -// Test that a Project resource can be created and an IAM policy -// associated -func TestAccGoogleProject_create(t *testing.T) { +// Test that a Project resource can be created without an organization +func TestAccGoogleProject_createWithoutOrg(t *testing.T) { + creds := multiEnvSearch(credsEnvVars) + if strings.Contains(creds, "iam.gserviceaccount.com") { + t.Skip("Service accounts cannot create projects without a parent. Requires user credentials.") + } + pid := "terraform-" + acctest.RandString(10) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ - // This step imports an existing project + // This step creates a new project + resource.TestStep{ + Config: testAccGoogleProject_createWithoutOrg(pid, pname), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + }, + }) +} + +// Test that a Project resource can be created and an IAM policy +// associated +func TestAccGoogleProject_create(t *testing.T) { + skipIfEnvNotSet(t, + []string{ + "GOOGLE_ORG", + }..., + ) + + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // This step creates a new project resource.TestStep{ Config: testAccGoogleProject_create(pid, pname, org), Check: resource.ComposeTestCheckFunc( diff --git a/website/docs/r/google_project.html.markdown b/website/docs/r/google_project.html.markdown index 43c84dda..3dbc3262 100755 --- a/website/docs/r/google_project.html.markdown +++ b/website/docs/r/google_project.html.markdown @@ -45,6 +45,20 @@ resource "google_project" "my_project" { } ``` +To create a project under a specific folder + +```hcl +resource "google_project" "my_project-in-a-folder" { + project_id = "your-project-id" + folder_id = "${google_folder.department1.name}" +} + +resource "google_folder" "department1" { + display_name = "Department 1" + parent = "organizations/1234567" +} +``` + ## Argument Reference The following arguments are supported: @@ -53,7 +67,7 @@ The following arguments are supported: Changing this forces a new project to be created. If this attribute is not set, `id` must be set. As `id` is deprecated, consider this attribute required. If you are using `project_id` and creating a new project, the - `org_id` and `name` attributes are also required. + `name` attribute is also required. * `id` - (Deprecated) The project ID. This attribute has unexpected behaviour and probably does not work @@ -62,8 +76,17 @@ The following arguments are supported: [below](#id-field) for more information about its behaviour. * `org_id` - (Optional) The numeric ID of the organization this project belongs to. - This is required if you are creating a new project. - Changing this forces a new project to be created. + Changing this forces a new project to be created. Only one of + `org_id` or `folder_id` may be specified. If the `org_id` is + specified then the project is created at the top level. Changing + this forces the project to be migrated to the newly specified + organization. + +* `folder_id` - (Optional) The numeric ID of the folder this project should be + created under. Only one of `org_id` or `folder_id` may be + specified. If the `folder_id` is specified, then the project is + created under the specified folder. Changing this forces the + project to be migrated to the newly specified folder. * `billing_account` - (Optional) The alphanumeric ID of the billing account this project belongs to. The user or service account performing this operation with Terraform