mirror of https://github.com/hashicorp/terraform
Add support for creating, updating, and deleting projects, as well as their enabled services and their IAM policies. Various concessions were made for backwards compatibility, and will be removed in 0.9 or 0.10.pull/10425/head
parent
e81d096699
commit
b9e9e777c8
@ -0,0 +1,417 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"google.golang.org/api/cloudresourcemanager/v1"
|
||||
)
|
||||
|
||||
func resourceGoogleProjectIamPolicy() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Create: resourceGoogleProjectIamPolicyCreate,
|
||||
Read: resourceGoogleProjectIamPolicyRead,
|
||||
Update: resourceGoogleProjectIamPolicyUpdate,
|
||||
Delete: resourceGoogleProjectIamPolicyDelete,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"project": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
"policy_data": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
DiffSuppressFunc: jsonPolicyDiffSuppress,
|
||||
},
|
||||
"authoritative": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
},
|
||||
"etag": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
"restore_policy": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
"disable_project": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resourceGoogleProjectIamPolicyCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
config := meta.(*Config)
|
||||
pid := d.Get("project").(string)
|
||||
// Get the policy in the template
|
||||
p, err := getResourceIamPolicy(d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not get valid 'policy_data' from resource: %v", err)
|
||||
}
|
||||
|
||||
// An authoritative policy is applied without regard for any existing IAM
|
||||
// policy.
|
||||
if v, ok := d.GetOk("authoritative"); ok && v.(bool) {
|
||||
log.Printf("[DEBUG] Setting authoritative IAM policy for project %q", pid)
|
||||
err := setProjectIamPolicy(p, config, pid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Printf("[DEBUG] Setting non-authoritative IAM policy for project %q", pid)
|
||||
// This is a non-authoritative policy, meaning it should be merged with
|
||||
// any existing policy
|
||||
ep, err := getProjectIamPolicy(pid, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// First, subtract the policy defined in the template from the
|
||||
// current policy in the project, and save the result. This will
|
||||
// allow us to restore the original policy at some point (which
|
||||
// assumes that Terraform owns any common policy that exists in
|
||||
// the template and project at create time.
|
||||
rp := subtractIamPolicy(ep, p)
|
||||
rps, err := json.Marshal(rp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error marshaling restorable IAM policy: %v", err)
|
||||
}
|
||||
d.Set("restore_policy", string(rps))
|
||||
|
||||
// Merge the policies together
|
||||
mb := mergeBindings(append(p.Bindings, rp.Bindings...))
|
||||
ep.Bindings = mb
|
||||
if err = setProjectIamPolicy(ep, config, pid); err != nil {
|
||||
return fmt.Errorf("Error applying IAM policy to project: %v", err)
|
||||
}
|
||||
}
|
||||
d.SetId(pid)
|
||||
return resourceGoogleProjectIamPolicyRead(d, meta)
|
||||
}
|
||||
|
||||
func resourceGoogleProjectIamPolicyRead(d *schema.ResourceData, meta interface{}) error {
|
||||
log.Printf("[DEBUG]: Reading google_project_iam_policy")
|
||||
config := meta.(*Config)
|
||||
pid := d.Get("project").(string)
|
||||
|
||||
p, err := getProjectIamPolicy(pid, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var bindings []*cloudresourcemanager.Binding
|
||||
if v, ok := d.GetOk("restore_policy"); ok {
|
||||
var restored cloudresourcemanager.Policy
|
||||
// if there's a restore policy, subtract it from the policy_data
|
||||
err := json.Unmarshal([]byte(v.(string)), &restored)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error unmarshaling restorable IAM policy: %v", err)
|
||||
}
|
||||
subtracted := subtractIamPolicy(p, &restored)
|
||||
bindings = subtracted.Bindings
|
||||
} else {
|
||||
bindings = p.Bindings
|
||||
}
|
||||
// we only marshal the bindings, because only the bindings get set in the config
|
||||
pBytes, err := json.Marshal(&cloudresourcemanager.Policy{Bindings: bindings})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error marshaling IAM policy: %v", err)
|
||||
}
|
||||
log.Printf("[DEBUG]: Setting etag=%s", p.Etag)
|
||||
d.Set("etag", p.Etag)
|
||||
d.Set("policy_data", string(pBytes))
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceGoogleProjectIamPolicyUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
log.Printf("[DEBUG]: Updating google_project_iam_policy")
|
||||
config := meta.(*Config)
|
||||
pid := d.Get("project").(string)
|
||||
|
||||
// Get the policy in the template
|
||||
p, err := getResourceIamPolicy(d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not get valid 'policy_data' from resource: %v", err)
|
||||
}
|
||||
pBytes, _ := json.Marshal(p)
|
||||
log.Printf("[DEBUG] Got policy from config: %s", string(pBytes))
|
||||
|
||||
// An authoritative policy is applied without regard for any existing IAM
|
||||
// policy.
|
||||
if v, ok := d.GetOk("authoritative"); ok && v.(bool) {
|
||||
log.Printf("[DEBUG] Updating authoritative IAM policy for project %q", pid)
|
||||
err := setProjectIamPolicy(p, config, pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error setting project IAM policy: %v", err)
|
||||
}
|
||||
d.Set("restore_policy", "")
|
||||
} else {
|
||||
log.Printf("[DEBUG] Updating non-authoritative IAM policy for project %q", pid)
|
||||
// Get the previous policy from state
|
||||
pp, err := getPrevResourceIamPolicy(d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error retrieving previous version of changed project IAM policy: %v", err)
|
||||
}
|
||||
ppBytes, _ := json.Marshal(pp)
|
||||
log.Printf("[DEBUG] Got previous version of changed project IAM policy: %s", string(ppBytes))
|
||||
|
||||
// Get the existing IAM policy from the API
|
||||
ep, err := getProjectIamPolicy(pid, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error retrieving IAM policy from project API: %v", err)
|
||||
}
|
||||
epBytes, _ := json.Marshal(ep)
|
||||
log.Printf("[DEBUG] Got existing version of changed IAM policy from project API: %s", string(epBytes))
|
||||
|
||||
// Subtract the previous and current policies from the policy retrieved from the API
|
||||
rp := subtractIamPolicy(ep, pp)
|
||||
rpBytes, _ := json.Marshal(rp)
|
||||
log.Printf("[DEBUG] After subtracting the previous policy from the existing policy, remaining policies: %s", string(rpBytes))
|
||||
rp = subtractIamPolicy(rp, p)
|
||||
rpBytes, _ = json.Marshal(rp)
|
||||
log.Printf("[DEBUG] After subtracting the remaining policies from the config policy, remaining policies: %s", string(rpBytes))
|
||||
rps, err := json.Marshal(rp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error marhsaling restorable IAM policy: %v", err)
|
||||
}
|
||||
d.Set("restore_policy", string(rps))
|
||||
|
||||
// Merge the policies together
|
||||
mb := mergeBindings(append(p.Bindings, rp.Bindings...))
|
||||
ep.Bindings = mb
|
||||
if err = setProjectIamPolicy(ep, config, pid); err != nil {
|
||||
return fmt.Errorf("Error applying IAM policy to project: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return resourceGoogleProjectIamPolicyRead(d, meta)
|
||||
}
|
||||
|
||||
func resourceGoogleProjectIamPolicyDelete(d *schema.ResourceData, meta interface{}) error {
|
||||
log.Printf("[DEBUG]: Deleting google_project_iam_policy")
|
||||
config := meta.(*Config)
|
||||
pid := d.Get("project").(string)
|
||||
|
||||
// Get the existing IAM policy from the API
|
||||
ep, err := getProjectIamPolicy(pid, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error retrieving IAM policy from project API: %v", err)
|
||||
}
|
||||
// Deleting an authoritative policy will leave the project with no policy,
|
||||
// and unaccessible by anyone without org-level privs. For this reason, the
|
||||
// "disable_project" property must be set to true, forcing the user to ack
|
||||
// this outcome
|
||||
if v, ok := d.GetOk("authoritative"); ok && v.(bool) {
|
||||
if v, ok := d.GetOk("disable_project"); !ok || !v.(bool) {
|
||||
return fmt.Errorf("You must set 'disable_project' to true before deleting an authoritative IAM policy")
|
||||
}
|
||||
ep.Bindings = make([]*cloudresourcemanager.Binding, 0)
|
||||
|
||||
} else {
|
||||
// A non-authoritative policy should set the policy to the value of "restore_policy" in state
|
||||
// Get the previous policy from state
|
||||
rp, err := getRestoreIamPolicy(d)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error retrieving previous version of changed project IAM policy: %v", err)
|
||||
}
|
||||
ep.Bindings = rp.Bindings
|
||||
}
|
||||
if err = setProjectIamPolicy(ep, config, pid); err != nil {
|
||||
return fmt.Errorf("Error applying IAM policy to project: %v", err)
|
||||
}
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subtract all bindings in policy b from policy a, and return the result
|
||||
func subtractIamPolicy(a, b *cloudresourcemanager.Policy) *cloudresourcemanager.Policy {
|
||||
am := rolesToMembersMap(a.Bindings)
|
||||
|
||||
for _, b := range b.Bindings {
|
||||
if _, ok := am[b.Role]; ok {
|
||||
for _, m := range b.Members {
|
||||
delete(am[b.Role], m)
|
||||
}
|
||||
if len(am[b.Role]) == 0 {
|
||||
delete(am, b.Role)
|
||||
}
|
||||
}
|
||||
}
|
||||
a.Bindings = rolesToMembersBinding(am)
|
||||
return a
|
||||
}
|
||||
|
||||
func setProjectIamPolicy(policy *cloudresourcemanager.Policy, config *Config, pid string) error {
|
||||
// Apply the policy
|
||||
pbytes, _ := json.Marshal(policy)
|
||||
log.Printf("[DEBUG] Setting policy %#v for project: %s", string(pbytes), pid)
|
||||
_, err := config.clientResourceManager.Projects.SetIamPolicy(pid,
|
||||
&cloudresourcemanager.SetIamPolicyRequest{Policy: policy}).Do()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error applying IAM policy for project %q. Policy is %+s, error is %s", pid, policy, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get a cloudresourcemanager.Policy from a schema.ResourceData
|
||||
func getResourceIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) {
|
||||
ps := d.Get("policy_data").(string)
|
||||
// The policy string is just a marshaled cloudresourcemanager.Policy.
|
||||
policy := &cloudresourcemanager.Policy{}
|
||||
if err := json.Unmarshal([]byte(ps), policy); err != nil {
|
||||
return nil, fmt.Errorf("Could not unmarshal %s:\n: %v", ps, err)
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// Get the previous cloudresourcemanager.Policy from a schema.ResourceData if the
|
||||
// resource has changed
|
||||
func getPrevResourceIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) {
|
||||
var policy *cloudresourcemanager.Policy = &cloudresourcemanager.Policy{}
|
||||
if d.HasChange("policy_data") {
|
||||
v, _ := d.GetChange("policy_data")
|
||||
if err := json.Unmarshal([]byte(v.(string)), policy); err != nil {
|
||||
return nil, fmt.Errorf("Could not unmarshal previous policy %s:\n: %v", v, err)
|
||||
}
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// Get the restore_policy that can be used to restore a project's IAM policy to its
|
||||
// state before it was adopted into Terraform
|
||||
func getRestoreIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) {
|
||||
if v, ok := d.GetOk("restore_policy"); ok {
|
||||
policy := &cloudresourcemanager.Policy{}
|
||||
if err := json.Unmarshal([]byte(v.(string)), policy); err != nil {
|
||||
return nil, fmt.Errorf("Could not unmarshal previous policy %s:\n: %v", v, err)
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
return nil, fmt.Errorf("Resource does not have a 'restore_policy' attribute defined.")
|
||||
}
|
||||
|
||||
// Retrieve the existing IAM Policy for a Project
|
||||
func getProjectIamPolicy(project string, config *Config) (*cloudresourcemanager.Policy, error) {
|
||||
p, err := config.clientResourceManager.Projects.GetIamPolicy(project,
|
||||
&cloudresourcemanager.GetIamPolicyRequest{}).Do()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error retrieving IAM policy for project %q: %s", project, err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Convert a map of roles->members to a list of Binding
|
||||
func rolesToMembersBinding(m map[string]map[string]bool) []*cloudresourcemanager.Binding {
|
||||
bindings := make([]*cloudresourcemanager.Binding, 0)
|
||||
for role, members := range m {
|
||||
b := cloudresourcemanager.Binding{
|
||||
Role: role,
|
||||
Members: make([]string, 0),
|
||||
}
|
||||
for m, _ := range members {
|
||||
b.Members = append(b.Members, m)
|
||||
}
|
||||
bindings = append(bindings, &b)
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
|
||||
// Map a role to a map of members, allowing easy merging of multiple bindings.
|
||||
func rolesToMembersMap(bindings []*cloudresourcemanager.Binding) map[string]map[string]bool {
|
||||
bm := make(map[string]map[string]bool)
|
||||
// Get each binding
|
||||
for _, b := range bindings {
|
||||
// Initialize members map
|
||||
if _, ok := bm[b.Role]; !ok {
|
||||
bm[b.Role] = make(map[string]bool)
|
||||
}
|
||||
// Get each member (user/principal) for the binding
|
||||
for _, m := range b.Members {
|
||||
// Add the member
|
||||
bm[b.Role][m] = true
|
||||
}
|
||||
}
|
||||
return bm
|
||||
}
|
||||
|
||||
// Merge multiple Bindings such that Bindings with the same Role result in
|
||||
// a single Binding with combined Members
|
||||
func mergeBindings(bindings []*cloudresourcemanager.Binding) []*cloudresourcemanager.Binding {
|
||||
bm := rolesToMembersMap(bindings)
|
||||
rb := make([]*cloudresourcemanager.Binding, 0)
|
||||
|
||||
for role, members := range bm {
|
||||
var b cloudresourcemanager.Binding
|
||||
b.Role = role
|
||||
b.Members = make([]string, 0)
|
||||
for m, _ := range members {
|
||||
b.Members = append(b.Members, m)
|
||||
}
|
||||
rb = append(rb, &b)
|
||||
}
|
||||
|
||||
return rb
|
||||
}
|
||||
|
||||
func jsonPolicyDiffSuppress(k, old, new string, d *schema.ResourceData) bool {
|
||||
var oldPolicy, newPolicy cloudresourcemanager.Policy
|
||||
if err := json.Unmarshal([]byte(old), &oldPolicy); err != nil {
|
||||
log.Printf("[ERROR] Could not unmarshal old policy %s: %v", old, err)
|
||||
return false
|
||||
}
|
||||
if err := json.Unmarshal([]byte(new), &newPolicy); err != nil {
|
||||
log.Printf("[ERROR] Could not unmarshal new policy %s: %v", new, err)
|
||||
return false
|
||||
}
|
||||
if newPolicy.Etag != oldPolicy.Etag {
|
||||
return false
|
||||
}
|
||||
if newPolicy.Version != oldPolicy.Version {
|
||||
return false
|
||||
}
|
||||
if len(newPolicy.Bindings) != len(oldPolicy.Bindings) {
|
||||
return false
|
||||
}
|
||||
sort.Sort(sortableBindings(newPolicy.Bindings))
|
||||
sort.Sort(sortableBindings(oldPolicy.Bindings))
|
||||
for pos, newBinding := range newPolicy.Bindings {
|
||||
oldBinding := oldPolicy.Bindings[pos]
|
||||
if oldBinding.Role != newBinding.Role {
|
||||
return false
|
||||
}
|
||||
if len(oldBinding.Members) != len(newBinding.Members) {
|
||||
return false
|
||||
}
|
||||
sort.Strings(oldBinding.Members)
|
||||
sort.Strings(newBinding.Members)
|
||||
for i, newMember := range newBinding.Members {
|
||||
oldMember := oldBinding.Members[i]
|
||||
if newMember != oldMember {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type sortableBindings []*cloudresourcemanager.Binding
|
||||
|
||||
func (b sortableBindings) Len() int {
|
||||
return len(b)
|
||||
}
|
||||
func (b sortableBindings) Swap(i, j int) {
|
||||
b[i], b[j] = b[j], b[i]
|
||||
}
|
||||
func (b sortableBindings) Less(i, j int) bool {
|
||||
return b[i].Role < b[j].Role
|
||||
}
|
||||
@ -0,0 +1,626 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/acctest"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"google.golang.org/api/cloudresourcemanager/v1"
|
||||
)
|
||||
|
||||
func TestSubtractIamPolicy(t *testing.T) {
|
||||
table := []struct {
|
||||
a *cloudresourcemanager.Policy
|
||||
b *cloudresourcemanager.Policy
|
||||
expect cloudresourcemanager.Policy
|
||||
}{
|
||||
{
|
||||
a: &cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "a",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "b",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "a",
|
||||
Members: []string{
|
||||
"3",
|
||||
"4",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "b",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "a",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
a: &cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "a",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "b",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "a",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "b",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{},
|
||||
},
|
||||
},
|
||||
{
|
||||
a: &cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "a",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "b",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "a",
|
||||
Members: []string{
|
||||
"1",
|
||||
"3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "b",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "a",
|
||||
Members: []string{
|
||||
"2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
a: &cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "a",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "b",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "a",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "b",
|
||||
Members: []string{
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: cloudresourcemanager.Policy{
|
||||
Bindings: []*cloudresourcemanager.Binding{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
c := subtractIamPolicy(test.a, test.b)
|
||||
sort.Sort(sortableBindings(c.Bindings))
|
||||
for i, _ := range c.Bindings {
|
||||
sort.Strings(c.Bindings[i].Members)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(derefBindings(c.Bindings), derefBindings(test.expect.Bindings)) {
|
||||
t.Errorf("\ngot %+v\nexpected %+v", derefBindings(c.Bindings), derefBindings(test.expect.Bindings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that an IAM policy can be applied to a project
|
||||
func TestAccGoogleProjectIamPolicy_basic(t *testing.T) {
|
||||
pid := "terraform-" + acctest.RandString(10)
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
Steps: []resource.TestStep{
|
||||
// Create a new project
|
||||
resource.TestStep{
|
||||
Config: testAccGoogleProject_create(pid, pname, org),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccGoogleProjectExistingPolicy(pid),
|
||||
),
|
||||
},
|
||||
// Apply an IAM policy from a data source. The application
|
||||
// merges policies, so we validate the expected state.
|
||||
resource.TestStep{
|
||||
Config: testAccGoogleProjectAssociatePolicyBasic(pid, pname, org),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckGoogleProjectIamPolicyIsMerged("google_project_iam_policy.acceptance", "data.google_iam_policy.admin", pid),
|
||||
),
|
||||
},
|
||||
// Finally, remove the custom IAM policy from config and apply, then
|
||||
// confirm that the project is in its original state.
|
||||
resource.TestStep{
|
||||
Config: testAccGoogleProject_create(pid, pname, org),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccGoogleProjectExistingPolicy(pid),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccCheckGoogleProjectIamPolicyIsMerged(projectRes, policyRes, pid string) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
// Get the project resource
|
||||
project, ok := s.RootModule().Resources[projectRes]
|
||||
if !ok {
|
||||
return fmt.Errorf("Not found: %s", projectRes)
|
||||
}
|
||||
// The project ID should match the config's project ID
|
||||
if project.Primary.ID != pid {
|
||||
return fmt.Errorf("Expected project %q to match ID %q in state", pid, project.Primary.ID)
|
||||
}
|
||||
|
||||
var projectP, policyP cloudresourcemanager.Policy
|
||||
// The project should have a policy
|
||||
ps, ok := project.Primary.Attributes["policy_data"]
|
||||
if !ok {
|
||||
return fmt.Errorf("Project resource %q did not have a 'policy_data' attribute. Attributes were %#v", project.Primary.Attributes["id"], project.Primary.Attributes)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(ps), &projectP); err != nil {
|
||||
return fmt.Errorf("Could not unmarshal %s:\n: %v", ps, err)
|
||||
}
|
||||
|
||||
// The data policy resource should have a policy
|
||||
policy, ok := s.RootModule().Resources[policyRes]
|
||||
if !ok {
|
||||
return fmt.Errorf("Not found: %s", policyRes)
|
||||
}
|
||||
ps, ok = policy.Primary.Attributes["policy_data"]
|
||||
if !ok {
|
||||
return fmt.Errorf("Data policy resource %q did not have a 'policy_data' attribute. Attributes were %#v", policy.Primary.Attributes["id"], project.Primary.Attributes)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(ps), &policyP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The bindings in both policies should be identical
|
||||
sort.Sort(sortableBindings(projectP.Bindings))
|
||||
sort.Sort(sortableBindings(policyP.Bindings))
|
||||
if !reflect.DeepEqual(derefBindings(projectP.Bindings), derefBindings(policyP.Bindings)) {
|
||||
return fmt.Errorf("Project and data source policies do not match: project policy is %+v, data resource policy is %+v", derefBindings(projectP.Bindings), derefBindings(policyP.Bindings))
|
||||
}
|
||||
|
||||
// Merge the project policy in Terraform state with the policy the project had before the config was applied
|
||||
expected := make([]*cloudresourcemanager.Binding, 0)
|
||||
expected = append(expected, originalPolicy.Bindings...)
|
||||
expected = append(expected, projectP.Bindings...)
|
||||
expectedM := mergeBindings(expected)
|
||||
|
||||
// Retrieve the actual policy from the project
|
||||
c := testAccProvider.Meta().(*Config)
|
||||
actual, err := getProjectIamPolicy(pid, c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", pid, err)
|
||||
}
|
||||
actualM := mergeBindings(actual.Bindings)
|
||||
|
||||
sort.Sort(sortableBindings(actualM))
|
||||
sort.Sort(sortableBindings(expectedM))
|
||||
// The bindings should match, indicating the policy was successfully applied and merged
|
||||
if !reflect.DeepEqual(derefBindings(actualM), derefBindings(expectedM)) {
|
||||
return fmt.Errorf("Actual and expected project policies do not match: actual policy is %+v, expected policy is %+v", derefBindings(actualM), derefBindings(expectedM))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestIamRolesToMembersBinding(t *testing.T) {
|
||||
table := []struct {
|
||||
expect []*cloudresourcemanager.Binding
|
||||
input map[string]map[string]bool
|
||||
}{
|
||||
{
|
||||
expect: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
"member-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
input: map[string]map[string]bool{
|
||||
"role-1": map[string]bool{
|
||||
"member-1": true,
|
||||
"member-2": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expect: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
"member-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
input: map[string]map[string]bool{
|
||||
"role-1": map[string]bool{
|
||||
"member-1": true,
|
||||
"member-2": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expect: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{},
|
||||
},
|
||||
},
|
||||
input: map[string]map[string]bool{
|
||||
"role-1": map[string]bool{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
got := rolesToMembersBinding(test.input)
|
||||
|
||||
sort.Sort(sortableBindings(got))
|
||||
for i, _ := range got {
|
||||
sort.Strings(got[i].Members)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(derefBindings(got), derefBindings(test.expect)) {
|
||||
t.Errorf("got %+v, expected %+v", derefBindings(got), derefBindings(test.expect))
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestIamRolesToMembersMap(t *testing.T) {
|
||||
table := []struct {
|
||||
input []*cloudresourcemanager.Binding
|
||||
expect map[string]map[string]bool
|
||||
}{
|
||||
{
|
||||
input: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
"member-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: map[string]map[string]bool{
|
||||
"role-1": map[string]bool{
|
||||
"member-1": true,
|
||||
"member-2": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
"member-2",
|
||||
"member-1",
|
||||
"member-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: map[string]map[string]bool{
|
||||
"role-1": map[string]bool{
|
||||
"member-1": true,
|
||||
"member-2": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "role-1",
|
||||
},
|
||||
},
|
||||
expect: map[string]map[string]bool{
|
||||
"role-1": map[string]bool{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
got := rolesToMembersMap(test.input)
|
||||
if !reflect.DeepEqual(got, test.expect) {
|
||||
t.Errorf("got %+v, expected %+v", got, test.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIamMergeBindings(t *testing.T) {
|
||||
table := []struct {
|
||||
input []*cloudresourcemanager.Binding
|
||||
expect []cloudresourcemanager.Binding
|
||||
}{
|
||||
{
|
||||
input: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
"member-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: []cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
"member-2",
|
||||
"member-3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []*cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-3",
|
||||
"member-4",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-2",
|
||||
"member-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "role-2",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-5",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "role-3",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "role-2",
|
||||
Members: []string{
|
||||
"member-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: []cloudresourcemanager.Binding{
|
||||
{
|
||||
Role: "role-1",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
"member-2",
|
||||
"member-3",
|
||||
"member-4",
|
||||
"member-5",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "role-2",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
"member-2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "role-3",
|
||||
Members: []string{
|
||||
"member-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range table {
|
||||
got := mergeBindings(test.input)
|
||||
sort.Sort(sortableBindings(got))
|
||||
for i, _ := range got {
|
||||
sort.Strings(got[i].Members)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(derefBindings(got), test.expect) {
|
||||
t.Errorf("\ngot %+v\nexpected %+v", derefBindings(got), test.expect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func derefBindings(b []*cloudresourcemanager.Binding) []cloudresourcemanager.Binding {
|
||||
db := make([]cloudresourcemanager.Binding, len(b))
|
||||
|
||||
for i, v := range b {
|
||||
db[i] = *v
|
||||
sort.Strings(db[i].Members)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
// Confirm that a project has an IAM policy with at least 1 binding
|
||||
func testAccGoogleProjectExistingPolicy(pid string) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
c := testAccProvider.Meta().(*Config)
|
||||
var err error
|
||||
originalPolicy, err = getProjectIamPolicy(pid, c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", pid, err)
|
||||
}
|
||||
if len(originalPolicy.Bindings) == 0 {
|
||||
return fmt.Errorf("Refuse to run test against project with zero IAM Bindings. This is likely an error in the test code that is not properly identifying the IAM policy of a project.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testAccGoogleProjectAssociatePolicyBasic(pid, name, org string) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "google_project" "acceptance" {
|
||||
project_id = "%s"
|
||||
name = "%s"
|
||||
org_id = "%s"
|
||||
}
|
||||
resource "google_project_iam_policy" "acceptance" {
|
||||
project = "${google_project.acceptance.id}"
|
||||
policy_data = "${data.google_iam_policy.admin.policy_data}"
|
||||
}
|
||||
data "google_iam_policy" "admin" {
|
||||
binding {
|
||||
role = "roles/storage.objectViewer"
|
||||
members = [
|
||||
"user:evanbrown@google.com",
|
||||
]
|
||||
}
|
||||
binding {
|
||||
role = "roles/compute.instanceAdmin"
|
||||
members = [
|
||||
"user:evanbrown@google.com",
|
||||
"user:evandbrown@gmail.com",
|
||||
]
|
||||
}
|
||||
}
|
||||
`, pid, name, org)
|
||||
}
|
||||
|
||||
func testAccGoogleProject_create(pid, name, org string) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "google_project" "acceptance" {
|
||||
project_id = "%s"
|
||||
name = "%s"
|
||||
org_id = "%s"
|
||||
}`, pid, name, org)
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func resourceGoogleProjectMigrateState(v int, s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
|
||||
if s.Empty() {
|
||||
log.Println("[DEBUG] Empty InstanceState; nothing to migrate.")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
switch v {
|
||||
case 0:
|
||||
log.Println("[INFO] Found Google Project State v0; migrating to v1")
|
||||
s, err := migrateGoogleProjectStateV0toV1(s, meta.(*Config))
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
return s, nil
|
||||
default:
|
||||
return s, fmt.Errorf("Unexpected schema version: %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
// This migration adjusts google_project resources to include several additional attributes
|
||||
// required to support project creation/deletion that was added in V1.
|
||||
func migrateGoogleProjectStateV0toV1(s *terraform.InstanceState, config *Config) (*terraform.InstanceState, error) {
|
||||
log.Printf("[DEBUG] Attributes before migration: %#v", s.Attributes)
|
||||
|
||||
s.Attributes["skip_delete"] = "true"
|
||||
s.Attributes["project_id"] = s.ID
|
||||
|
||||
if s.Attributes["policy_data"] != "" {
|
||||
p, err := getProjectIamPolicy(s.ID, config)
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("Could not retrieve project's IAM policy while attempting to migrate state from V0 to V1: %v", err)
|
||||
}
|
||||
s.Attributes["policy_etag"] = p.Etag
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Attributes after migration: %#v", s.Attributes)
|
||||
return s, nil
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestGoogleProjectMigrateState(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
StateVersion int
|
||||
Attributes map[string]string
|
||||
Expected map[string]string
|
||||
Meta interface{}
|
||||
}{
|
||||
"deprecate policy_data and support creation/deletion": {
|
||||
StateVersion: 0,
|
||||
Attributes: map[string]string{},
|
||||
Expected: map[string]string{
|
||||
"project_id": "test-project",
|
||||
"skip_delete": "true",
|
||||
},
|
||||
Meta: &Config{},
|
||||
},
|
||||
}
|
||||
|
||||
for tn, tc := range cases {
|
||||
is := &terraform.InstanceState{
|
||||
ID: "test-project",
|
||||
Attributes: tc.Attributes,
|
||||
}
|
||||
is, err := resourceGoogleProjectMigrateState(
|
||||
tc.StateVersion, is, tc.Meta)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("bad: %s, err: %#v", tn, err)
|
||||
}
|
||||
|
||||
for k, v := range tc.Expected {
|
||||
if is.Attributes[k] != v {
|
||||
t.Fatalf(
|
||||
"bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v",
|
||||
tn, k, v, k, is.Attributes[k], is.Attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleProjectMigrateState_empty(t *testing.T) {
|
||||
var is *terraform.InstanceState
|
||||
var meta *Config
|
||||
|
||||
// should handle nil
|
||||
is, err := resourceGoogleProjectMigrateState(0, is, meta)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("err: %#v", err)
|
||||
}
|
||||
if is != nil {
|
||||
t.Fatalf("expected nil instancestate, got: %#v", is)
|
||||
}
|
||||
|
||||
// should handle non-nil but empty
|
||||
is = &terraform.InstanceState{}
|
||||
is, err = resourceGoogleProjectMigrateState(0, is, meta)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("err: %#v", err)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,214 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"google.golang.org/api/servicemanagement/v1"
|
||||
)
|
||||
|
||||
func resourceGoogleProjectServices() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Create: resourceGoogleProjectServicesCreate,
|
||||
Read: resourceGoogleProjectServicesRead,
|
||||
Update: resourceGoogleProjectServicesUpdate,
|
||||
Delete: resourceGoogleProjectServicesDelete,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"project": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
"services": {
|
||||
Type: schema.TypeSet,
|
||||
Required: true,
|
||||
Elem: &schema.Schema{Type: schema.TypeString},
|
||||
Set: schema.HashString,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resourceGoogleProjectServicesCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
config := meta.(*Config)
|
||||
pid := d.Get("project").(string)
|
||||
|
||||
// Get services from config
|
||||
cfgServices := getConfigServices(d)
|
||||
|
||||
// Get services from API
|
||||
apiServices, err := getApiServices(pid, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating services: %v", err)
|
||||
}
|
||||
|
||||
// This call disables any APIs that aren't defined in cfgServices,
|
||||
// and enables all of those that are
|
||||
err = reconcileServices(cfgServices, apiServices, config, pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating services: %v", err)
|
||||
}
|
||||
|
||||
d.SetId(pid)
|
||||
return resourceGoogleProjectServicesRead(d, meta)
|
||||
}
|
||||
|
||||
func resourceGoogleProjectServicesRead(d *schema.ResourceData, meta interface{}) error {
|
||||
config := meta.(*Config)
|
||||
|
||||
services, err := getApiServices(d.Id(), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Set("services", services)
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceGoogleProjectServicesUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
log.Printf("[DEBUG]: Updating google_project_services")
|
||||
config := meta.(*Config)
|
||||
pid := d.Get("project").(string)
|
||||
|
||||
// Get services from config
|
||||
cfgServices := getConfigServices(d)
|
||||
|
||||
// Get services from API
|
||||
apiServices, err := getApiServices(pid, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error updating services: %v", err)
|
||||
}
|
||||
|
||||
// This call disables any APIs that aren't defined in cfgServices,
|
||||
// and enables all of those that are
|
||||
err = reconcileServices(cfgServices, apiServices, config, pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error updating services: %v", err)
|
||||
}
|
||||
|
||||
return resourceGoogleProjectServicesRead(d, meta)
|
||||
}
|
||||
|
||||
func resourceGoogleProjectServicesDelete(d *schema.ResourceData, meta interface{}) error {
|
||||
log.Printf("[DEBUG]: Deleting google_project_services")
|
||||
config := meta.(*Config)
|
||||
services := resourceServices(d)
|
||||
for _, s := range services {
|
||||
disableService(s, d.Id(), config)
|
||||
}
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
|
||||
// This function ensures that the services enabled for a project exactly match that
|
||||
// in a config by disabling any services that are returned by the API but not present
|
||||
// in the config
|
||||
func reconcileServices(cfgServices, apiServices []string, config *Config, pid string) error {
|
||||
// Helper to convert slice to map
|
||||
m := func(vals []string) map[string]struct{} {
|
||||
sm := make(map[string]struct{})
|
||||
for _, s := range vals {
|
||||
sm[s] = struct{}{}
|
||||
}
|
||||
return sm
|
||||
}
|
||||
|
||||
cfgMap := m(cfgServices)
|
||||
apiMap := m(apiServices)
|
||||
|
||||
for k, _ := range apiMap {
|
||||
if _, ok := cfgMap[k]; !ok {
|
||||
// The service in the API is not in the config; disable it.
|
||||
err := disableService(k, pid, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// The service exists in the config and the API, so we don't need
|
||||
// to re-enable it
|
||||
delete(cfgMap, k)
|
||||
}
|
||||
}
|
||||
|
||||
for k, _ := range cfgMap {
|
||||
err := enableService(k, pid, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retrieve services defined in a config
|
||||
func getConfigServices(d *schema.ResourceData) (services []string) {
|
||||
if v, ok := d.GetOk("services"); ok {
|
||||
for _, svc := range v.(*schema.Set).List() {
|
||||
services = append(services, svc.(string))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve a project's services from the API
|
||||
func getApiServices(pid string, config *Config) ([]string, error) {
|
||||
apiServices := make([]string, 0)
|
||||
// Get services from the API
|
||||
svcResp, err := config.clientServiceMan.Services.List().ConsumerId("project:" + pid).Do()
|
||||
if err != nil {
|
||||
return apiServices, err
|
||||
}
|
||||
for _, v := range svcResp.Services {
|
||||
apiServices = append(apiServices, v.ServiceName)
|
||||
}
|
||||
return apiServices, nil
|
||||
}
|
||||
|
||||
func enableService(s, pid string, config *Config) error {
|
||||
esr := newEnableServiceRequest(pid)
|
||||
sop, err := config.clientServiceMan.Services.Enable(s, esr).Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error enabling service %q for project %q: %v", s, pid, err)
|
||||
}
|
||||
// Wait for the operation to complete
|
||||
waitErr := serviceManagementOperationWait(config, sop, "api to enable")
|
||||
if waitErr != nil {
|
||||
return waitErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func disableService(s, pid string, config *Config) error {
|
||||
dsr := newDisableServiceRequest(pid)
|
||||
sop, err := config.clientServiceMan.Services.Disable(s, dsr).Do()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error disabling service %q for project %q: %v", s, pid, err)
|
||||
}
|
||||
// Wait for the operation to complete
|
||||
waitErr := serviceManagementOperationWait(config, sop, "api to disable")
|
||||
if waitErr != nil {
|
||||
return waitErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newEnableServiceRequest(pid string) *servicemanagement.EnableServiceRequest {
|
||||
return &servicemanagement.EnableServiceRequest{ConsumerId: "project:" + pid}
|
||||
}
|
||||
|
||||
func newDisableServiceRequest(pid string) *servicemanagement.DisableServiceRequest {
|
||||
return &servicemanagement.DisableServiceRequest{ConsumerId: "project:" + pid}
|
||||
}
|
||||
|
||||
func resourceServices(d *schema.ResourceData) []string {
|
||||
// Calculate the tags
|
||||
var services []string
|
||||
if s := d.Get("services"); s != nil {
|
||||
ss := s.(*schema.Set)
|
||||
services = make([]string, ss.Len())
|
||||
for i, v := range ss.List() {
|
||||
services[i] = v.(string)
|
||||
}
|
||||
}
|
||||
return services
|
||||
}
|
||||
@ -0,0 +1,178 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/acctest"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"google.golang.org/api/servicemanagement/v1"
|
||||
)
|
||||
|
||||
// Test that services can be enabled and disabled on a project
|
||||
func TestAccGoogleProjectServices_basic(t *testing.T) {
|
||||
pid := "terraform-" + acctest.RandString(10)
|
||||
services1 := []string{"iam.googleapis.com", "cloudresourcemanager.googleapis.com"}
|
||||
services2 := []string{"cloudresourcemanager.googleapis.com"}
|
||||
oobService := "iam.googleapis.com"
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
Steps: []resource.TestStep{
|
||||
// Create a new project with some services
|
||||
resource.TestStep{
|
||||
Config: testAccGoogleProjectAssociateServicesBasic(services1, pid, pname, org),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testProjectServicesMatch(services1, pid),
|
||||
),
|
||||
},
|
||||
// Update services to remove one
|
||||
resource.TestStep{
|
||||
Config: testAccGoogleProjectAssociateServicesBasic(services2, pid, pname, org),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testProjectServicesMatch(services2, pid),
|
||||
),
|
||||
},
|
||||
// Add a service out-of-band and ensure it is removed
|
||||
resource.TestStep{
|
||||
PreConfig: func() {
|
||||
config := testAccProvider.Meta().(*Config)
|
||||
enableService(oobService, pid, config)
|
||||
},
|
||||
Config: testAccGoogleProjectAssociateServicesBasic(services2, pid, pname, org),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testProjectServicesMatch(services2, pid),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Test that services are authoritative when a project has existing
|
||||
// sevices not represented in config
|
||||
func TestAccGoogleProjectServices_authoritative(t *testing.T) {
|
||||
pid := "terraform-" + acctest.RandString(10)
|
||||
services := []string{"cloudresourcemanager.googleapis.com"}
|
||||
oobService := "iam.googleapis.com"
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
Steps: []resource.TestStep{
|
||||
// Create a new project with no services
|
||||
resource.TestStep{
|
||||
Config: testAccGoogleProject_create(pid, pname, org),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckGoogleProjectExists("google_project.acceptance", pid),
|
||||
),
|
||||
},
|
||||
// Add a service out-of-band, then apply a config that creates a service.
|
||||
// It should remove the out-of-band service.
|
||||
resource.TestStep{
|
||||
PreConfig: func() {
|
||||
config := testAccProvider.Meta().(*Config)
|
||||
enableService(oobService, pid, config)
|
||||
},
|
||||
Config: testAccGoogleProjectAssociateServicesBasic(services, pid, pname, org),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testProjectServicesMatch(services, pid),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Test that services are authoritative when a project has existing
|
||||
// sevices, some which are represented in the config and others
|
||||
// that are not
|
||||
func TestAccGoogleProjectServices_authoritative2(t *testing.T) {
|
||||
pid := "terraform-" + acctest.RandString(10)
|
||||
oobServices := []string{"iam.googleapis.com", "cloudresourcemanager.googleapis.com"}
|
||||
services := []string{"iam.googleapis.com"}
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheck(t) },
|
||||
Providers: testAccProviders,
|
||||
Steps: []resource.TestStep{
|
||||
// Create a new project with no services
|
||||
resource.TestStep{
|
||||
Config: testAccGoogleProject_create(pid, pname, org),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckGoogleProjectExists("google_project.acceptance", pid),
|
||||
),
|
||||
},
|
||||
// Add a service out-of-band, then apply a config that creates a service.
|
||||
// It should remove the out-of-band service.
|
||||
resource.TestStep{
|
||||
PreConfig: func() {
|
||||
config := testAccProvider.Meta().(*Config)
|
||||
for _, s := range oobServices {
|
||||
enableService(s, pid, config)
|
||||
}
|
||||
},
|
||||
Config: testAccGoogleProjectAssociateServicesBasic(services, pid, pname, org),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testProjectServicesMatch(services, pid),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccGoogleProjectAssociateServicesBasic(services []string, pid, name, org string) string {
|
||||
return fmt.Sprintf(`
|
||||
resource "google_project" "acceptance" {
|
||||
project_id = "%s"
|
||||
name = "%s"
|
||||
org_id = "%s"
|
||||
}
|
||||
resource "google_project_services" "acceptance" {
|
||||
project = "${google_project.acceptance.project_id}"
|
||||
services = [%s]
|
||||
}
|
||||
`, pid, name, org, testStringsToString(services))
|
||||
}
|
||||
|
||||
func testProjectServicesMatch(services []string, pid string) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
config := testAccProvider.Meta().(*Config)
|
||||
|
||||
apiServices, err := getApiServices(pid, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error listing services for project %q: %v", pid, err)
|
||||
}
|
||||
|
||||
sort.Strings(services)
|
||||
sort.Strings(apiServices)
|
||||
if !reflect.DeepEqual(services, apiServices) {
|
||||
return fmt.Errorf("Services in config (%v) do not exactly match services returned by API (%v)", services, apiServices)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testStringsToString(s []string) string {
|
||||
var b bytes.Buffer
|
||||
for i, v := range s {
|
||||
b.WriteString(fmt.Sprintf("\"%s\"", v))
|
||||
if i < len(s)-1 {
|
||||
b.WriteString(",")
|
||||
}
|
||||
}
|
||||
r := b.String()
|
||||
log.Printf("[DEBUG]: Converted list of strings to %s", r)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func testManagedServicesToString(svcs []*servicemanagement.ManagedService) string {
|
||||
var b bytes.Buffer
|
||||
for _, s := range svcs {
|
||||
b.WriteString(s.ServiceName)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"google.golang.org/api/cloudresourcemanager/v1"
|
||||
)
|
||||
|
||||
type ResourceManagerOperationWaiter struct {
|
||||
Service *cloudresourcemanager.Service
|
||||
Op *cloudresourcemanager.Operation
|
||||
}
|
||||
|
||||
func (w *ResourceManagerOperationWaiter) RefreshFunc() resource.StateRefreshFunc {
|
||||
return func() (interface{}, string, error) {
|
||||
op, err := w.Service.Operations.Get(w.Op.Name).Do()
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Got %v while polling for operation %s's 'done' status", op.Done, w.Op.Name)
|
||||
|
||||
return op, fmt.Sprint(op.Done), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ResourceManagerOperationWaiter) Conf() *resource.StateChangeConf {
|
||||
return &resource.StateChangeConf{
|
||||
Pending: []string{"false"},
|
||||
Target: []string{"true"},
|
||||
Refresh: w.RefreshFunc(),
|
||||
}
|
||||
}
|
||||
|
||||
func resourceManagerOperationWait(config *Config, op *cloudresourcemanager.Operation, activity string) error {
|
||||
return resourceManagerOperationWaitTime(config, op, activity, 4)
|
||||
}
|
||||
|
||||
func resourceManagerOperationWaitTime(config *Config, op *cloudresourcemanager.Operation, activity string, timeoutMin int) error {
|
||||
w := &ResourceManagerOperationWaiter{
|
||||
Service: config.clientResourceManager,
|
||||
Op: op,
|
||||
}
|
||||
|
||||
state := w.Conf()
|
||||
state.Delay = 10 * time.Second
|
||||
state.Timeout = time.Duration(timeoutMin) * time.Minute
|
||||
state.MinTimeout = 2 * time.Second
|
||||
opRaw, err := state.WaitForState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error waiting for %s: %s", activity, err)
|
||||
}
|
||||
|
||||
op = opRaw.(*cloudresourcemanager.Operation)
|
||||
if op.Error != nil {
|
||||
return fmt.Errorf("Error code %v, message: %s", op.Error.Code, op.Error.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package google
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
"google.golang.org/api/servicemanagement/v1"
|
||||
)
|
||||
|
||||
type ServiceManagementOperationWaiter struct {
|
||||
Service *servicemanagement.APIService
|
||||
Op *servicemanagement.Operation
|
||||
}
|
||||
|
||||
func (w *ServiceManagementOperationWaiter) RefreshFunc() resource.StateRefreshFunc {
|
||||
return func() (interface{}, string, error) {
|
||||
var op *servicemanagement.Operation
|
||||
var err error
|
||||
|
||||
op, err = w.Service.Operations.Get(w.Op.Name).Do()
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Got %v while polling for operation %s's 'done' status", op.Done, w.Op.Name)
|
||||
|
||||
return op, fmt.Sprint(op.Done), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ServiceManagementOperationWaiter) Conf() *resource.StateChangeConf {
|
||||
return &resource.StateChangeConf{
|
||||
Pending: []string{"false"},
|
||||
Target: []string{"true"},
|
||||
Refresh: w.RefreshFunc(),
|
||||
}
|
||||
}
|
||||
|
||||
func serviceManagementOperationWait(config *Config, op *servicemanagement.Operation, activity string) error {
|
||||
return serviceManagementOperationWaitTime(config, op, activity, 4)
|
||||
}
|
||||
|
||||
func serviceManagementOperationWaitTime(config *Config, op *servicemanagement.Operation, activity string, timeoutMin int) error {
|
||||
w := &ServiceManagementOperationWaiter{
|
||||
Service: config.clientServiceMan,
|
||||
Op: op,
|
||||
}
|
||||
|
||||
state := w.Conf()
|
||||
state.Delay = 10 * time.Second
|
||||
state.Timeout = time.Duration(timeoutMin) * time.Minute
|
||||
state.MinTimeout = 2 * time.Second
|
||||
opRaw, err := state.WaitForState()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error waiting for %s: %s", activity, err)
|
||||
}
|
||||
|
||||
op = opRaw.(*servicemanagement.Operation)
|
||||
if op.Error != nil {
|
||||
return fmt.Errorf("Error code %v, message: %s", op.Error.Code, op.Error.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
2857
vendor/google.golang.org/api/servicemanagement/v1/servicemanagement-api.json
generated
vendored
2857
vendor/google.golang.org/api/servicemanagement/v1/servicemanagement-api.json
generated
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,69 @@
|
||||
---
|
||||
layout: "google"
|
||||
page_title: "Google: google_project_iam_policy"
|
||||
sidebar_current: "docs-google-project-iam-policy"
|
||||
description: |-
|
||||
Allows management of an IAM policy for a Google Cloud Platform project.
|
||||
---
|
||||
|
||||
# google\_project\_iam\_policy
|
||||
|
||||
Allows creation and management of an IAM policy for an existing Google Cloud
|
||||
Platform project.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```js
|
||||
resource "google_project_iam_policy" "project" {
|
||||
project = "your-project-id"
|
||||
policy_data = "${data.google_iam_policy.admin.policy_data}"
|
||||
}
|
||||
|
||||
data "google_iam_policy" "admin" {
|
||||
binding {
|
||||
role = "roles/editor"
|
||||
members = [
|
||||
"user:jane@example.com",
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `project` - (Required) The project ID.
|
||||
Changing this forces a new project to be created.
|
||||
|
||||
* `policy_data` - (Required) The `google_iam_policy` data source that represents
|
||||
the IAM policy that will be applied to the project. The policy will be
|
||||
merged with any existing policy applied to the project.
|
||||
|
||||
Changing this updates the policy.
|
||||
|
||||
Deleting this removes the policy, but leaves the original project policy
|
||||
intact. If there are overlapping `binding` entries between the original
|
||||
project policy and the data source policy, they will be removed.
|
||||
|
||||
* `authoritative` - (Optional) A boolean value indicating if this policy
|
||||
should overwrite any existing IAM policy on the project. When set to true,
|
||||
**any policies not in your config file will be removed**. This can **lock
|
||||
you out** of your project until an Organization Administrator grants you
|
||||
access again, so please exercise caution. If this argument is `true` and you
|
||||
want to delete the resource, you must set the `disable_project` argument to
|
||||
`true`, acknowledging that the project will be inaccessible to anyone but the
|
||||
Organization Admins, as it will no longer have an IAM policy.
|
||||
|
||||
* `disable_project` - (Optional) A boolean value that must be set to `true`
|
||||
if you want to delete a `google_project_iam_policy` that is authoritative.
|
||||
|
||||
## Attributes Reference
|
||||
|
||||
In addition to the arguments listed above, the following computed attributes are
|
||||
exported:
|
||||
|
||||
* `etag` - (Computed) The etag of the project's IAM policy.
|
||||
|
||||
* `restore_policy` - (Computed) The IAM policy that will be resotred when a
|
||||
non-authoritative policy resource is deleted.
|
||||
@ -0,0 +1,32 @@
|
||||
---
|
||||
layout: "google"
|
||||
page_title: "Google: google_project_services"
|
||||
sidebar_current: "docs-google-project-services"
|
||||
description: |-
|
||||
Allows management of API services for a Google Cloud Platform project.
|
||||
---
|
||||
|
||||
# google\_project\_services
|
||||
|
||||
Allows management of enabled API services for an existing Google Cloud
|
||||
Platform project. Services in an existing project that are not defined
|
||||
in the config will be removed.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```js
|
||||
resource "google_project_services" "project" {
|
||||
project_id = "your-project-id"
|
||||
services = ["iam.googleapis.com", "cloudresourcemanager.googleapis.com"]
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
* `project_id` - (Required) The project ID.
|
||||
Changing this forces a new project to be created.
|
||||
|
||||
* `services` - (Required) The list of services that are enabled. Supports
|
||||
update.
|
||||
Loading…
Reference in new issue