github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/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  			saName := d.Id()
   122  			// The resource doesn't exist anymore
   123  			d.SetId("")
   124  
   125  			return fmt.Errorf("Error getting service account with name %q: %s", saName, err)
   126  		}
   127  		return fmt.Errorf("Error reading service account %q: %q", d.Id(), err)
   128  	}
   129  
   130  	d.Set("email", sa.Email)
   131  	d.Set("unique_id", sa.UniqueId)
   132  	d.Set("name", sa.Name)
   133  	d.Set("display_name", sa.DisplayName)
   134  	return nil
   135  }
   136  
   137  func resourceGoogleServiceAccountDelete(d *schema.ResourceData, meta interface{}) error {
   138  	config := meta.(*Config)
   139  	name := d.Id()
   140  	_, err := config.clientIAM.Projects.ServiceAccounts.Delete(name).Do()
   141  	if err != nil {
   142  		return err
   143  	}
   144  	d.SetId("")
   145  	return nil
   146  }
   147  
   148  func resourceGoogleServiceAccountUpdate(d *schema.ResourceData, meta interface{}) error {
   149  	config := meta.(*Config)
   150  	var err error
   151  	if ok := d.HasChange("display_name"); ok {
   152  		sa, err := config.clientIAM.Projects.ServiceAccounts.Get(d.Id()).Do()
   153  		if err != nil {
   154  			return fmt.Errorf("Error retrieving service account %q: %s", d.Id(), err)
   155  		}
   156  		_, err = config.clientIAM.Projects.ServiceAccounts.Update(d.Id(),
   157  			&iam.ServiceAccount{
   158  				DisplayName: d.Get("display_name").(string),
   159  				Etag:        sa.Etag,
   160  			}).Do()
   161  		if err != nil {
   162  			return fmt.Errorf("Error updating service account %q: %s", d.Id(), err)
   163  		}
   164  	}
   165  
   166  	if ok := d.HasChange("policy_data"); ok {
   167  		// The policy string is just a marshaled cloudresourcemanager.Policy.
   168  		// Unmarshal it to a struct that contains the old and new policies
   169  		oldP, newP := d.GetChange("policy_data")
   170  		oldPString := oldP.(string)
   171  		newPString := newP.(string)
   172  
   173  		// JSON Unmarshaling would fail
   174  		if oldPString == "" {
   175  			oldPString = "{}"
   176  		}
   177  		if newPString == "" {
   178  			newPString = "{}"
   179  		}
   180  
   181  		log.Printf("[DEBUG]: Old policy: %q\nNew policy: %q", string(oldPString), string(newPString))
   182  
   183  		var oldPolicy, newPolicy iam.Policy
   184  		if err = json.Unmarshal([]byte(newPString), &newPolicy); err != nil {
   185  			return err
   186  		}
   187  		if err = json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil {
   188  			return err
   189  		}
   190  
   191  		// Find any Roles and Members that were removed (i.e., those that are present
   192  		// in the old but absent in the new
   193  		oldMap := saRolesToMembersMap(oldPolicy.Bindings)
   194  		newMap := saRolesToMembersMap(newPolicy.Bindings)
   195  		deleted := make(map[string]map[string]bool)
   196  
   197  		// Get each role and its associated members in the old state
   198  		for role, members := range oldMap {
   199  			// Initialize map for role
   200  			if _, ok := deleted[role]; !ok {
   201  				deleted[role] = make(map[string]bool)
   202  			}
   203  			// The role exists in the new state
   204  			if _, ok := newMap[role]; ok {
   205  				// Check each memeber
   206  				for member, _ := range members {
   207  					// Member does not exist in new state, so it was deleted
   208  					if _, ok = newMap[role][member]; !ok {
   209  						deleted[role][member] = true
   210  					}
   211  				}
   212  			} else {
   213  				// This indicates an entire role was deleted. Mark all members
   214  				// for delete.
   215  				for member, _ := range members {
   216  					deleted[role][member] = true
   217  				}
   218  			}
   219  		}
   220  		log.Printf("[DEBUG] Roles and Members to be deleted: %#v", deleted)
   221  
   222  		// Retrieve existing IAM policy from project. This will be merged
   223  		// with the policy in the current state
   224  		// TODO(evanbrown): Add an 'authoritative' flag that allows policy
   225  		// in manifest to overwrite existing policy.
   226  		p, err := getServiceAccountIamPolicy(d.Id(), config)
   227  		if err != nil {
   228  			return err
   229  		}
   230  		log.Printf("[DEBUG] Got existing bindings from service account %q: %#v", d.Id(), p.Bindings)
   231  
   232  		// Merge existing policy with policy in the current state
   233  		log.Printf("[DEBUG] Merging new bindings from service account %q: %#v", d.Id(), newPolicy.Bindings)
   234  		mergedBindings := saMergeBindings(append(p.Bindings, newPolicy.Bindings...))
   235  
   236  		// Remove any roles and members that were explicitly deleted
   237  		mergedBindingsMap := saRolesToMembersMap(mergedBindings)
   238  		for role, members := range deleted {
   239  			for member, _ := range members {
   240  				delete(mergedBindingsMap[role], member)
   241  			}
   242  		}
   243  
   244  		p.Bindings = saRolesToMembersBinding(mergedBindingsMap)
   245  		log.Printf("[DEBUG] Setting new policy for project: %#v", p)
   246  
   247  		dump, _ := json.MarshalIndent(p.Bindings, " ", "  ")
   248  		log.Printf(string(dump))
   249  		_, err = config.clientIAM.Projects.ServiceAccounts.SetIamPolicy(d.Id(),
   250  			&iam.SetIamPolicyRequest{Policy: p}).Do()
   251  
   252  		if err != nil {
   253  			return fmt.Errorf("Error applying IAM policy for service account %q: %s", d.Id(), err)
   254  		}
   255  	}
   256  	return nil
   257  }
   258  
   259  // Retrieve the existing IAM Policy for a service account
   260  func getServiceAccountIamPolicy(sa string, config *Config) (*iam.Policy, error) {
   261  	p, err := config.clientIAM.Projects.ServiceAccounts.GetIamPolicy(sa).Do()
   262  
   263  	if err != nil {
   264  		return nil, fmt.Errorf("Error retrieving IAM policy for service account %q: %s", sa, err)
   265  	}
   266  	return p, nil
   267  }
   268  
   269  // Convert a map of roles->members to a list of Binding
   270  func saRolesToMembersBinding(m map[string]map[string]bool) []*iam.Binding {
   271  	bindings := make([]*iam.Binding, 0)
   272  	for role, members := range m {
   273  		b := iam.Binding{
   274  			Role:    role,
   275  			Members: make([]string, 0),
   276  		}
   277  		for m, _ := range members {
   278  			b.Members = append(b.Members, m)
   279  		}
   280  		bindings = append(bindings, &b)
   281  	}
   282  	return bindings
   283  }
   284  
   285  // Map a role to a map of members, allowing easy merging of multiple bindings.
   286  func saRolesToMembersMap(bindings []*iam.Binding) map[string]map[string]bool {
   287  	bm := make(map[string]map[string]bool)
   288  	// Get each binding
   289  	for _, b := range bindings {
   290  		// Initialize members map
   291  		if _, ok := bm[b.Role]; !ok {
   292  			bm[b.Role] = make(map[string]bool)
   293  		}
   294  		// Get each member (user/principal) for the binding
   295  		for _, m := range b.Members {
   296  			// Add the member
   297  			bm[b.Role][m] = true
   298  		}
   299  	}
   300  	return bm
   301  }
   302  
   303  // Merge multiple Bindings such that Bindings with the same Role result in
   304  // a single Binding with combined Members
   305  func saMergeBindings(bindings []*iam.Binding) []*iam.Binding {
   306  	bm := saRolesToMembersMap(bindings)
   307  	rb := make([]*iam.Binding, 0)
   308  
   309  	for role, members := range bm {
   310  		var b iam.Binding
   311  		b.Role = role
   312  		b.Members = make([]string, 0)
   313  		for m, _ := range members {
   314  			b.Members = append(b.Members, m)
   315  		}
   316  		rb = append(rb, &b)
   317  	}
   318  
   319  	return rb
   320  }