blob: 973093e7c6c04461e9e2b578b0bd75ebf19f3cd4 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package resourcemanager_test
import (
"fmt"
"os"
"reflect"
"strconv"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/acctest"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/envvar"
"github.com/hashicorp/terraform-provider-google-beta/google-beta/services/resourcemanager"
transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport"
)
var (
TestPrefix = "tf-test"
)
// Test that a Project resource can be created without an organization
func TestAccProject_createWithoutOrg(t *testing.T) {
t.Parallel()
creds := transport_tpg.MultiEnvSearch(envvar.CredsEnvVars)
if strings.Contains(creds, "iam.gserviceaccount.com") {
t.Skip("Service accounts cannot create projects without a parent. Requires user credentials.")
}
pid := fmt.Sprintf("%s-%d", TestPrefix, acctest.RandInt(t))
acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
// This step creates a new project
{
Config: testAccProject_createWithoutOrg(pid),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectExists("google_project.acceptance", pid),
),
},
},
})
}
// Test that a Project resource can be created and an IAM policy
// associated
func TestAccProject_create(t *testing.T) {
t.Parallel()
org := envvar.GetTestOrgFromEnv(t)
pid := fmt.Sprintf("%s-%d", TestPrefix, acctest.RandInt(t))
acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
// This step creates a new project
{
Config: testAccProject_create(pid, org),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectExists("google_project.acceptance", pid),
),
},
},
})
}
// Test that a Project resource can be created with an associated
// billing account
func TestAccProject_billing(t *testing.T) {
t.Parallel()
org := envvar.GetTestOrgFromEnv(t)
// This is a second billing account that can be charged, which is used only in this test to
// verify that a project can update its billing account.
envvar.SkipIfEnvNotSet(t, "GOOGLE_BILLING_ACCOUNT_2")
billingId2 := os.Getenv("GOOGLE_BILLING_ACCOUNT_2")
billingId := envvar.GetTestBillingAccountFromEnv(t)
pid := fmt.Sprintf("%s-%d", TestPrefix, acctest.RandInt(t))
acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
// This step creates a new project with a billing account
{
Config: testAccProject_createBilling(pid, org, billingId),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectHasBillingAccount(t, "google_project.acceptance", pid, billingId),
),
},
// Make sure import supports billing account
{
ResourceName: "google_project.acceptance",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"skip_delete"},
},
// Update to a different billing account
{
Config: testAccProject_createBilling(pid, org, billingId2),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectHasBillingAccount(t, "google_project.acceptance", pid, billingId2),
),
},
// Unlink the billing account
{
Config: testAccProject_create(pid, org),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectHasBillingAccount(t, "google_project.acceptance", pid, ""),
),
},
},
})
}
// Test that a Project resource can be created with labels
func TestAccProject_labels(t *testing.T) {
t.Parallel()
org := envvar.GetTestOrgFromEnv(t)
pid := fmt.Sprintf("%s-%d", TestPrefix, acctest.RandInt(t))
acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccProject_labels(pid, org, map[string]string{"test": "that"}),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectHasLabels(t, "google_project.acceptance", pid, map[string]string{"test": "that"}),
),
},
// Make sure import supports labels
{
ResourceName: "google_project.acceptance",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"skip_delete", "labels", "terraform_labels"},
},
// update project with labels
{
Config: testAccProject_labels(pid, org, map[string]string{"label": "label-value"}),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectExists("google_project.acceptance", pid),
testAccCheckGoogleProjectHasLabels(t, "google_project.acceptance", pid, map[string]string{"label": "label-value"}),
),
},
// update project delete labels
{
Config: testAccProject_create(pid, org),
Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectExists("google_project.acceptance", pid),
testAccCheckGoogleProjectHasNoLabels(t, "google_project.acceptance", pid),
),
},
},
})
}
func TestAccProject_deleteDefaultNetwork(t *testing.T) {
t.Parallel()
org := envvar.GetTestOrgFromEnv(t)
pid := fmt.Sprintf("%s-%d", TestPrefix, acctest.RandInt(t))
billingId := envvar.GetTestBillingAccountFromEnv(t)
acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccProject_deleteDefaultNetwork(pid, org, billingId),
},
},
})
}
func TestAccProject_parentFolder(t *testing.T) {
t.Parallel()
org := envvar.GetTestOrgFromEnv(t)
pid := fmt.Sprintf("%s-%d", TestPrefix, acctest.RandInt(t))
folderDisplayName := TestPrefix + acctest.RandString(t, 10)
acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccProject_parentFolder(pid, folderDisplayName, org),
},
},
})
}
func TestAccProject_migrateParent(t *testing.T) {
t.Parallel()
org := envvar.GetTestOrgFromEnv(t)
pid := fmt.Sprintf("%s-%d", TestPrefix, acctest.RandInt(t))
folderDisplayName := TestPrefix + acctest.RandString(t, 10)
acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccProject_migrateParentFolder(pid, folderDisplayName, org),
},
{
ResourceName: "google_project.acceptance",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"skip_delete"},
},
{
Config: testAccProject_migrateParentOrg(pid, folderDisplayName, org),
},
{
ResourceName: "google_project.acceptance",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"skip_delete"},
},
{
Config: testAccProject_migrateParentFolder(pid, folderDisplayName, org),
},
{
ResourceName: "google_project.acceptance",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"skip_delete"},
},
},
})
}
func testAccCheckGoogleProjectExists(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)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No ID is set")
}
projectId := fmt.Sprintf("projects/%s", pid)
if rs.Primary.ID != projectId {
return fmt.Errorf("Expected project %q to match ID %q in state", projectId, rs.Primary.ID)
}
return nil
}
}
func testAccCheckGoogleProjectHasBillingAccount(t *testing.T, r, pid, billingId 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 match expected
if rs.Primary.Attributes["billing_account"] != billingId {
return fmt.Errorf("Billing ID in state (%s) does not match expected value (%s)", rs.Primary.Attributes["billing_account"], billingId)
}
// Actual value in API should match state and expected
// Read the billing account
config := acctest.GoogleProviderConfig(t)
ba, err := config.NewBillingClient(config.UserAgent).Projects.GetBillingInfo(resourcemanager.PrefixedProject(pid)).Do()
if err != nil {
return fmt.Errorf("Error reading billing account for project %q: %v", resourcemanager.PrefixedProject(pid), err)
}
if billingId != strings.TrimPrefix(ba.BillingAccountName, "billingAccounts/") {
return fmt.Errorf("Billing ID returned by API (%s) did not match expected value (%s)", ba.BillingAccountName, billingId)
}
return nil
}
}
func testAccCheckGoogleProjectHasLabels(t *testing.T, 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 := acctest.GoogleProviderConfig(t)
found, err := config.NewResourceManagerClient(config.UserAgent).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(t *testing.T, 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 v, ok := rs.Primary.Attributes["labels.%"]; ok && v != "0" {
return fmt.Errorf("Expected 0 labels, got %s", rs.Primary.Attributes["labels.%"])
}
// Actual value in API should match state and expected
config := acctest.GoogleProviderConfig(t)
found, err := config.NewResourceManagerClient(config.UserAgent).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 testAccProject_createWithoutOrg(pid string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
}
`, pid, pid)
}
func testAccProject_createBilling(pid, org, billing string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
org_id = "%s"
billing_account = "%s"
}
`, pid, pid, org, billing)
}
func testAccProject_labels(pid, org string, labels map[string]string) string {
r := fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
org_id = "%s"
labels = {`, pid, pid, org)
l := ""
for key, value := range labels {
l += fmt.Sprintf("%q = %q\n", key, value)
}
l += fmt.Sprintf("}\n}")
return r + l
}
func testAccProject_deleteDefaultNetwork(pid, org, billing string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
org_id = "%s"
billing_account = "%s" # requires billing to enable compute API
auto_create_network = false
}
`, pid, pid, org, billing)
}
func testAccProject_parentFolder(pid, folderName, org string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
folder_id = google_folder.folder1.id
}
resource "google_folder" "folder1" {
display_name = "%s"
parent = "organizations/%s"
}
`, pid, pid, folderName, org)
}
func testAccProject_migrateParentFolder(pid, folderName, org string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
folder_id = google_folder.folder1.id
}
resource "google_folder" "folder1" {
display_name = "%s"
parent = "organizations/%s"
}
`, pid, pid, folderName, org)
}
func testAccProject_migrateParentOrg(pid, folderName, org string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
name = "%s"
org_id = "%s"
}
resource "google_folder" "folder1" {
display_name = "%s"
parent = "organizations/%s"
}
`, pid, pid, org, folderName, org)
}