2016-08-10 04:44:53 +00:00
package google
import (
"fmt"
"log"
"net/http"
"strconv"
2017-02-20 17:32:24 +00:00
"strings"
2018-04-16 21:25:01 +00:00
"time"
2016-08-10 04:44:53 +00:00
"github.com/hashicorp/terraform/helper/schema"
2017-02-20 17:32:24 +00:00
"google.golang.org/api/cloudbilling/v1"
2016-08-10 04:44:53 +00:00
"google.golang.org/api/cloudresourcemanager/v1"
"google.golang.org/api/googleapi"
)
2016-08-21 07:25:00 +00:00
// resourceGoogleProject returns a *schema.Resource that allows a customer
2016-11-23 06:55:40 +00:00
// to declare a Google Cloud Project resource.
2016-08-10 04:44:53 +00:00
func resourceGoogleProject ( ) * schema . Resource {
return & schema . Resource {
2016-11-23 06:55:40 +00:00
SchemaVersion : 1 ,
2016-08-10 04:44:53 +00:00
Create : resourceGoogleProjectCreate ,
Read : resourceGoogleProjectRead ,
Update : resourceGoogleProjectUpdate ,
Delete : resourceGoogleProjectDelete ,
2016-11-23 06:55:40 +00:00
2016-12-01 18:38:27 +00:00
Importer : & schema . ResourceImporter {
2018-04-10 22:23:04 +00:00
State : resourceProjectImportState ,
2016-12-01 18:38:27 +00:00
} ,
2016-11-23 06:55:40 +00:00
MigrateState : resourceGoogleProjectMigrateState ,
2016-08-10 04:44:53 +00:00
Schema : map [ string ] * schema . Schema {
2016-11-23 06:55:40 +00:00
"project_id" : & schema . Schema {
2016-08-10 04:44:53 +00:00
Type : schema . TypeString ,
2017-03-13 23:19:53 +00:00
Required : true ,
2016-08-10 04:44:53 +00:00
ForceNew : true ,
} ,
2016-11-23 06:55:40 +00:00
"skip_delete" : & schema . Schema {
Type : schema . TypeBool ,
2016-08-10 04:44:53 +00:00
Optional : true ,
2016-11-23 06:55:40 +00:00
Computed : true ,
2016-08-10 04:44:53 +00:00
} ,
2018-04-10 22:23:04 +00:00
"auto_create_network" : & schema . Schema {
Type : schema . TypeBool ,
Optional : true ,
Default : true ,
} ,
2016-08-10 04:44:53 +00:00
"name" : & schema . Schema {
Type : schema . TypeString ,
2017-03-13 23:19:53 +00:00
Required : true ,
2016-08-10 04:44:53 +00:00
} ,
2016-11-23 06:55:40 +00:00
"org_id" : & schema . Schema {
2017-09-22 18:03:08 +00:00
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 ,
2016-11-23 06:55:40 +00:00
} ,
"policy_data" : & schema . Schema {
2017-03-13 23:19:53 +00:00
Type : schema . TypeString ,
Optional : true ,
Computed : true ,
Removed : "Use the 'google_project_iam_policy' resource to define policies for a Google Project" ,
2016-11-23 06:55:40 +00:00
} ,
"policy_etag" : & schema . Schema {
2017-03-13 23:19:53 +00:00
Type : schema . TypeString ,
Computed : true ,
Removed : "Use the the 'google_project_iam_policy' resource to define policies for a Google Project" ,
2016-11-23 06:55:40 +00:00
} ,
2016-08-10 04:44:53 +00:00
"number" : & schema . Schema {
Type : schema . TypeString ,
Computed : true ,
} ,
2017-02-20 17:32:24 +00:00
"billing_account" : & schema . Schema {
Type : schema . TypeString ,
Optional : true ,
} ,
2017-09-14 17:39:21 +00:00
"labels" : & schema . Schema {
Type : schema . TypeMap ,
Optional : true ,
Elem : & schema . Schema { Type : schema . TypeString } ,
Set : schema . HashString ,
} ,
2016-08-10 04:44:53 +00:00
} ,
}
}
func resourceGoogleProjectCreate ( d * schema . ResourceData , meta interface { } ) error {
config := meta . ( * Config )
2016-11-23 06:55:40 +00:00
var pid string
var err error
pid = d . Get ( "project_id" ) . ( string )
log . Printf ( "[DEBUG]: Creating new project %q" , pid )
project := & cloudresourcemanager . Project {
ProjectId : pid ,
Name : d . Get ( "name" ) . ( string ) ,
}
2017-09-22 18:03:08 +00:00
getParentResourceId ( d , project )
2017-09-14 17:39:21 +00:00
if _ , ok := d . GetOk ( "labels" ) ; ok {
project . Labels = expandLabels ( d )
}
2016-11-23 06:55:40 +00:00
op , err := config . clientResourceManager . Projects . Create ( project ) . Do ( )
2016-08-10 04:44:53 +00:00
if err != nil {
2016-11-23 06:55:40 +00:00
return fmt . Errorf ( "Error creating project %s (%s): %s." , project . ProjectId , project . Name , err )
2016-08-10 04:44:53 +00:00
}
2016-11-23 06:55:40 +00:00
d . SetId ( pid )
// Wait for the operation to complete
waitErr := resourceManagerOperationWait ( config , op , "project to create" )
if waitErr != nil {
2017-04-13 23:16:47 +00:00
// The resource wasn't actually created
d . SetId ( "" )
2016-11-23 06:55:40 +00:00
return waitErr
2016-08-10 04:44:53 +00:00
}
2017-02-20 17:32:24 +00:00
// Set the billing account
2018-04-16 21:25:01 +00:00
if _ , ok := d . GetOk ( "billing_account" ) ; ok {
err = updateProjectBillingAccount ( d , config )
2017-02-20 17:32:24 +00:00
if err != nil {
2018-04-16 21:25:01 +00:00
return err
2017-02-20 17:32:24 +00:00
}
}
2018-04-10 22:23:04 +00:00
err = resourceGoogleProjectRead ( d , meta )
if err != nil {
return err
}
// There's no such thing as "don't auto-create network", only "delete the network
// post-creation" - but that's what it's called in the UI and let's not confuse
// people if we don't have to. The GCP Console is doing the same thing - creating
// a network and deleting it in the background.
if ! d . Get ( "auto_create_network" ) . ( bool ) {
2018-04-16 17:25:49 +00:00
// The compute API has to be enabled before we can delete a network.
if err = enableService ( "compute.googleapis.com" , project . ProjectId , config ) ; err != nil {
return fmt . Errorf ( "Error enabling the Compute Engine API required to delete the default network: %s" , err )
2018-04-10 22:23:04 +00:00
}
2018-04-16 17:25:49 +00:00
if err = forceDeleteComputeNetwork ( project . ProjectId , "default" , config ) ; err != nil {
return fmt . Errorf ( "Error deleting default network in project %s: %s" , project . ProjectId , err )
2018-04-10 22:23:04 +00:00
}
}
return nil
2016-08-10 04:44:53 +00:00
}
func resourceGoogleProjectRead ( d * schema . ResourceData , meta interface { } ) error {
config := meta . ( * Config )
2016-11-23 06:55:40 +00:00
pid := d . Id ( )
2016-08-10 04:44:53 +00:00
2016-11-23 06:55:40 +00:00
// Read the project
p , err := config . clientResourceManager . Projects . Get ( pid ) . Do ( )
2016-08-10 04:44:53 +00:00
if err != nil {
2017-05-09 23:00:47 +00:00
return handleNotFoundError ( err , d , fmt . Sprintf ( "Project %q" , pid ) )
2016-08-10 04:44:53 +00:00
}
2017-09-26 23:55:32 +00:00
// If the project has been deleted from outside Terraform, remove it from state file.
if p . LifecycleState != "ACTIVE" {
log . Printf ( "[WARN] Removing project '%s' because its state is '%s' (requires 'ACTIVE')." , pid , p . LifecycleState )
d . SetId ( "" )
return nil
}
2016-11-23 06:55:40 +00:00
d . Set ( "project_id" , pid )
2016-08-10 04:44:53 +00:00
d . Set ( "number" , strconv . FormatInt ( int64 ( p . ProjectNumber ) , 10 ) )
d . Set ( "name" , p . Name )
2017-09-14 17:39:21 +00:00
d . Set ( "labels" , p . Labels )
2016-08-10 04:44:53 +00:00
2016-11-23 06:55:40 +00:00
if p . Parent != nil {
2017-09-22 18:03:08 +00:00
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" , "" )
}
2016-11-23 06:55:40 +00:00
}
2017-02-20 17:32:24 +00:00
// Read the billing account
ba , err := config . clientBilling . Projects . GetBillingInfo ( prefixedProject ( pid ) ) . Do ( )
if err != nil {
return fmt . Errorf ( "Error reading billing account for project %q: %v" , prefixedProject ( pid ) , err )
}
if ba . BillingAccountName != "" {
// BillingAccountName is contains the resource name of the billing account
// associated with the project, if any. For example,
// `billingAccounts/012345-567890-ABCDEF`. We care about the ID and not
// the `billingAccounts/` prefix, so we need to remove that. If the
// prefix ever changes, we'll validate to make sure it's something we
// recognize.
_ba := strings . TrimPrefix ( ba . BillingAccountName , "billingAccounts/" )
if ba . BillingAccountName == _ba {
return fmt . Errorf ( "Error parsing billing account for project %q. Expected value to begin with 'billingAccounts/' but got %s" , prefixedProject ( pid ) , ba . BillingAccountName )
}
d . Set ( "billing_account" , _ba )
}
2016-08-10 04:44:53 +00:00
return nil
}
2017-02-20 17:32:24 +00:00
func prefixedProject ( pid string ) string {
return "projects/" + pid
}
2017-03-13 23:19:53 +00:00
2017-09-22 18:03:08 +00:00
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
}
2016-08-10 04:44:53 +00:00
func resourceGoogleProjectUpdate ( d * schema . ResourceData , meta interface { } ) error {
config := meta . ( * Config )
2016-11-23 06:55:40 +00:00
pid := d . Id ( )
2017-09-22 18:03:08 +00:00
project_name := d . Get ( "name" ) . ( string )
2016-11-23 06:55:40 +00:00
// Read the project
// we need the project even though refresh has already been called
// because the API doesn't support patch, so we need the actual object
p , err := config . clientResourceManager . Projects . Get ( pid ) . Do ( )
2016-08-10 04:44:53 +00:00
if err != nil {
2016-11-23 06:55:40 +00:00
if v , ok := err . ( * googleapi . Error ) ; ok && v . Code == http . StatusNotFound {
return fmt . Errorf ( "Project %q does not exist." , pid )
}
return fmt . Errorf ( "Error checking project %q: %s" , pid , err )
}
2017-09-22 18:03:08 +00:00
d . Partial ( true )
// Project display name has changed
2016-11-23 06:55:40 +00:00
if ok := d . HasChange ( "name" ) ; ok {
2017-09-22 18:03:08 +00:00
p . Name = project_name
2016-11-23 06:55:40 +00:00
// Do update on project
p , err = config . clientResourceManager . Projects . Update ( p . ProjectId , p ) . Do ( )
if err != nil {
2017-09-22 18:03:08 +00:00
return fmt . Errorf ( "Error updating project %q: %s" , project_name , err )
2016-11-23 06:55:40 +00:00
}
2017-09-22 18:03:08 +00:00
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" )
2016-11-23 06:55:40 +00:00
}
2017-02-20 17:32:24 +00:00
// Billing account has changed
if ok := d . HasChange ( "billing_account" ) ; ok {
2018-04-16 21:25:01 +00:00
err = updateProjectBillingAccount ( d , config )
2017-02-20 17:32:24 +00:00
if err != nil {
2018-04-16 21:25:01 +00:00
return err
2017-02-20 17:32:24 +00:00
}
}
2017-09-14 17:39:21 +00:00
// 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 {
2018-01-27 21:55:39 +00:00
return fmt . Errorf ( "Error updating project %q: %s" , project_name , err )
2017-09-14 17:39:21 +00:00
}
}
2017-09-22 18:03:08 +00:00
d . Partial ( false )
2017-03-13 23:19:53 +00:00
return nil
2016-11-23 06:55:40 +00:00
}
func resourceGoogleProjectDelete ( d * schema . ResourceData , meta interface { } ) error {
config := meta . ( * Config )
// Only delete projects if skip_delete isn't set
if ! d . Get ( "skip_delete" ) . ( bool ) {
pid := d . Id ( )
_ , err := config . clientResourceManager . Projects . Delete ( pid ) . Do ( )
if err != nil {
return fmt . Errorf ( "Error deleting project %q: %s" , pid , err )
}
2016-08-10 04:44:53 +00:00
}
2016-11-23 06:55:40 +00:00
d . SetId ( "" )
return nil
}
2018-04-10 22:23:04 +00:00
func resourceProjectImportState ( d * schema . ResourceData , meta interface { } ) ( [ ] * schema . ResourceData , error ) {
// Explicitly set to default as a workaround for `ImportStateVerify` tests, and so that users
// don't see a diff immediately after import.
d . Set ( "auto_create_network" , true )
return [ ] * schema . ResourceData { d } , nil
}
2018-04-16 17:25:49 +00:00
// Delete a compute network along with the firewall rules inside it.
func forceDeleteComputeNetwork ( projectId , networkName string , config * Config ) error {
networkLink := fmt . Sprintf ( "https://www.googleapis.com/compute/v1/projects/%s/global/networks/%s" , projectId , networkName )
token := ""
for paginate := true ; paginate ; {
filter := fmt . Sprintf ( "network eq %s" , networkLink )
resp , err := config . clientCompute . Firewalls . List ( projectId ) . Filter ( filter ) . Do ( )
if err != nil {
return fmt . Errorf ( "Error listing firewall rules in proj: %s" , err )
}
log . Printf ( "[DEBUG] Found %d firewall rules in %q network" , len ( resp . Items ) , networkName )
for _ , firewall := range resp . Items {
op , err := config . clientCompute . Firewalls . Delete ( projectId , firewall . Name ) . Do ( )
if err != nil {
return fmt . Errorf ( "Error deleting firewall: %s" , err )
}
err = computeSharedOperationWait ( config . clientCompute , op , projectId , "Deleting Firewall" )
if err != nil {
return err
}
}
token = resp . NextPageToken
paginate = token != ""
}
return deleteComputeNetwork ( projectId , networkName , config )
}
2018-04-16 21:25:01 +00:00
func updateProjectBillingAccount ( d * schema . ResourceData , config * Config ) error {
pid := d . Id ( )
name := d . Get ( "billing_account" ) . ( string )
ba := cloudbilling . ProjectBillingInfo { }
// If we're unlinking an existing billing account, an empty request does that, not an empty-string billing account.
if name != "" {
ba . BillingAccountName = "billingAccounts/" + 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 setting billing account %q for project %q: %v" , name , prefixedProject ( pid ) , _err )
}
return fmt . Errorf ( "Error setting billing account %q for project %q: %v" , name , prefixedProject ( pid ) , err )
}
for retries := 0 ; retries < 3 ; retries ++ {
err = resourceGoogleProjectRead ( d , config )
if err != nil {
return err
}
if d . Get ( "billing_account" ) . ( string ) == name {
break
}
time . Sleep ( 3 )
}
if d . Get ( "billing_account" ) . ( string ) != name {
return fmt . Errorf ( "Timed out waiting for billing account to return correct value. Waiting for %s, got %s." ,
d . Get ( "billding_account" ) . ( string ) , name )
}
return nil
}