Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create, update, move and delete a GCP folders. #416

Merged
merged 6 commits into from
Sep 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions google/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"google.golang.org/api/bigquery/v2"
"google.golang.org/api/cloudbilling/v1"
"google.golang.org/api/cloudresourcemanager/v1"
resourceManagerV2Beta1 "google.golang.org/api/cloudresourcemanager/v2beta1"
computeBeta "google.golang.org/api/compute/v0.beta"
"google.golang.org/api/compute/v1"
"google.golang.org/api/container/v1"
Expand All @@ -40,21 +41,22 @@ type Config struct {
Project string
Region string

clientBilling *cloudbilling.Service
clientCompute *compute.Service
clientComputeBeta *computeBeta.Service
clientContainer *container.Service
clientDns *dns.Service
clientPubsub *pubsub.Service
clientResourceManager *cloudresourcemanager.Service
clientRuntimeconfig *runtimeconfig.Service
clientSpanner *spanner.Service
clientSourceRepo *sourcerepo.Service
clientStorage *storage.Service
clientSqlAdmin *sqladmin.Service
clientIAM *iam.Service
clientServiceMan *servicemanagement.APIService
clientBigQuery *bigquery.Service
clientBilling *cloudbilling.Service
clientCompute *compute.Service
clientComputeBeta *computeBeta.Service
clientContainer *container.Service
clientDns *dns.Service
clientPubsub *pubsub.Service
clientResourceManager *cloudresourcemanager.Service
clientResourceManagerV2Beta1 *resourceManagerV2Beta1.Service
clientRuntimeconfig *runtimeconfig.Service
clientSpanner *spanner.Service
clientSourceRepo *sourcerepo.Service
clientStorage *storage.Service
clientSqlAdmin *sqladmin.Service
clientIAM *iam.Service
clientServiceMan *servicemanagement.APIService
clientBigQuery *bigquery.Service

bigtableClientFactory *BigtableClientFactory
}
Expand Down Expand Up @@ -179,6 +181,13 @@ func (c *Config) loadAndValidate() error {
}
c.clientResourceManager.UserAgent = userAgent

log.Printf("[INFO] Instantiating Google Cloud ResourceManager V Client...")
c.clientResourceManagerV2Beta1, err = resourceManagerV2Beta1.New(client)
if err != nil {
return err
}
c.clientResourceManagerV2Beta1.UserAgent = userAgent

