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,
},
"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
}

View File

@ -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" {

View File

@ -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(

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
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