github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/builtin/providers/google/resource_google_service_account.go (about)

     1  package google
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"log"
     7  
     8  	"github.com/hashicorp/terraform/helper/schema"
     9  	"google.golang.org/api/googleapi"
    10  	"google.golang.org/api/iam/v1"
    11  )
    12  
    13  func resourceGoogleServiceAccount() *schema.Resource {
    14  	return &schema.Resource{
    15  		Create: resourceGoogleServiceAccountCreate,
    16  		Read:   resourceGoogleServiceAccountRead,
    17  		Delete: resourceGoogleServiceAccountDelete,
    18  		Update: resourceGoogleServiceAccountUpdate,
    19  		Schema: map[string]*schema.Schema{
    20  			"email": &schema.Schema{
    21  				Type:     schema.TypeString,
    22  				Computed: true,
    23  			},
    24  			"unique_id": &schema.Schema{
    25  				Type:     schema.TypeString,
    26  				Computed: true,
    27  			},
    28  			"name": &schema.Schema{
    29  				Type:     schema.TypeString,
    30  				Computed: true,
    31  			},
    32  			"account_id": &schema.Schema{
    33  				Type:     schema.TypeString,
    34  				Required: true,
    35  				ForceNew: true,
    36  			},
    37  			"display_name": &schema.Schema{
    38  				Type:     schema.TypeString,
    39  				Optional: true,
    40  			},
    41  			"project": &schema.Schema{
    42  				Type:     schema.TypeString,
    43  				Optional: true,
    44  				ForceNew: true,
    45  			},
    46  			"policy_data": &schema.Schema{
    47  				Type:     schema.TypeString,
    48  				Optional: true,
    49  			},
    50  		},
    51  	}
    52  }
    53  
    54  func resourceGoogleServiceAccountCreate(d *schema.ResourceData, meta interface{}) error {
    55  	config := meta.(*Config)
    56  	project, err := getProject(d, config)
    57  	if err != nil {
    58  		return err
    59  	}
    60  	aid := d.Get("account_id").(string)
    61  	displayName := d.Get("display_name").(string)
    62  
    63  	sa := &iam.ServiceAccount{
    64  		DisplayName: displayName,
    65  	}
    66  
    67  	r := &iam.CreateServiceAccountRequest{
    68  		AccountId:      aid,
    69  		ServiceAccount: sa,
    70  	}
    71  
    72  	sa, err = config.clientIAM.Projects.ServiceAccounts.Create("projects/"+project, r).Do()
    73  	if err != nil {
    74  		return fmt.Errorf("Error creating service account: %s", err)
    75  	}
    76  
    77  	d.SetId(sa.Name)
    78  
    79  	// Apply the IAM policy if it is set
    80  	if pString, ok := d.GetOk("policy_data"); ok {
    81  		// The policy string is just a marshaled cloudresourcemanager.Policy.
    82  		// Unmarshal it to a struct.
    83  		var policy iam.Policy
    84  		if err = json.Unmarshal([]byte(pString.(string)), &policy); err != nil {
    85  			return err
    86  		}
    87  
    88  		// Retrieve existing IAM policy from project. This will be merged
    89  		// with the policy defined here.
    90  		// TODO(evanbrown): Add an 'authoritative' flag that allows policy
    91  		// in manifest to overwrite existing policy.
    92  		p, err := getServiceAccountIamPolicy(sa.Name, config)
    93  		if err != nil {
    94  			return fmt.Errorf("Could not find service account %q when applying IAM policy: %s", sa.Name, err)
    95  		}
    96  		log.Printf("[DEBUG] Got existing bindings for service account: %#v", p.Bindings)
    97  
    98  		// Merge the existing policy bindings with those defined in this manifest.
    99  		p.Bindings = saMergeBindings(append(p.Bindings, policy.Bindings...))
   100  
   101  		// Apply the merged policy
   102  		log.Printf("[DEBUG] Setting new policy for service account: %#v", p)
   103  		_, err = config.clientIAM.Projects.ServiceAccounts.SetIamPolicy(sa.Name,
   104  			&iam.SetIamPolicyRequest{Policy: p}).Do()
   105  
   106  		if err != nil {
   107  			return fmt.Errorf("Error applying IAM policy for service account %q: %s", sa.Name, err)
   108  		}
   109  	}
   110  	return resourceGoogleServiceAccountRead(d, meta)
   111  }
   112  
   113  func resourceGoogleServiceAccountRead(d *schema.ResourceData, meta interface{}) error {
   114  	config := meta.(*Config)
   115  
   116  	// Confirm the service account exists
   117  	sa, err := config.clientIAM.Projects.ServiceAccounts.Get(d.Id()).Do()
   118  	if err != nil {
   119  		if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 {
   120  			log.Printf("[WARN] Removing reference to service account %q because it no longer exists", d.Id())
   121  			// The resource doesn't exist anymore
   122  			d.SetId("")
   123  
   124  			return nil
   125  		}
   126  		return fmt.Errorf("Error reading service account %q: %q", d.Id(), err)
   127  	}
   128  
   129  	d.Set("email", sa.Email)
   130  	d.Set("unique_id", sa.UniqueId)
   131  	d.Set("name", sa.Name)
   132  	d.Set("display_name", sa.DisplayName)
   133  	return nil
   134  }
   135  
   136  func resourceGoogleServiceAccountDelete(d *schema.ResourceData, meta interface{}) error {
   137  	config := meta.(*Config)
   138  	name := d.Id()
   139  	_, err := config.clientIAM.Projects.ServiceAccounts.Delete(name).Do()
   140  	if err != nil {
   141  		return err
   142  	}
   143  	d.SetId("")
   144  	return nil
   145  }
   146  
   147  func resourceGoogleServiceAccountUpdate(d *schema.ResourceData, meta interface{}) error {
   148  	config := meta.(*Config)
   149  	var err error
   150  	if ok := d.HasChange("display_name"); ok {
   151  		sa, err := config.clientIAM.Projects.ServiceAccounts.Get(d.Id()).Do()
   152  		if err != nil {
   153  			return fmt.Errorf("Error retrieving service account %q: %s", d.Id(), err)
   154  		}
   155  		_, err = config.clientIAM.Projects.ServiceAccounts.Update(d.Id(),
   156  			&iam.ServiceAccount{
   157  				DisplayName: d.Get("display_name").(string),
   158  				Etag:        sa.Etag,
   159  			}).Do()
   160  		if err != nil {
   161  			return fmt.Errorf("Error updating service account %q: %s", d.Id(), err)
   162  		}
   163  	}
   164  
   165  	if ok := d.HasChange("policy_data"); ok {
   166  		// The policy string is just a marshaled cloudresourcemanager.Policy.
   167  		// Unmarshal it to a struct that contains the old and new policies
   168  		oldP, newP := d.GetChange("policy_data")
   169  		oldPString := oldP.(string)
   170  		newPString := newP.(string)
   171  
   172  		// JSON Unmarshaling would fail
   173  		if oldPString == "" {
   174  			oldPString = "{}"
   175  		}
   176  		if newPString == "" {
   177  			newPString = "{}"
   178  		}
   179  
   180  		log.Printf("[DEBUG]: Old policy: %q\nNew policy: %q", string(oldPString), string(newPString))
   181  
   182  		var oldPolicy, newPolicy iam.Policy
   183  		if err = json.Unmarshal([]byte(newPString), &newPolicy); err != nil {
   184  			return err
   185  		}
   186  		if err = json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil {
   187  			return err
   188  		}
   189  
   190  		// Find any Roles and Members that were removed (i.e., those that are present
   191  		// in the old but absent in the new
   192  		oldMap := saRolesToMembersMap(oldPolicy.Bindings)
   193  		newMap := saRolesToMembersMap(newPolicy.Bindings)
   194  		deleted := make(map[string]map[string]bool)
   195  
   196  		// Get each role and its associated members in the old state
   197  		for role, members := range oldMap {
   198  			// Initialize map for role
   199  			if _, ok := deleted[role]; !ok {
   200  				deleted[role] = make(map[string]bool)
   201  			}
   202  			// The role exists in the new state
   203  			if _, ok := newMap[role]; ok {
   204  				// Check each memeber
   205  				for member, _ := range members {
   206  					// Member does not exist in new state, so it was deleted
   207  					if _, ok = newMap[role][member]; !ok {
   208  						deleted[role][member] = true
   209  					}
   210  				}
   211  			} else {
   212  				// This indicates an entire role was deleted. Mark all members
   213  				// for delete.
   214  				for member, _ := range members {
   215  					deleted[role][member] = true
   216  				}
   217  			}
   218  		}
   219  		log.Printf("[DEBUG] Roles and Members to be deleted: %#v", deleted)
   220  
   221  		// Retrieve existing IAM policy from project. This will be merged
   222  		// with the policy in the current state
   223  		// TODO(evanbrown): Add an 'authoritative' flag that allows policy
   224  		// in manifest to overwrite existing policy.
   225  		p, err := getServiceAccountIamPolicy(d.Id(), config)
   226  		if err != nil {
   227  			return err
   228  		}
   229  		log.Printf("[DEBUG] Got existing bindings from service account %q: %#v", d.Id(), p.Bindings)
   230  
   231  		// Merge existing policy with policy in the current state
   232  		log.Printf("[DEBUG] Merging new bindings from service account %q: %#v", d.Id(), newPolicy.Bindings)
   233  		mergedBindings := saMergeBindings(append(p.Bindings, newPolicy.Bindings...))
   234  
   235  		// Remove any roles and members that were explicitly deleted
   236  		mergedBindingsMap := saRolesToMembersMap(mergedBindings)
   237  		for role, members := range deleted {
   238  			for member, _ := range members {
   239  				delete(mergedBindingsMap[role], member)
   240  			}
   241  		}
   242  
   243  		p.Bindings = saRolesToMembersBinding(mergedBindingsMap)
   244  		log.Printf("[DEBUG] Setting new policy for project: %#v", p)
   245  
   246  		dump, _ := json.MarshalIndent(p.Bindings, " ", "  ")
   247  		log.Printf(string(dump))
   248  		_, err = config.clientIAM.Projects.ServiceAccounts.SetIamPolicy(d.Id(),
   249  			&iam.SetIamPolicyRequest{Policy: p}).Do()
   250  
   251  		if err != nil {
   252  			return fmt.Errorf("Error applying IAM policy for service account %q: %s", d.Id(), err)
   253  		}
   254  	}
   255  	return nil
   256  }
   257  
   258  // Retrieve the existing IAM Policy for a service account
   259  func getServiceAccountIamPolicy(sa string, config *Config) (*iam.Policy, error) {
   260  	p, err := config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(sa).Do()
   261  
   262  	if err != nil {
   263  		return nil, fmt.Errorf("Error retrieving IAM policy for service account %q: %s", sa, err)
   264  	}
   265  	return p, nil
   266  }
   267  
   268  // Convert a map of roles->members to a list of Binding
   269  func saRolesToMembersBinding(m map[string]map[string]bool) []*iam.Binding {
   270  	bindings := make([]*iam.Binding, 0)
   271  	for role, members := range m {
   272  		b := iam.Binding{
   273  			Role:    role,
   274  			Members: make([]string, 0),
   275  		}
   276  		for m, _ := range members {
   277  			b.Members = append(b.Members, m)
   278  		}
   279  		bindings = append(bindings, &b)
   280  	}
   281  	return bindings
   282  }
   283  
   284  // Map a role to a map of members, allowing easy merging of multiple bindings.
   285  func saRolesToMembersMap(bindings []*iam.Binding) map[string]map[string]bool {
   286  	bm := make(map[string]map[string]bool)
   287  	// Get each binding
   288  	for _, b := range bindings {
   289  		// Initialize members map
   290  		if _, ok := bm[b.Role]; !ok {
   291  			bm[b.Role] = make(map[string]bool)
   292  		}
   293  		// Get each member (user/principal) for the binding
   294  		for _, m := range b.Members {
   295  			// Add the member
   296  			bm[b.Role][m] = true
   297  		}
   298  	}
   299  	return bm
   300  }
   301  
   302  // Merge multiple Bindings such that Bindings with the same Role result in
   303  // a single Binding with combined Members
   304  func saMergeBindings(bindings []*iam.Binding) []*iam.Binding {
   305  	bm := saRolesToMembersMap(bindings)
   306  	rb := make([]*iam.Binding, 0)
   307  
   308  	for role, members := range bm {
   309  		var b iam.Binding
   310  		b.Role = role
   311  		b.Members = make([]string, 0)
   312  		for m, _ := range members {
   313  			b.Members = append(b.Members, m)
   314  		}
   315  		rb = append(rb, &b)
   316  	}
   317  
   318  	return rb
   319  }