mirror of
https://github.com/letic/terraform-provider-google.git
synced 2024-09-28 22:16:04 +00:00
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:
parent
f3ecd1ea1c
commit
c1d0e716d9
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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" {
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user