log.Printf("[INFO] Instantiating Google Cloud Runtimeconfig Client...")
c.clientRuntimeconfig, err = runtimeconfig.New(client)
if err != nil {
Expand Down
32 changes: 32 additions & 0 deletions google/import_google_folder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package google

import (
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"os"
"testing"
)

func TestAccGoogleFolder_import(t *testing.T) {
skipIfEnvNotSet(t, "GOOGLE_ORG")

folderDisplayName := "tf-test-" + acctest.RandString(10)
org := os.Getenv("GOOGLE_ORG")
parent := "organizations/" + org

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckGoogleFolderDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccGoogleFolder_basic(folderDisplayName, parent),
},
resource.TestStep{
ResourceName: "google_folder.folder1",
ImportState: true,
ImportStateVerify: true,
},
},
})
}
1 change: 1 addition & 0 deletions google/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func Provider() terraform.ResourceProvider {
"google_container_node_pool": resourceContainerNodePool(),
"google_dns_managed_zone": resourceDnsManagedZone(),
"google_dns_record_set": resourceDnsRecordSet(),
"google_folder": resourceGoogleFolder(),
"google_sourcerepo_repository": resourceSourceRepoRepository(),
"google_spanner_instance": resourceSpannerInstance(),
"google_spanner_database": resourceSpannerDatabase(),
Expand Down
168 changes: 168 additions & 0 deletions google/resource_google_folder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package google

import (
"fmt"
"github.com/hashicorp/terraform/helper/schema"
resourceManagerV2Beta1 "google.golang.org/api/cloudresourcemanager/v2beta1"
"strings"
)

func resourceGoogleFolder() *schema.Resource {
return &schema.Resource{
Create: resourceGoogleFolderCreate,
Read: resourceGoogleFolderRead,
Update: resourceGoogleFolderUpdate,
Delete: resourceGoogleFolderDelete,

Importer: &schema.ResourceImporter{
State: resourceGoogleFolderImportState,
},

Schema: map[string]*schema.Schema{
// Format is either folders/{folder_id} or organizations/{org_id}.
"parent": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
// Must be unique amongst its siblings.
"display_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},

// Format is 'folders/{folder_id}.
// The terraform id holds the same value.
"name": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"lifecycle_state": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"create_time": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceGoogleFolderCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

displayName := d.Get("display_name").(string)
parent := d.Get("parent").(string)

op, err := config.clientResourceManagerV2Beta1.Folders.Create(&resourceManagerV2Beta1.Folder{
DisplayName: displayName,
}).Parent(parent).Do()

if err != nil {
return fmt.Errorf("Error creating folder '%s' in '%s': %s", displayName, parent, err)
}

err = resourceManagerV2Beta1OperationWait(config, op, "creating folder")

if err != nil {
return fmt.Errorf("Error creating folder '%s' in '%s': %s", displayName, parent, err)
}

// Since we waited above, the operation is guaranteed to have been successful by this point.
waitOp, err := config.clientResourceManager.Operations.Get(op.Name).Do()
if err != nil {
return fmt.Errorf("The folder '%s' has been created but we could not retrieve its id. Delete the folder manually and retry or use 'terraform import': %s", displayName, err)
}

// Requires 3 successive checks for safety. Nested IFs are used to avoid 3 error statement with the same message.
if response, ok := waitOp.Response.(map[string]interface{}); ok {
if val, ok := response["name"]; ok {
if name, ok := val.(string); ok {
d.SetId(name)
return resourceGoogleFolderRead(d, meta)
}
}
}
return fmt.Errorf("The folder '%s' has been created but we could not retrieve its id. Delete the folder manually and retry or use 'terraform import'", displayName)
}

func resourceGoogleFolderRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

folder, err := config.clientResourceManagerV2Beta1.Folders.Get(d.Id()).Do()
if err != nil {
return handleNotFoundError(err, d, d.Id())
}

d.Set("name", folder.Name)
d.Set("parent", folder.Parent)
d.Set("display_name", folder.DisplayName)
d.Set("lifecycle_state", folder.LifecycleState)
d.Set("create_time", folder.CreateTime)

return nil
}

func resourceGoogleFolderUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
displayName := d.Get("display_name").(string)

d.Partial(true)
if d.HasChange("display_name") {
_, err := config.clientResourceManagerV2Beta1.Folders.Patch(d.Id(), &resourceManagerV2Beta1.Folder{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any way you could combine the patch code for display_name and parent into one api call? This would result in less code and be less likely to fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No we can't. The display_name must be updated with Patch and the parent must be updated with Move

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed that, thanks.

DisplayName: displayName,
}).Do()

if err != nil {
return fmt.Errorf("Error updating display_name to '%s': %s", displayName, err)
}

d.SetPartial("display_name")
}

if d.HasChange("parent") {
newParent := d.Get("parent").(string)
op, err := config.clientResourceManagerV2Beta1.Folders.Move(d.Id(), &resourceManagerV2Beta1.MoveFolderRequest{
DestinationParent: newParent,
}).Do()

if err != nil {
return fmt.Errorf("Error moving folder '%s' to '%s': %s", displayName, newParent, err)
}

err = resourceManagerV2Beta1OperationWait(config, op, "move folder")
if err != nil {
return fmt.Errorf("Error moving folder '%s' to '%s': %s", displayName, newParent, err)
}

d.SetPartial("parent")
}

d.Partial(false)

return nil
}

func resourceGoogleFolderDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
displayName := d.Get("display_name").(string)

_, err := config.clientResourceManagerV2Beta1.Folders.Delete(d.Id()).Do()
if err != nil {
return fmt.Errorf("Error deleting folder %s", displayName)
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a d.SetId("") call to indicate successful deletion

Although apparently that isn't needed since the tests passed? I'm curious if we should be doing that or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In resource.go in terraform core:

// Destroy the resource since it is created
if err := r.Delete(data, meta); err != nil {
  return r.recordCurrentSchemaVersion(data.State()), err
}

// Make sure the ID is gone.
data.SetId("")

It does it for us. Calling it again won't cause harm. Should I add it for consistency?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 👍 on not setting it, except for in non-Delete methods (Read when not found, etc.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know, will omit that on delete in the future.

return nil
}

func resourceGoogleFolderImportState(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
id := d.Id()

if !strings.HasPrefix(d.Id(), "folders/") {
id = fmt.Sprintf("folders/%s", id)
}

d.SetId(id)

return []*schema.ResourceData{d}, nil
}
Loading