github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/google/resource_google_project.go (about)

     1  package google
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/terraform/helper/schema"
    12  	"google.golang.org/api/cloudbilling/v1"
    13  	"google.golang.org/api/cloudresourcemanager/v1"
    14  	"google.golang.org/api/googleapi"
    15  )
    16  
    17  // resourceGoogleProject returns a *schema.Resource that allows a customer
    18  // to declare a Google Cloud Project resource.
    19  //
    20  // This example shows a project with a policy declared in config:
    21  //
    22  // resource "google_project" "my-project" {
    23  //    project = "a-project-id"
    24  //    policy = "${data.google_iam_policy.admin.policy}"
    25  // }
    26  func resourceGoogleProject() *schema.Resource {
    27  	return &schema.Resource{
    28  		SchemaVersion: 1,
    29  
    30  		Create: resourceGoogleProjectCreate,
    31  		Read:   resourceGoogleProjectRead,
    32  		Update: resourceGoogleProjectUpdate,
    33  		Delete: resourceGoogleProjectDelete,
    34  
    35  		Importer: &schema.ResourceImporter{
    36  			State: schema.ImportStatePassthrough,
    37  		},
    38  		MigrateState: resourceGoogleProjectMigrateState,
    39  
    40  		Schema: map[string]*schema.Schema{
    41  			"id": &schema.Schema{
    42  				Type:       schema.TypeString,
    43  				Optional:   true,
    44  				Computed:   true,
    45  				Deprecated: "The id field has unexpected behaviour and probably doesn't do what you expect. See https://www.terraform.io/docs/providers/google/r/google_project.html#id-field for more information. Please use project_id instead; future versions of Terraform will remove the id field.",
    46  			},
    47  			"project_id": &schema.Schema{
    48  				Type:     schema.TypeString,
    49  				Optional: true,
    50  				ForceNew: true,
    51  				DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
    52  					// This suppresses the diff if project_id is not set
    53  					if new == "" {
    54  						return true
    55  					}
    56  					return false
    57  				},
    58  			},
    59  			"skip_delete": &schema.Schema{
    60  				Type:     schema.TypeBool,
    61  				Optional: true,
    62  				Computed: true,
    63  			},
    64  			"name": &schema.Schema{
    65  				Type:     schema.TypeString,
    66  				Optional: true,
    67  				Computed: true,
    68  			},
    69  			"org_id": &schema.Schema{
    70  				Type:     schema.TypeString,
    71  				Optional: true,
    72  				Computed: true,
    73  				ForceNew: true,
    74  			},
    75  			"policy_data": &schema.Schema{
    76  				Type:             schema.TypeString,
    77  				Optional:         true,
    78  				Computed:         true,
    79  				Deprecated:       "Use the 'google_project_iam_policy' resource to define policies for a Google Project",
    80  				DiffSuppressFunc: jsonPolicyDiffSuppress,
    81  			},
    82  			"policy_etag": &schema.Schema{
    83  				Type:       schema.TypeString,
    84  				Computed:   true,
    85  				Deprecated: "Use the the 'google_project_iam_policy' resource to define policies for a Google Project",
    86  			},
    87  			"number": &schema.Schema{
    88  				Type:     schema.TypeString,
    89  				Computed: true,
    90  			},
    91  			"billing_account": &schema.Schema{
    92  				Type:     schema.TypeString,
    93  				Optional: true,
    94  			},
    95  		},
    96  	}
    97  }
    98  
    99  func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error {
   100  	config := meta.(*Config)
   101  
   102  	var pid string
   103  	var err error
   104  	pid = d.Get("project_id").(string)
   105  	if pid == "" {
   106  		pid, err = getProject(d, config)
   107  		if err != nil {
   108  			return fmt.Errorf("Error getting project ID: %v", err)
   109  		}
   110  		if pid == "" {
   111  			return fmt.Errorf("'project_id' must be set in the config")
   112  		}
   113  	}
   114  
   115  	// we need to check if name and org_id are set, and throw an error if they aren't
   116  	// we can't just set these as required on the object, however, as that would break
   117  	// all configs that used previous iterations of the resource.
   118  	// TODO(paddy): remove this for 0.9 and set these attributes as required.
   119  	name, org_id := d.Get("name").(string), d.Get("org_id").(string)
   120  	if name == "" {
   121  		return fmt.Errorf("`name` must be set in the config if you're creating a project.")
   122  	}
   123  	if org_id == "" {
   124  		return fmt.Errorf("`org_id` must be set in the config if you're creating a project.")
   125  	}
   126  
   127  	log.Printf("[DEBUG]: Creating new project %q", pid)
   128  	project := &cloudresourcemanager.Project{
   129  		ProjectId: pid,
   130  		Name:      d.Get("name").(string),
   131  		Parent: &cloudresourcemanager.ResourceId{
   132  			Id:   d.Get("org_id").(string),
   133  			Type: "organization",
   134  		},
   135  	}
   136  
   137  	op, err := config.clientResourceManager.Projects.Create(project).Do()
   138  	if err != nil {
   139  		return fmt.Errorf("Error creating project %s (%s): %s.", project.ProjectId, project.Name, err)
   140  	}
   141  
   142  	d.SetId(pid)
   143  
   144  	// Wait for the operation to complete
   145  	waitErr := resourceManagerOperationWait(config, op, "project to create")
   146  	if waitErr != nil {
   147  		return waitErr
   148  	}
   149  
   150  	// Apply the IAM policy if it is set
   151  	if pString, ok := d.GetOk("policy_data"); ok {
   152  		// The policy string is just a marshaled cloudresourcemanager.Policy.
   153  		// Unmarshal it to a struct.
   154  		var policy cloudresourcemanager.Policy
   155  		if err := json.Unmarshal([]byte(pString.(string)), &policy); err != nil {
   156  			return err
   157  		}
   158  		log.Printf("[DEBUG] Got policy from config: %#v", policy.Bindings)
   159  
   160  		// Retrieve existing IAM policy from project. This will be merged
   161  		// with the policy defined here.
   162  		p, err := getProjectIamPolicy(pid, config)
   163  		if err != nil {
   164  			return err
   165  		}
   166  		log.Printf("[DEBUG] Got existing bindings from project: %#v", p.Bindings)
   167  
   168  		// Merge the existing policy bindings with those defined in this manifest.
   169  		p.Bindings = mergeBindings(append(p.Bindings, policy.Bindings...))
   170  
   171  		// Apply the merged policy
   172  		log.Printf("[DEBUG] Setting new policy for project: %#v", p)
   173  		_, err = config.clientResourceManager.Projects.SetIamPolicy(pid,
   174  			&cloudresourcemanager.SetIamPolicyRequest{Policy: p}).Do()
   175  
   176  		if err != nil {
   177  			return fmt.Errorf("Error applying IAM policy for project %q: %s", pid, err)
   178  		}
   179  	}
   180  
   181  	// Set the billing account
   182  	if v, ok := d.GetOk("billing_account"); ok {
   183  		name := v.(string)
   184  		ba := cloudbilling.ProjectBillingInfo{
   185  			BillingAccountName: "billingAccounts/" + name,
   186  		}
   187  		_, err = config.clientBilling.Projects.UpdateBillingInfo(prefixedProject(pid), &ba).Do()
   188  		if err != nil {
   189  			d.Set("billing_account", "")
   190  			if _err, ok := err.(*googleapi.Error); ok {
   191  				return fmt.Errorf("Error setting billing account %q for project %q: %v", name, prefixedProject(pid), _err)
   192  			}
   193  			return fmt.Errorf("Error setting billing account %q for project %q: %v", name, prefixedProject(pid), err)
   194  		}
   195  	}
   196  
   197  	return resourceGoogleProjectRead(d, meta)
   198  }
   199  
   200  func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error {
   201  	config := meta.(*Config)
   202  	pid := d.Id()
   203  
   204  	// Read the project
   205  	p, err := config.clientResourceManager.Projects.Get(pid).Do()
   206  	if err != nil {
   207  		if v, ok := err.(*googleapi.Error); ok && v.Code == http.StatusNotFound {
   208  			return fmt.Errorf("Project %q does not exist.", pid)
   209  		}
   210  		return fmt.Errorf("Error checking project %q: %s", pid, err)
   211  	}
   212  
   213  	d.Set("project_id", pid)
   214  	d.Set("number", strconv.FormatInt(int64(p.ProjectNumber), 10))
   215  	d.Set("name", p.Name)
   216  
   217  	if p.Parent != nil {
   218  		d.Set("org_id", p.Parent.Id)
   219  	}
   220  
   221  	// Read the billing account
   222  	ba, err := config.clientBilling.Projects.GetBillingInfo(prefixedProject(pid)).Do()
   223  	if err != nil {
   224  		return fmt.Errorf("Error reading billing account for project %q: %v", prefixedProject(pid), err)
   225  	}
   226  	if ba.BillingAccountName != "" {
   227  		// BillingAccountName is contains the resource name of the billing account
   228  		// associated with the project, if any. For example,
   229  		// `billingAccounts/012345-567890-ABCDEF`. We care about the ID and not
   230  		// the `billingAccounts/` prefix, so we need to remove that. If the
   231  		// prefix ever changes, we'll validate to make sure it's something we
   232  		// recognize.
   233  		_ba := strings.TrimPrefix(ba.BillingAccountName, "billingAccounts/")
   234  		if ba.BillingAccountName == _ba {
   235  			return fmt.Errorf("Error parsing billing account for project %q. Expected value to begin with 'billingAccounts/' but got %s", prefixedProject(pid), ba.BillingAccountName)
   236  		}
   237  		d.Set("billing_account", _ba)
   238  	}
   239  	return nil
   240  }
   241  
   242  func prefixedProject(pid string) string {
   243  	return "projects/" + pid
   244  }
   245  func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error {
   246  	config := meta.(*Config)
   247  	pid := d.Id()
   248  
   249  	// Read the project
   250  	// we need the project even though refresh has already been called
   251  	// because the API doesn't support patch, so we need the actual object
   252  	p, err := config.clientResourceManager.Projects.Get(pid).Do()
   253  	if err != nil {
   254  		if v, ok := err.(*googleapi.Error); ok && v.Code == http.StatusNotFound {
   255  			return fmt.Errorf("Project %q does not exist.", pid)
   256  		}
   257  		return fmt.Errorf("Error checking project %q: %s", pid, err)
   258  	}
   259  
   260  	// Project name has changed
   261  	if ok := d.HasChange("name"); ok {
   262  		p.Name = d.Get("name").(string)
   263  		// Do update on project
   264  		p, err = config.clientResourceManager.Projects.Update(p.ProjectId, p).Do()
   265  		if err != nil {
   266  			return fmt.Errorf("Error updating project %q: %s", p.Name, err)
   267  		}
   268  	}
   269  
   270  	// Billing account has changed
   271  	if ok := d.HasChange("billing_account"); ok {
   272  		name := d.Get("billing_account").(string)
   273  		ba := cloudbilling.ProjectBillingInfo{
   274  			BillingAccountName: "billingAccounts/" + name,
   275  		}
   276  		_, err = config.clientBilling.Projects.UpdateBillingInfo(prefixedProject(pid), &ba).Do()
   277  		if err != nil {
   278  			d.Set("billing_account", "")
   279  			if _err, ok := err.(*googleapi.Error); ok {
   280  				return fmt.Errorf("Error updating billing account %q for project %q: %v", name, prefixedProject(pid), _err)
   281  			}
   282  			return fmt.Errorf("Error updating billing account %q for project %q: %v", name, prefixedProject(pid), err)
   283  		}
   284  	}
   285  	return updateProjectIamPolicy(d, config, pid)
   286  }
   287  
   288  func resourceGoogleProjectDelete(d *schema.ResourceData, meta interface{}) error {
   289  	config := meta.(*Config)
   290  	// Only delete projects if skip_delete isn't set
   291  	if !d.Get("skip_delete").(bool) {
   292  		pid := d.Id()
   293  		_, err := config.clientResourceManager.Projects.Delete(pid).Do()
   294  		if err != nil {
   295  			return fmt.Errorf("Error deleting project %q: %s", pid, err)
   296  		}
   297  	}
   298  	d.SetId("")
   299  	return nil
   300  }
   301  
   302  func updateProjectIamPolicy(d *schema.ResourceData, config *Config, pid string) error {
   303  	// Policy has changed
   304  	if ok := d.HasChange("policy_data"); ok {
   305  		// The policy string is just a marshaled cloudresourcemanager.Policy.
   306  		// Unmarshal it to a struct that contains the old and new policies
   307  		oldP, newP := d.GetChange("policy_data")
   308  		oldPString := oldP.(string)
   309  		newPString := newP.(string)
   310  
   311  		// JSON Unmarshaling would fail
   312  		if oldPString == "" {
   313  			oldPString = "{}"
   314  		}
   315  		if newPString == "" {
   316  			newPString = "{}"
   317  		}
   318  
   319  		log.Printf("[DEBUG]: Old policy: %q\nNew policy: %q", oldPString, newPString)
   320  
   321  		var oldPolicy, newPolicy cloudresourcemanager.Policy
   322  		if err := json.Unmarshal([]byte(newPString), &newPolicy); err != nil {
   323  			return err
   324  		}
   325  		if err := json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil {
   326  			return err
   327  		}
   328  
   329  		// Find any Roles and Members that were removed (i.e., those that are present
   330  		// in the old but absent in the new
   331  		oldMap := rolesToMembersMap(oldPolicy.Bindings)
   332  		newMap := rolesToMembersMap(newPolicy.Bindings)
   333  		deleted := make(map[string]map[string]bool)
   334  
   335  		// Get each role and its associated members in the old state
   336  		for role, members := range oldMap {
   337  			// Initialize map for role
   338  			if _, ok := deleted[role]; !ok {
   339  				deleted[role] = make(map[string]bool)
   340  			}
   341  			// The role exists in the new state
   342  			if _, ok := newMap[role]; ok {
   343  				// Check each memeber
   344  				for member, _ := range members {
   345  					// Member does not exist in new state, so it was deleted
   346  					if _, ok = newMap[role][member]; !ok {
   347  						deleted[role][member] = true
   348  					}
   349  				}
   350  			} else {
   351  				// This indicates an entire role was deleted. Mark all members
   352  				// for delete.
   353  				for member, _ := range members {
   354  					deleted[role][member] = true
   355  				}
   356  			}
   357  		}
   358  		log.Printf("[DEBUG] Roles and Members to be deleted: %#v", deleted)
   359  
   360  		// Retrieve existing IAM policy from project. This will be merged
   361  		// with the policy in the current state
   362  		// TODO(evanbrown): Add an 'authoritative' flag that allows policy
   363  		// in manifest to overwrite existing policy.
   364  		p, err := getProjectIamPolicy(pid, config)
   365  		if err != nil {
   366  			return err
   367  		}
   368  		log.Printf("[DEBUG] Got existing bindings from project: %#v", p.Bindings)
   369  
   370  		// Merge existing policy with policy in the current state
   371  		log.Printf("[DEBUG] Merging new bindings from project: %#v", newPolicy.Bindings)
   372  		mergedBindings := mergeBindings(append(p.Bindings, newPolicy.Bindings...))
   373  
   374  		// Remove any roles and members that were explicitly deleted
   375  		mergedBindingsMap := rolesToMembersMap(mergedBindings)
   376  		for role, members := range deleted {
   377  			for member, _ := range members {
   378  				delete(mergedBindingsMap[role], member)
   379  			}
   380  		}
   381  
   382  		p.Bindings = rolesToMembersBinding(mergedBindingsMap)
   383  		dump, _ := json.MarshalIndent(p.Bindings, " ", "  ")
   384  		log.Printf("[DEBUG] Setting new policy for project: %#v:\n%s", p, string(dump))
   385  
   386  		_, err = config.clientResourceManager.Projects.SetIamPolicy(pid,
   387  			&cloudresourcemanager.SetIamPolicyRequest{Policy: p}).Do()
   388  
   389  		if err != nil {
   390  			return fmt.Errorf("Error applying IAM policy for project %q: %s", pid, err)
   391  		}
   392  	}
   393  	return nil
   394  }