Folder support: Assign/Reassign a google project to a folder. (#438)

+ Make the org_id optional when creating a project. Closes #131
+ Mark org_id as computed to allow for GCP automatically assigning the org.
+ Add an acceptance test for project creation without an organization.
+ Skip TestAccGoogleProject_createWithoutOrg if GOOGLE_ORG is set.
+ Add a folder_id to the google_project resource, optionally
  specifying the ID of the GCP folder in which the GCP project should
  live.
+ Document how one can provision a project into a folder, and added a
  sample configuration to create a project into an existing folder.
* Skip test without org if service account is used
* Support folders/* or id only for the folder id field
This commit is contained in:
Manoj Srivastava 2017-09-22 11:03:08 -07:00 committed by Vincent Roseberry
parent f3ecd1ea1c
commit c1d0e716d9
4 changed files with 141 additions and 23 deletions

View File

@ -51,9 +51,17 @@ func resourceGoogleProject() *schema.Resource {
Required: true, Required: true,
}, },
"org_id": &schema.Schema{ "org_id": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Optional: true,
ForceNew: 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{ "policy_data": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -95,12 +103,10 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error
project := &cloudresourcemanager.Project{ project := &cloudresourcemanager.Project{
ProjectId: pid, ProjectId: pid,
Name: d.Get("name").(string), 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 { if _, ok := d.GetOk("labels"); ok {
project.Labels = expandLabels(d) project.Labels = expandLabels(d)
} }
@ -155,7 +161,14 @@ func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error {
d.Set("labels", p.Labels) d.Set("labels", p.Labels)
if p.Parent != nil { 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 // Read the billing account
@ -183,9 +196,36 @@ func prefixedProject(pid string) string {
return "projects/" + pid 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 { func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config) config := meta.(*Config)
pid := d.Id() pid := d.Id()
project_name := d.Get("name").(string)
// Read the project // Read the project
// we need the project even though refresh has already been called // 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) 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 { if ok := d.HasChange("name"); ok {
p.Name = d.Get("name").(string) p.Name = project_name
// Do update on project // Do update on project
p, err = config.clientResourceManager.Projects.Update(p.ProjectId, p).Do() p, err = config.clientResourceManager.Projects.Update(p.ProjectId, p).Do()
if err != nil { 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 // Billing account has changed
if ok := d.HasChange("billing_account"); ok { if ok := d.HasChange("billing_account"); ok {
name := d.Get("billing_account").(string) billing_name := d.Get("billing_account").(string)
ba := cloudbilling.ProjectBillingInfo{} ba := cloudbilling.ProjectBillingInfo{}
if name != "" { if billing_name != "" {
ba.BillingAccountName = "billingAccounts/" + name ba.BillingAccountName = "billingAccounts/" + billing_name
} }
_, err = config.clientBilling.Projects.UpdateBillingInfo(prefixedProject(pid), &ba).Do() _, err = config.clientBilling.Projects.UpdateBillingInfo(prefixedProject(pid), &ba).Do()
if err != nil { if err != nil {
d.Set("billing_account", "") d.Set("billing_account", "")
if _err, ok := err.(*googleapi.Error); ok { 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) return fmt.Errorf("Error updating project %q: %s", p.Name, err)
} }
} }
d.Partial(false)
return nil return nil
} }

View File

@ -658,6 +658,14 @@ data "google_iam_policy" "admin" {
`, pid, name, org) `, 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 { func testAccGoogleProject_create(pid, name, org string) string {
return fmt.Sprintf(` return fmt.Sprintf(`
resource "google_project" "acceptance" { resource "google_project" "acceptance" {

View File

@ -24,15 +24,44 @@ var (
originalPolicy *cloudresourcemanager.Policy originalPolicy *cloudresourcemanager.Policy
) )
// Test that a Project resource can be created and an IAM policy // Test that a Project resource can be created without an organization
// associated func TestAccGoogleProject_createWithoutOrg(t *testing.T) {
func TestAccGoogleProject_create(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) pid := "terraform-" + acctest.RandString(10)
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
Steps: []resource.TestStep{ 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{ resource.TestStep{
Config: testAccGoogleProject_create(pid, pname, org), Config: testAccGoogleProject_create(pid, pname, org),
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(

View File

@ -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 ## Argument Reference
The following arguments are supported: 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 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 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 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. * `id` - (Deprecated) The project ID.
This attribute has unexpected behaviour and probably does not work 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. [below](#id-field) for more information about its behaviour.
* `org_id` - (Optional) The numeric ID of the organization this project belongs to. * `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. Only one of
Changing this forces a new project to be created. `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 * `billing_account` - (Optional) The alphanumeric ID of the billing account this project
belongs to. The user or service account performing this operation with Terraform belongs to. The user or service account performing this operation with Terraform