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 }