diff --git a/google/resource_google_project.go b/google/resource_google_project.go index 68ab2dc68af..ece7172a323 100644 --- a/google/resource_google_project.go +++ b/google/resource_google_project.go @@ -74,6 +74,12 @@ func resourceGoogleProject() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "labels": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, }, } } @@ -95,6 +101,10 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error }, } + if _, ok := d.GetOk("labels"); ok { + project.Labels = expandLabels(d) + } + op, err := config.clientResourceManager.Projects.Create(project).Do() if err != nil { return fmt.Errorf("Error creating project %s (%s): %s.", project.ProjectId, project.Name, err) @@ -142,6 +152,7 @@ func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error { d.Set("project_id", pid) d.Set("number", strconv.FormatInt(int64(p.ProjectNumber), 10)) d.Set("name", p.Name) + d.Set("labels", p.Labels) if p.Parent != nil { d.Set("org_id", p.Parent.Id) @@ -213,6 +224,17 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error updating billing account %q for project %q: %v", name, prefixedProject(pid), err) } } + + // Project Labels have changed + if ok := d.HasChange("labels"); ok { + p.Labels = expandLabels(d) + + // 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 nil } diff --git a/google/resource_google_project_test.go b/google/resource_google_project_test.go index ccafbb8d7c9..a3829f13732 100644 --- a/google/resource_google_project_test.go +++ b/google/resource_google_project_test.go @@ -3,9 +3,12 @@ package google import ( "fmt" "os" + "reflect" + "strconv" "strings" "testing" + "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" @@ -76,6 +79,23 @@ func TestAccGoogleProject_createBilling(t *testing.T) { }) } +// Test that a Project resource can be created with labels +func TestAccGoogleProject_createLabels(t *testing.T) { + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccGoogleProject_createLabels(pid, pname, org, "test", "that"), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectHasLabels("google_project.acceptance", pid, map[string]string{"test": "that"}), + ), + }, + }, + }) +} + // Test that a Project resource can be created and updated // with billing account information func TestAccGoogleProject_updateBilling(t *testing.T) { @@ -154,6 +174,48 @@ func TestAccGoogleProject_merge(t *testing.T) { }) } +// Test that a Project resource can be updated with labels +func TestAccGoogleProject_updateLabels(t *testing.T) { + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // create project without labels + { + Config: testAccGoogleProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + // update project with labels + { + Config: testAccGoogleProject_updateLabels(pid, pname, org, map[string]string{"label": "label-value"}), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + testAccCheckGoogleProjectHasLabels("google_project.acceptance", pid, map[string]string{"label": "label-value"}), + ), + }, + // update project with other labels + { + Config: testAccGoogleProject_updateLabels(pid, pname, org, map[string]string{"empty-label": ""}), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + testAccCheckGoogleProjectHasLabels("google_project.acceptance", pid, map[string]string{"empty-label": ""}), + ), + }, + // update project delete labels + { + Config: testAccGoogleProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + testAccCheckGoogleProjectHasNoLabels("google_project.acceptance", pid), + ), + }, + }, + }) +} + func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[r] @@ -212,6 +274,77 @@ func testAccCheckGoogleProjectHasMoreBindingsThan(pid string, count int) resourc } } +func testAccCheckGoogleProjectHasLabels(r, pid string, expected map[string]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[r] + if !ok { + return fmt.Errorf("Not found: %s", r) + } + + // State should have the same number of labels + if rs.Primary.Attributes["labels.%"] != strconv.Itoa(len(expected)) { + return fmt.Errorf("Expected %d labels, got %s", len(expected), rs.Primary.Attributes["labels.%"]) + } + + // Actual value in API should match state and expected + config := testAccProvider.Meta().(*Config) + + found, err := config.clientResourceManager.Projects.Get(pid).Do() + if err != nil { + return err + } + + actual := found.Labels + if !reflect.DeepEqual(actual, expected) { + // Determine only the different attributes + for k, v := range expected { + if av, ok := actual[k]; ok && v == av { + delete(expected, k) + delete(actual, k) + } + } + + spewConf := spew.NewDefaultConfig() + spewConf.SortKeys = true + return fmt.Errorf( + "Labels not equivalent. Difference is shown below. Top is actual, bottom is expected."+ + "\n\n%s\n\n%s", + spewConf.Sdump(actual), spewConf.Sdump(expected), + ) + } + return nil + } +} + +func testAccCheckGoogleProjectHasNoLabels(r, pid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[r] + if !ok { + return fmt.Errorf("Not found: %s", r) + } + + // State should have zero labels + if rs.Primary.Attributes["labels.%"] != "0" { + return fmt.Errorf("Expected 0 labels, got %s", rs.Primary.Attributes["labels.%"]) + } + + // Actual value in API should match state and expected + config := testAccProvider.Meta().(*Config) + + found, err := config.clientResourceManager.Projects.Get(pid).Do() + if err != nil { + return err + } + + spewConf := spew.NewDefaultConfig() + spewConf.SortKeys = true + if found.Labels != nil { + return fmt.Errorf("Labels should be empty. Actual \n%s", spewConf.Sdump(found.Labels)) + } + return nil + } +} + func testAccGoogleProject_toMerge(pid, name, org string) string { return fmt.Sprintf(` resource "google_project" "acceptance" { @@ -244,6 +377,35 @@ resource "google_project" "acceptance" { }`, pid, name, org) } +func testAccGoogleProject_createLabels(pid, name, org, key, value string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + labels { + "%s" = "%s" + } +}`, pid, name, org, key, value) +} + +func testAccGoogleProject_updateLabels(pid, name, org string, labels map[string]string) string { + r := fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" + labels {`, pid, name, org) + + l := "" + for key, value := range labels { + l += fmt.Sprintf("%q = %q\n", key, value) + } + + l += fmt.Sprintf("}\n}") + return r + l +} + func skipIfEnvNotSet(t *testing.T, envs ...string) { for _, k := range envs { if os.Getenv(k) == "" { diff --git a/website/docs/r/google_project.html.markdown b/website/docs/r/google_project.html.markdown index 97ba0428b30..43c84ddabbc 100755 --- a/website/docs/r/google_project.html.markdown +++ b/website/docs/r/google_project.html.markdown @@ -80,6 +80,8 @@ The following arguments are supported: This argument is no longer supported, and will be removed in a future version of Terraform. It should be replaced with a `google_project_iam_policy` resource. +* `labels` - (Optional) A set of key/value label pairs to assign to the project. + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are