diff --git a/cloudmock/aws/mockiam/api.go b/cloudmock/aws/mockiam/api.go index 3158ffdf4c..12cf2bd1c5 100644 --- a/cloudmock/aws/mockiam/api.go +++ b/cloudmock/aws/mockiam/api.go @@ -33,6 +33,7 @@ type MockIAM struct { InstanceProfiles map[string]*iam.InstanceProfile Roles map[string]*iam.Role RolePolicies []*rolePolicy + AttachedPolicies map[string][]*iam.AttachedPolicy } var _ iamiface.IAMAPI = &MockIAM{} diff --git a/cloudmock/aws/mockiam/iamrole.go b/cloudmock/aws/mockiam/iamrole.go index 7c8c6afdc5..5a3d9852fd 100644 --- a/cloudmock/aws/mockiam/iamrole.go +++ b/cloudmock/aws/mockiam/iamrole.go @@ -141,9 +141,47 @@ func (m *MockIAM) DeleteRole(request *iam.DeleteRoleInput) (*iam.DeleteRoleOutpu return &iam.DeleteRoleOutput{}, nil } + func (m *MockIAM) DeleteRoleWithContext(aws.Context, *iam.DeleteRoleInput, ...request.Option) (*iam.DeleteRoleOutput, error) { panic("Not implemented") } + func (m *MockIAM) DeleteRoleRequest(*iam.DeleteRoleInput) (*request.Request, *iam.DeleteRoleOutput) { panic("Not implemented") } + +func (m *MockIAM) ListAttachedRolePolicies(input *iam.ListAttachedRolePoliciesInput) (*iam.ListAttachedRolePoliciesOutput, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + klog.Infof("ListAttachedRolePolicies: %s", aws.StringValue(input.RoleName)) + + for _, r := range m.Roles { + if r.RoleName == input.RoleName { + role := aws.StringValue(r.RoleName) + + return &iam.ListAttachedRolePoliciesOutput{ + AttachedPolicies: m.AttachedPolicies[role], + }, nil + } + } + + return &iam.ListAttachedRolePoliciesOutput{}, nil +} + +func (m *MockIAM) ListAttachedRolePoliciesPages(input *iam.ListAttachedRolePoliciesInput, pager func(*iam.ListAttachedRolePoliciesOutput, bool) bool) error { + m.mutex.Lock() + defer m.mutex.Unlock() + + klog.Infof("ListAttachedRolePolicies: %s", aws.StringValue(input.RoleName)) + + role := aws.StringValue(input.RoleName) + + if pager(&iam.ListAttachedRolePoliciesOutput{ + AttachedPolicies: m.AttachedPolicies[role], + }, true) { + return nil + } + + return nil +} diff --git a/cmd/kops/integration_test.go b/cmd/kops/integration_test.go index a8b8215ab2..463a251a0d 100644 --- a/cmd/kops/integration_test.go +++ b/cmd/kops/integration_test.go @@ -82,6 +82,11 @@ func TestComplex(t *testing.T) { runTestCloudformation(t, "complex.example.com", "complex", "v1alpha2", false, nil, true) } +// TestExternalPolicies tests external policies output +func TestExternalPolicies(t *testing.T) { + runTestAWS(t, "externalpolicies.example.com", "externalpolicies", "v1alpha2", false, 1, true, false, nil, true, false) +} + func TestNoSSHKey(t *testing.T) { runTestAWS(t, "nosshkey.example.com", "nosshkey", "v1alpha2", false, 1, true, false, nil, false, false) } diff --git a/docs/iam_roles.md b/docs/iam_roles.md index e889c9b6de..cdc819376e 100644 --- a/docs/iam_roles.md +++ b/docs/iam_roles.md @@ -57,6 +57,26 @@ The additional permissions are: } ``` +## Adding External Policies + +At times you may want to attach policies shared to you by another AWS account or that are maintained by an outside application. You can specify managed policies through the `policyOverrides` spec field. + +Policy Overrides are specified by their ARN on AWS and are grouped by their role type. See the example below: + +```yaml +spec: + externalPolicies: + node: + - aws:arn:iam:123456789000:policy:test-policy + master: + - aws:arn:iam:123456789000:policy:test-policy + bastion: + - aws:arn:iam:123456789000:policy:test-policy +``` + +External Policy attachments are treated declaritively. Any policies declared will be attached to the role, any policies not specified will be detached _after_ new policies are attached. This does not replace or affect built in Kops policies in any way. + +It's important to note that externalPolicies will only handle the attachment and detachment of policies, not creation, modification, or deletion. ## Adding Additional Policies diff --git a/hack/make-apimachinery.sh b/hack/make-apimachinery.sh index 7ae086b448..106001710c 100755 --- a/hack/make-apimachinery.sh +++ b/hack/make-apimachinery.sh @@ -18,7 +18,7 @@ . $(dirname "${BASH_SOURCE}")/common.sh -WORK_DIR=`mktemp -d` +WORK_DIR=$(mktemp -d) cleanup() { chmod -R +w "${WORK_DIR}" @@ -26,19 +26,19 @@ cleanup() { } trap cleanup EXIT -mkdir -p ${WORK_DIR}/go/ -ln -s ${GOPATH}/src/k8s.io/kops/vendor/ ${WORK_DIR}/go/src +mkdir -p "${WORK_DIR}/go/" +cp -R "${GOPATH}/src/k8s.io/kops/vendor/" "${WORK_DIR}/go/src" unset GOBIN -GOPATH=${WORK_DIR}/go/ go install -v k8s.io/code-generator/cmd/conversion-gen/ -cp ${WORK_DIR}/go/bin/conversion-gen ${GOPATH}/bin/ -GOPATH=${WORK_DIR}/go/ go install k8s.io/code-generator/cmd/deepcopy-gen/ -cp ${WORK_DIR}/go/bin/deepcopy-gen ${GOPATH}/bin/ +env GOBIN="${WORK_DIR}/go/bin" GOPATH="${WORK_DIR}/go/" go install -v k8s.io/code-generator/cmd/conversion-gen/ +cp "${WORK_DIR}/go/bin/conversion-gen" "${GOPATH}/bin/" -GOPATH=${WORK_DIR}/go/ go install k8s.io/code-generator/cmd/defaulter-gen/ -cp ${WORK_DIR}/go/bin/defaulter-gen ${GOPATH}/bin/ +env GOBIN="${WORK_DIR}/go/bin" GOPATH="${WORK_DIR}/go/" go install k8s.io/code-generator/cmd/deepcopy-gen/ +cp "${WORK_DIR}/go/bin/deepcopy-gen" "${GOPATH}/bin/" -GOPATH=${WORK_DIR}/go/ go install k8s.io/code-generator/cmd/client-gen/ -cp ${WORK_DIR}/go/bin/client-gen ${GOPATH}/bin/ +env GOBIN="${WORK_DIR}/go/bin" GOPATH="${WORK_DIR}/go/" go install k8s.io/code-generator/cmd/defaulter-gen/ +cp "${WORK_DIR}/go/bin/defaulter-gen" "${GOPATH}/bin/" +env GOBIN="${WORK_DIR}/go/bin" GOPATH="${WORK_DIR}/go/" go install k8s.io/code-generator/cmd/client-gen/ +cp "${WORK_DIR}/go/bin/client-gen" "${GOPATH}/bin/" \ No newline at end of file diff --git a/k8s/crds/kops.k8s.io_clusters.yaml b/k8s/crds/kops.k8s.io_clusters.yaml index 5d4b31a169..f725f57c13 100644 --- a/k8s/crds/kops.k8s.io_clusters.yaml +++ b/k8s/crds/kops.k8s.io_clusters.yaml @@ -670,6 +670,14 @@ spec: (use to control whom can creates dns entries) type: string type: object + externalPolicies: + additionalProperties: + items: + type: string + type: array + description: ExternalPolicies allows the insertion of pre-existing managed + policies on IG Roles + type: object fileAssets: description: A collection of files assets for deployed cluster wide items: diff --git a/pkg/apis/kops/cluster.go b/pkg/apis/kops/cluster.go index f066bacc8d..6613f0fede 100644 --- a/pkg/apis/kops/cluster.go +++ b/pkg/apis/kops/cluster.go @@ -136,6 +136,8 @@ type ClusterSpec struct { // 'external' do not apply updates automatically - they are applied manually or by an external system // missing: default policy (currently OS security upgrades that do not require a reboot) UpdatePolicy *string `json:"updatePolicy,omitempty"` + // ExternalPolicies allows the insertion of pre-existing managed policies on IG Roles + ExternalPolicies *map[string][]string `json:"externalPolicies,omitempty"` // Additional policies to add for roles AdditionalPolicies *map[string]string `json:"additionalPolicies,omitempty"` // A collection of files assets for deployed cluster wide diff --git a/pkg/apis/kops/v1alpha1/cluster.go b/pkg/apis/kops/v1alpha1/cluster.go index d3b55dea5e..66faa91ae1 100644 --- a/pkg/apis/kops/v1alpha1/cluster.go +++ b/pkg/apis/kops/v1alpha1/cluster.go @@ -130,6 +130,8 @@ type ClusterSpec struct { // 'external' do not apply updates automatically - they are applied manually or by an external system // missing: default policy (currently OS security upgrades that do not require a reboot) UpdatePolicy *string `json:"updatePolicy,omitempty"` + // ExternalPolicies allows the insertion of pre-existing managed policies on IG Roles + ExternalPolicies *map[string][]string `json:"externalPolicies,omitempty"` // Additional policies to add for roles AdditionalPolicies *map[string]string `json:"additionalPolicies,omitempty"` // A collection of files assets for deployed cluster wide diff --git a/pkg/apis/kops/v1alpha1/zz_generated.conversion.go b/pkg/apis/kops/v1alpha1/zz_generated.conversion.go index e58eea7f9a..7ca0da9353 100644 --- a/pkg/apis/kops/v1alpha1/zz_generated.conversion.go +++ b/pkg/apis/kops/v1alpha1/zz_generated.conversion.go @@ -1674,6 +1674,7 @@ func autoConvert_v1alpha1_ClusterSpec_To_kops_ClusterSpec(in *ClusterSpec, out * // WARNING: in.AdminAccess requires manual conversion: does not exist in peer-type out.IsolateMasters = in.IsolateMasters out.UpdatePolicy = in.UpdatePolicy + out.ExternalPolicies = in.ExternalPolicies out.AdditionalPolicies = in.AdditionalPolicies if in.FileAssets != nil { in, out := &in.FileAssets, &out.FileAssets @@ -1991,6 +1992,7 @@ func autoConvert_kops_ClusterSpec_To_v1alpha1_ClusterSpec(in *kops.ClusterSpec, // WARNING: in.KubernetesAPIAccess requires manual conversion: does not exist in peer-type out.IsolateMasters = in.IsolateMasters out.UpdatePolicy = in.UpdatePolicy + out.ExternalPolicies = in.ExternalPolicies out.AdditionalPolicies = in.AdditionalPolicies if in.FileAssets != nil { in, out := &in.FileAssets, &out.FileAssets diff --git a/pkg/apis/kops/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/kops/v1alpha1/zz_generated.deepcopy.go index d03286ce25..6c2b8aca8d 100644 --- a/pkg/apis/kops/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/kops/v1alpha1/zz_generated.deepcopy.go @@ -651,6 +651,25 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { *out = new(string) **out = **in } + if in.ExternalPolicies != nil { + in, out := &in.ExternalPolicies, &out.ExternalPolicies + *out = new(map[string][]string) + if **in != nil { + in, out := *in, *out + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + } if in.AdditionalPolicies != nil { in, out := &in.AdditionalPolicies, &out.AdditionalPolicies *out = new(map[string]string) diff --git a/pkg/apis/kops/v1alpha2/cluster.go b/pkg/apis/kops/v1alpha2/cluster.go index 23ab7533af..5cceda4fab 100644 --- a/pkg/apis/kops/v1alpha2/cluster.go +++ b/pkg/apis/kops/v1alpha2/cluster.go @@ -135,6 +135,8 @@ type ClusterSpec struct { // 'external' do not apply updates automatically - they are applied manually or by an external system // missing: default policy (currently OS security upgrades that do not require a reboot) UpdatePolicy *string `json:"updatePolicy,omitempty"` + // ExternalPolicies allows the insertion of pre-existing managed policies on IG Roles + ExternalPolicies *map[string][]string `json:"externalPolicies,omitempty"` // Additional policies to add for roles AdditionalPolicies *map[string]string `json:"additionalPolicies,omitempty"` // A collection of files assets for deployed cluster wide diff --git a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go index 0c66aa7434..103a32abb1 100644 --- a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go +++ b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go @@ -1737,6 +1737,7 @@ func autoConvert_v1alpha2_ClusterSpec_To_kops_ClusterSpec(in *ClusterSpec, out * out.KubernetesAPIAccess = in.KubernetesAPIAccess out.IsolateMasters = in.IsolateMasters out.UpdatePolicy = in.UpdatePolicy + out.ExternalPolicies = in.ExternalPolicies out.AdditionalPolicies = in.AdditionalPolicies if in.FileAssets != nil { in, out := &in.FileAssets, &out.FileAssets @@ -2059,6 +2060,7 @@ func autoConvert_kops_ClusterSpec_To_v1alpha2_ClusterSpec(in *kops.ClusterSpec, out.KubernetesAPIAccess = in.KubernetesAPIAccess out.IsolateMasters = in.IsolateMasters out.UpdatePolicy = in.UpdatePolicy + out.ExternalPolicies = in.ExternalPolicies out.AdditionalPolicies = in.AdditionalPolicies if in.FileAssets != nil { in, out := &in.FileAssets, &out.FileAssets diff --git a/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go index 918afa559a..8f451b93bb 100644 --- a/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go @@ -634,6 +634,25 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { *out = new(string) **out = **in } + if in.ExternalPolicies != nil { + in, out := &in.ExternalPolicies, &out.ExternalPolicies + *out = new(map[string][]string) + if **in != nil { + in, out := *in, *out + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + } if in.AdditionalPolicies != nil { in, out := &in.AdditionalPolicies, &out.AdditionalPolicies *out = new(map[string]string) diff --git a/pkg/apis/kops/zz_generated.deepcopy.go b/pkg/apis/kops/zz_generated.deepcopy.go index c8ae8adbbb..19f773353f 100644 --- a/pkg/apis/kops/zz_generated.deepcopy.go +++ b/pkg/apis/kops/zz_generated.deepcopy.go @@ -734,6 +734,25 @@ func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { *out = new(string) **out = **in } + if in.ExternalPolicies != nil { + in, out := &in.ExternalPolicies, &out.ExternalPolicies + *out = new(map[string][]string) + if **in != nil { + in, out := *in, *out + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + } if in.AdditionalPolicies != nil { in, out := &in.AdditionalPolicies, &out.AdditionalPolicies *out = new(map[string]string) diff --git a/pkg/model/iam.go b/pkg/model/iam.go index d7e41678a7..a18e610fdb 100644 --- a/pkg/model/iam.go +++ b/pkg/model/iam.go @@ -165,6 +165,27 @@ func (b *IAMModelBuilder) buildIAMTasks(igRole kops.InstanceGroupRole, iamName s c.AddTask(iamInstanceProfileRole) } + // Create External Policy tasks + { + var externalPolicies []string + + if b.Cluster.Spec.ExternalPolicies != nil { + p := *(b.Cluster.Spec.ExternalPolicies) + externalPolicies = append(externalPolicies, p[strings.ToLower(string(igRole))]...) + } + + name := fmt.Sprintf("%s-policyoverride", strings.ToLower(string(igRole))) + t := &awstasks.IAMRolePolicy{ + Name: s(name), + Lifecycle: b.Lifecycle, + Role: iamRole, + Managed: true, + ExternalPolicies: &externalPolicies, + } + + c.AddTask(t) + } + // Generate additional policies if needed, and attach to existing role { additionalPolicy := "" diff --git a/pkg/resources/aws/aws.go b/pkg/resources/aws/aws.go index 535caa6e98..4ed9c7eef5 100644 --- a/pkg/resources/aws/aws.go +++ b/pkg/resources/aws/aws.go @@ -1889,11 +1889,13 @@ func ListRoute53Records(cloud fi.Cloud, clusterName string) ([]*resources.Resour } func DeleteIAMRole(cloud fi.Cloud, r *resources.Resource) error { - c := cloud.(awsup.AWSCloud) + var attachedPolicies []*iam.AttachedPolicy + var policyNames []string + c := cloud.(awsup.AWSCloud) roleName := r.Name - var policyNames []string + // List Inline policies { request := &iam.ListRolePoliciesInput{ RoleName: aws.String(roleName), @@ -1914,6 +1916,26 @@ func DeleteIAMRole(cloud fi.Cloud, r *resources.Resource) error { } } + // List Attached Policies + { + request := &iam.ListAttachedRolePoliciesInput{ + RoleName: aws.String(roleName), + } + err := c.IAM().ListAttachedRolePoliciesPages(request, func(page *iam.ListAttachedRolePoliciesOutput, lastPage bool) bool { + attachedPolicies = append(attachedPolicies, page.AttachedPolicies...) + return true + }) + if err != nil { + if awsup.AWSErrorCode(err) == "NoSuchEntity" { + klog.V(2).Infof("Got NoSuchEntity describing IAM RolePolicy %q; will treat as already-detached", roleName) + return nil + } + + return fmt.Errorf("error listing IAM role policies for %q: %v", roleName, err) + } + } + + // Delete inline policies for _, policyName := range policyNames { klog.V(2).Infof("Deleting IAM role policy %q %q", roleName, policyName) request := &iam.DeleteRolePolicyInput{ @@ -1926,6 +1948,20 @@ func DeleteIAMRole(cloud fi.Cloud, r *resources.Resource) error { } } + // Detach Managed Policies + for _, policy := range attachedPolicies { + klog.V(2).Infof("Deleting IAM role policy %q %q", roleName, policy) + request := &iam.DetachRolePolicyInput{ + RoleName: aws.String(r.Name), + PolicyArn: policy.PolicyArn, + } + _, err := c.IAM().DetachRolePolicy(request) + if err != nil { + return fmt.Errorf("error detaching IAM role policy %q %q: %v", roleName, *policy.PolicyArn, err) + } + } + + // Delete Role { klog.V(2).Infof("Deleting IAM role %q", r.Name) request := &iam.DeleteRoleInput{ diff --git a/tests/integration/update_cluster/externalpolicies/id_rsa.pub b/tests/integration/update_cluster/externalpolicies/id_rsa.pub new file mode 100755 index 0000000000..81cb012783 --- /dev/null +++ b/tests/integration/update_cluster/externalpolicies/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCtWu40XQo8dczLsCq0OWV+hxm9uV3WxeH9Kgh4sMzQxNtoU1pvW0XdjpkBesRKGoolfWeCLXWxpyQb1IaiMkKoz7MdhQ/6UKjMjP66aFWWp3pwD0uj0HuJ7tq4gKHKRYGTaZIRWpzUiANBrjugVgA+Sd7E/mYwc/DMXkIyRZbvhQ== diff --git a/tests/integration/update_cluster/externalpolicies/in-v1alpha2.yaml b/tests/integration/update_cluster/externalpolicies/in-v1alpha2.yaml new file mode 100644 index 0000000000..c489b869fd --- /dev/null +++ b/tests/integration/update_cluster/externalpolicies/in-v1alpha2.yaml @@ -0,0 +1,103 @@ +apiVersion: kops.k8s.io/v1alpha2 +kind: Cluster +metadata: + creationTimestamp: "2016-12-10T22:42:27Z" + name: externalpolicies.example.com +spec: + api: + loadBalancer: + type: Public + additionalSecurityGroups: + - sg-exampleid3 + - sg-exampleid4 + kubernetesApiAccess: + - 0.0.0.0/0 + channel: stable + cloudProvider: aws + cloudLabels: + Owner: John Doe + foo/bar: fib+baz + configBase: memfs://clusters.example.com/externalpolicies.example.com + etcdClusters: + - etcdMembers: + - instanceGroup: master-us-test-1a + name: us-test-1a + name: main + - etcdMembers: + - instanceGroup: master-us-test-1a + name: us-test-1a + name: events + kubeAPIServer: + serviceNodePortRange: 28000-32767 + auditWebhookBatchThrottleQps: 3.14 + kubernetesVersion: v1.14.0 + masterInternalName: api.internal.externalpolicies.example.com + masterPublicName: api.externalpolicies.example.com + networkCIDR: 172.20.0.0/16 + networking: + kubenet: {} + nodePortAccess: + - 1.2.3.4/32 + - 10.20.30.0/24 + nonMasqueradeCIDR: 100.64.0.0/10 + sshAccess: + - 0.0.0.0/0 + topology: + masters: public + nodes: public + externalPolicies: + node: + - aws:arn:iam:123456789000:policy:test-policy + master: + - aws:arn:iam:123456789000:policy:test-policy + bastion: + - aws:arn:iam:123456789000:policy:test-policy + subnets: + - cidr: 172.20.32.0/19 + name: us-test-1a + type: Public + zone: us-test-1a + +--- + +apiVersion: kops.k8s.io/v1alpha2 +kind: InstanceGroup +metadata: + creationTimestamp: "2016-12-10T22:42:28Z" + name: nodes + labels: + kops.k8s.io/cluster: externalpolicies.example.com +spec: + additionalSecurityGroups: + - sg-exampleid3 + - sg-exampleid4 + associatePublicIp: true + suspendProcesses: + - AZRebalance + image: kope.io/k8s-1.4-debian-jessie-amd64-hvm-ebs-2016-10-21 + machineType: t2.medium + maxSize: 2 + minSize: 2 + role: Node + subnets: + - us-test-1a + detailedInstanceMonitoring: true + +--- + +apiVersion: kops.k8s.io/v1alpha2 +kind: InstanceGroup +metadata: + creationTimestamp: "2016-12-10T22:42:28Z" + name: master-us-test-1a + labels: + kops.k8s.io/cluster: externalpolicies.example.com +spec: + associatePublicIp: true + image: kope.io/k8s-1.4-debian-jessie-amd64-hvm-ebs-2016-10-21 + machineType: m3.medium + maxSize: 1 + minSize: 1 + role: Master + subnets: + - us-test-1a diff --git a/tests/integration/update_cluster/externalpolicies/kubernetes.tf b/tests/integration/update_cluster/externalpolicies/kubernetes.tf new file mode 100644 index 0000000000..20211696d4 --- /dev/null +++ b/tests/integration/update_cluster/externalpolicies/kubernetes.tf @@ -0,0 +1,644 @@ +locals = { + cluster_name = "externalpolicies.example.com" + master_autoscaling_group_ids = ["${aws_autoscaling_group.master-us-test-1a-masters-externalpolicies-example-com.id}"] + master_security_group_ids = ["${aws_security_group.masters-externalpolicies-example-com.id}"] + masters_role_arn = "${aws_iam_role.masters-externalpolicies-example-com.arn}" + masters_role_name = "${aws_iam_role.masters-externalpolicies-example-com.name}" + node_autoscaling_group_ids = ["${aws_autoscaling_group.nodes-externalpolicies-example-com.id}"] + node_security_group_ids = ["${aws_security_group.nodes-externalpolicies-example-com.id}", "sg-exampleid3", "sg-exampleid4"] + node_subnet_ids = ["${aws_subnet.us-test-1a-externalpolicies-example-com.id}"] + nodes_role_arn = "${aws_iam_role.nodes-externalpolicies-example-com.arn}" + nodes_role_name = "${aws_iam_role.nodes-externalpolicies-example-com.name}" + region = "us-test-1" + route_table_public_id = "${aws_route_table.externalpolicies-example-com.id}" + subnet_us-test-1a_id = "${aws_subnet.us-test-1a-externalpolicies-example-com.id}" + vpc_cidr_block = "${aws_vpc.externalpolicies-example-com.cidr_block}" + vpc_id = "${aws_vpc.externalpolicies-example-com.id}" +} + +output "cluster_name" { + value = "externalpolicies.example.com" +} + +output "master_autoscaling_group_ids" { + value = ["${aws_autoscaling_group.master-us-test-1a-masters-externalpolicies-example-com.id}"] +} + +output "master_security_group_ids" { + value = ["${aws_security_group.masters-externalpolicies-example-com.id}"] +} + +output "masters_role_arn" { + value = "${aws_iam_role.masters-externalpolicies-example-com.arn}" +} + +output "masters_role_name" { + value = "${aws_iam_role.masters-externalpolicies-example-com.name}" +} + +output "node_autoscaling_group_ids" { + value = ["${aws_autoscaling_group.nodes-externalpolicies-example-com.id}"] +} + +output "node_security_group_ids" { + value = ["${aws_security_group.nodes-externalpolicies-example-com.id}", "sg-exampleid3", "sg-exampleid4"] +} + +output "node_subnet_ids" { + value = ["${aws_subnet.us-test-1a-externalpolicies-example-com.id}"] +} + +output "nodes_role_arn" { + value = "${aws_iam_role.nodes-externalpolicies-example-com.arn}" +} + +output "nodes_role_name" { + value = "${aws_iam_role.nodes-externalpolicies-example-com.name}" +} + +output "region" { + value = "us-test-1" +} + +output "route_table_public_id" { + value = "${aws_route_table.externalpolicies-example-com.id}" +} + +output "subnet_us-test-1a_id" { + value = "${aws_subnet.us-test-1a-externalpolicies-example-com.id}" +} + +output "vpc_cidr_block" { + value = "${aws_vpc.externalpolicies-example-com.cidr_block}" +} + +output "vpc_id" { + value = "${aws_vpc.externalpolicies-example-com.id}" +} + +provider "aws" { + region = "us-test-1" +} + +resource "aws_autoscaling_attachment" "master-us-test-1a-masters-externalpolicies-example-com" { + elb = "${aws_elb.api-externalpolicies-example-com.id}" + autoscaling_group_name = "${aws_autoscaling_group.master-us-test-1a-masters-externalpolicies-example-com.id}" +} + +resource "aws_autoscaling_group" "master-us-test-1a-masters-externalpolicies-example-com" { + name = "master-us-test-1a.masters.externalpolicies.example.com" + launch_configuration = "${aws_launch_configuration.master-us-test-1a-masters-externalpolicies-example-com.id}" + max_size = 1 + min_size = 1 + vpc_zone_identifier = ["${aws_subnet.us-test-1a-externalpolicies-example-com.id}"] + + tag = { + key = "KubernetesCluster" + value = "externalpolicies.example.com" + propagate_at_launch = true + } + + tag = { + key = "Name" + value = "master-us-test-1a.masters.externalpolicies.example.com" + propagate_at_launch = true + } + + tag = { + key = "Owner" + value = "John Doe" + propagate_at_launch = true + } + + tag = { + key = "foo/bar" + value = "fib+baz" + propagate_at_launch = true + } + + tag = { + key = "k8s.io/role/master" + value = "1" + propagate_at_launch = true + } + + tag = { + key = "kops.k8s.io/instancegroup" + value = "master-us-test-1a" + propagate_at_launch = true + } + + metrics_granularity = "1Minute" + enabled_metrics = ["GroupDesiredCapacity", "GroupInServiceInstances", "GroupMaxSize", "GroupMinSize", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"] +} + +resource "aws_autoscaling_group" "nodes-externalpolicies-example-com" { + name = "nodes.externalpolicies.example.com" + launch_configuration = "${aws_launch_configuration.nodes-externalpolicies-example-com.id}" + max_size = 2 + min_size = 2 + vpc_zone_identifier = ["${aws_subnet.us-test-1a-externalpolicies-example-com.id}"] + + tag = { + key = "KubernetesCluster" + value = "externalpolicies.example.com" + propagate_at_launch = true + } + + tag = { + key = "Name" + value = "nodes.externalpolicies.example.com" + propagate_at_launch = true + } + + tag = { + key = "Owner" + value = "John Doe" + propagate_at_launch = true + } + + tag = { + key = "foo/bar" + value = "fib+baz" + propagate_at_launch = true + } + + tag = { + key = "k8s.io/role/node" + value = "1" + propagate_at_launch = true + } + + tag = { + key = "kops.k8s.io/instancegroup" + value = "nodes" + propagate_at_launch = true + } + + metrics_granularity = "1Minute" + enabled_metrics = ["GroupDesiredCapacity", "GroupInServiceInstances", "GroupMaxSize", "GroupMinSize", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"] + suspended_processes = ["AZRebalance"] +} + +resource "aws_ebs_volume" "us-test-1a-etcd-events-externalpolicies-example-com" { + availability_zone = "us-test-1a" + size = 20 + type = "gp2" + encrypted = false + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "us-test-1a.etcd-events.externalpolicies.example.com" + Owner = "John Doe" + "foo/bar" = "fib+baz" + "k8s.io/etcd/events" = "us-test-1a/us-test-1a" + "k8s.io/role/master" = "1" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + } +} + +resource "aws_ebs_volume" "us-test-1a-etcd-main-externalpolicies-example-com" { + availability_zone = "us-test-1a" + size = 20 + type = "gp2" + encrypted = false + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "us-test-1a.etcd-main.externalpolicies.example.com" + Owner = "John Doe" + "foo/bar" = "fib+baz" + "k8s.io/etcd/main" = "us-test-1a/us-test-1a" + "k8s.io/role/master" = "1" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + } +} + +resource "aws_elb" "api-externalpolicies-example-com" { + name = "api-externalpolicies-exam-5cse45" + + listener = { + instance_port = 443 + instance_protocol = "TCP" + lb_port = 443 + lb_protocol = "TCP" + } + + security_groups = ["${aws_security_group.api-elb-externalpolicies-example-com.id}", "sg-exampleid3", "sg-exampleid4"] + subnets = ["${aws_subnet.us-test-1a-externalpolicies-example-com.id}"] + + health_check = { + target = "SSL:443" + healthy_threshold = 2 + unhealthy_threshold = 2 + interval = 10 + timeout = 5 + } + + cross_zone_load_balancing = false + idle_timeout = 300 + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "api.externalpolicies.example.com" + Owner = "John Doe" + "foo/bar" = "fib+baz" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + } +} + +resource "aws_iam_instance_profile" "masters-externalpolicies-example-com" { + name = "masters.externalpolicies.example.com" + role = "${aws_iam_role.masters-externalpolicies-example-com.name}" +} + +resource "aws_iam_instance_profile" "nodes-externalpolicies-example-com" { + name = "nodes.externalpolicies.example.com" + role = "${aws_iam_role.nodes-externalpolicies-example-com.name}" +} + +resource "aws_iam_role" "masters-externalpolicies-example-com" { + name = "masters.externalpolicies.example.com" + assume_role_policy = "${file("${path.module}/data/aws_iam_role_masters.externalpolicies.example.com_policy")}" +} + +resource "aws_iam_role" "nodes-externalpolicies-example-com" { + name = "nodes.externalpolicies.example.com" + assume_role_policy = "${file("${path.module}/data/aws_iam_role_nodes.externalpolicies.example.com_policy")}" +} + +resource "aws_iam_role_policy" "masters-externalpolicies-example-com" { + name = "masters.externalpolicies.example.com" + role = "${aws_iam_role.masters-externalpolicies-example-com.name}" + policy = "${file("${path.module}/data/aws_iam_role_policy_masters.externalpolicies.example.com_policy")}" +} + +resource "aws_iam_role_policy" "nodes-externalpolicies-example-com" { + name = "nodes.externalpolicies.example.com" + role = "${aws_iam_role.nodes-externalpolicies-example-com.name}" + policy = "${file("${path.module}/data/aws_iam_role_policy_nodes.externalpolicies.example.com_policy")}" +} + +resource "aws_iam_role_policy_attachment" "master-policyoverride-1178482798" { + role = "${aws_iam_role.masters-externalpolicies-example-com.name}" + policy_arn = "aws:arn:iam:123456789000:policy:test-policy" +} + +resource "aws_iam_role_policy_attachment" "node-policyoverride-1178482798" { + role = "${aws_iam_role.nodes-externalpolicies-example-com.name}" + policy_arn = "aws:arn:iam:123456789000:policy:test-policy" +} + +resource "aws_internet_gateway" "externalpolicies-example-com" { + vpc_id = "${aws_vpc.externalpolicies-example-com.id}" + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "externalpolicies.example.com" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + } +} + +resource "aws_key_pair" "kubernetes-externalpolicies-example-com-c4a6ed9aa889b9e2c39cd663eb9c7157" { + key_name = "kubernetes.externalpolicies.example.com-c4:a6:ed:9a:a8:89:b9:e2:c3:9c:d6:63:eb:9c:71:57" + public_key = "${file("${path.module}/data/aws_key_pair_kubernetes.externalpolicies.example.com-c4a6ed9aa889b9e2c39cd663eb9c7157_public_key")}" +} + +resource "aws_launch_configuration" "master-us-test-1a-masters-externalpolicies-example-com" { + name_prefix = "master-us-test-1a.masters.externalpolicies.example.com-" + image_id = "ami-12345678" + instance_type = "m3.medium" + key_name = "${aws_key_pair.kubernetes-externalpolicies-example-com-c4a6ed9aa889b9e2c39cd663eb9c7157.id}" + iam_instance_profile = "${aws_iam_instance_profile.masters-externalpolicies-example-com.id}" + security_groups = ["${aws_security_group.masters-externalpolicies-example-com.id}"] + associate_public_ip_address = true + user_data = "${file("${path.module}/data/aws_launch_configuration_master-us-test-1a.masters.externalpolicies.example.com_user_data")}" + + root_block_device = { + volume_type = "gp2" + volume_size = 64 + delete_on_termination = true + } + + ephemeral_block_device = { + device_name = "/dev/sdc" + virtual_name = "ephemeral0" + } + + lifecycle = { + create_before_destroy = true + } + + enable_monitoring = false +} + +resource "aws_launch_configuration" "nodes-externalpolicies-example-com" { + name_prefix = "nodes.externalpolicies.example.com-" + image_id = "ami-12345678" + instance_type = "t2.medium" + key_name = "${aws_key_pair.kubernetes-externalpolicies-example-com-c4a6ed9aa889b9e2c39cd663eb9c7157.id}" + iam_instance_profile = "${aws_iam_instance_profile.nodes-externalpolicies-example-com.id}" + security_groups = ["${aws_security_group.nodes-externalpolicies-example-com.id}", "sg-exampleid3", "sg-exampleid4"] + associate_public_ip_address = true + user_data = "${file("${path.module}/data/aws_launch_configuration_nodes.externalpolicies.example.com_user_data")}" + + root_block_device = { + volume_type = "gp2" + volume_size = 128 + delete_on_termination = true + } + + lifecycle = { + create_before_destroy = true + } + + enable_monitoring = true +} + +resource "aws_route" "route-0-0-0-0--0" { + route_table_id = "${aws_route_table.externalpolicies-example-com.id}" + destination_cidr_block = "0.0.0.0/0" + gateway_id = "${aws_internet_gateway.externalpolicies-example-com.id}" +} + +resource "aws_route53_record" "api-externalpolicies-example-com" { + name = "api.externalpolicies.example.com" + type = "A" + + alias = { + name = "${aws_elb.api-externalpolicies-example-com.dns_name}" + zone_id = "${aws_elb.api-externalpolicies-example-com.zone_id}" + evaluate_target_health = false + } + + zone_id = "/hostedzone/Z1AFAKE1ZON3YO" +} + +resource "aws_route_table" "externalpolicies-example-com" { + vpc_id = "${aws_vpc.externalpolicies-example-com.id}" + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "externalpolicies.example.com" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + "kubernetes.io/kops/role" = "public" + } +} + +resource "aws_route_table_association" "us-test-1a-externalpolicies-example-com" { + subnet_id = "${aws_subnet.us-test-1a-externalpolicies-example-com.id}" + route_table_id = "${aws_route_table.externalpolicies-example-com.id}" +} + +resource "aws_security_group" "api-elb-externalpolicies-example-com" { + name = "api-elb.externalpolicies.example.com" + vpc_id = "${aws_vpc.externalpolicies-example-com.id}" + description = "Security group for api ELB" + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "api-elb.externalpolicies.example.com" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + } +} + +resource "aws_security_group" "masters-externalpolicies-example-com" { + name = "masters.externalpolicies.example.com" + vpc_id = "${aws_vpc.externalpolicies-example-com.id}" + description = "Security group for masters" + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "masters.externalpolicies.example.com" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + } +} + +resource "aws_security_group" "nodes-externalpolicies-example-com" { + name = "nodes.externalpolicies.example.com" + vpc_id = "${aws_vpc.externalpolicies-example-com.id}" + description = "Security group for nodes" + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "nodes.externalpolicies.example.com" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + } +} + +resource "aws_security_group_rule" "all-master-to-master" { + type = "ingress" + security_group_id = "${aws_security_group.masters-externalpolicies-example-com.id}" + source_security_group_id = "${aws_security_group.masters-externalpolicies-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" +} + +resource "aws_security_group_rule" "all-master-to-node" { + type = "ingress" + security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + source_security_group_id = "${aws_security_group.masters-externalpolicies-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" +} + +resource "aws_security_group_rule" "all-node-to-node" { + type = "ingress" + security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + source_security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" +} + +resource "aws_security_group_rule" "api-elb-egress" { + type = "egress" + security_group_id = "${aws_security_group.api-elb-externalpolicies-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "https-api-elb-0-0-0-0--0" { + type = "ingress" + security_group_id = "${aws_security_group.api-elb-externalpolicies-example-com.id}" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "https-elb-to-master" { + type = "ingress" + security_group_id = "${aws_security_group.masters-externalpolicies-example-com.id}" + source_security_group_id = "${aws_security_group.api-elb-externalpolicies-example-com.id}" + from_port = 443 + to_port = 443 + protocol = "tcp" +} + +resource "aws_security_group_rule" "icmp-pmtu-api-elb-0-0-0-0--0" { + type = "ingress" + security_group_id = "${aws_security_group.api-elb-externalpolicies-example-com.id}" + from_port = 3 + to_port = 4 + protocol = "icmp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "master-egress" { + type = "egress" + security_group_id = "${aws_security_group.masters-externalpolicies-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "node-egress" { + type = "egress" + security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "node-to-master-tcp-1-2379" { + type = "ingress" + security_group_id = "${aws_security_group.masters-externalpolicies-example-com.id}" + source_security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 1 + to_port = 2379 + protocol = "tcp" +} + +resource "aws_security_group_rule" "node-to-master-tcp-2382-4000" { + type = "ingress" + security_group_id = "${aws_security_group.masters-externalpolicies-example-com.id}" + source_security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 2382 + to_port = 4000 + protocol = "tcp" +} + +resource "aws_security_group_rule" "node-to-master-tcp-4003-65535" { + type = "ingress" + security_group_id = "${aws_security_group.masters-externalpolicies-example-com.id}" + source_security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 4003 + to_port = 65535 + protocol = "tcp" +} + +resource "aws_security_group_rule" "node-to-master-udp-1-65535" { + type = "ingress" + security_group_id = "${aws_security_group.masters-externalpolicies-example-com.id}" + source_security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 1 + to_port = 65535 + protocol = "udp" +} + +resource "aws_security_group_rule" "nodeport-tcp-external-to-node-1-2-3-4--32" { + type = "ingress" + security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 28000 + to_port = 32767 + protocol = "tcp" + cidr_blocks = ["1.2.3.4/32"] +} + +resource "aws_security_group_rule" "nodeport-tcp-external-to-node-10-20-30-0--24" { + type = "ingress" + security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 28000 + to_port = 32767 + protocol = "tcp" + cidr_blocks = ["10.20.30.0/24"] +} + +resource "aws_security_group_rule" "nodeport-udp-external-to-node-1-2-3-4--32" { + type = "ingress" + security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 28000 + to_port = 32767 + protocol = "udp" + cidr_blocks = ["1.2.3.4/32"] +} + +resource "aws_security_group_rule" "nodeport-udp-external-to-node-10-20-30-0--24" { + type = "ingress" + security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 28000 + to_port = 32767 + protocol = "udp" + cidr_blocks = ["10.20.30.0/24"] +} + +resource "aws_security_group_rule" "ssh-external-to-master-0-0-0-0--0" { + type = "ingress" + security_group_id = "${aws_security_group.masters-externalpolicies-example-com.id}" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "ssh-external-to-node-0-0-0-0--0" { + type = "ingress" + security_group_id = "${aws_security_group.nodes-externalpolicies-example-com.id}" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_subnet" "us-test-1a-externalpolicies-example-com" { + vpc_id = "${aws_vpc.externalpolicies-example-com.id}" + cidr_block = "172.20.32.0/19" + availability_zone = "us-test-1a" + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "us-test-1a.externalpolicies.example.com" + SubnetType = "Public" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + "kubernetes.io/role/elb" = "1" + } +} + +resource "aws_vpc" "externalpolicies-example-com" { + cidr_block = "172.20.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "externalpolicies.example.com" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + } +} + +resource "aws_vpc_dhcp_options" "externalpolicies-example-com" { + domain_name = "us-test-1.compute.internal" + domain_name_servers = ["AmazonProvidedDNS"] + + tags = { + KubernetesCluster = "externalpolicies.example.com" + Name = "externalpolicies.example.com" + "kubernetes.io/cluster/externalpolicies.example.com" = "owned" + } +} + +resource "aws_vpc_dhcp_options_association" "externalpolicies-example-com" { + vpc_id = "${aws_vpc.externalpolicies-example-com.id}" + dhcp_options_id = "${aws_vpc_dhcp_options.externalpolicies-example-com.id}" +} + +terraform = { + required_version = ">= 0.9.3" +} diff --git a/upup/pkg/fi/cloudup/awstasks/iamrolepolicy.go b/upup/pkg/fi/cloudup/awstasks/iamrolepolicy.go index b4e78e9549..bcc6bb783b 100644 --- a/upup/pkg/fi/cloudup/awstasks/iamrolepolicy.go +++ b/upup/pkg/fi/cloudup/awstasks/iamrolepolicy.go @@ -18,6 +18,7 @@ package awstasks import ( "fmt" + "hash/fnv" "encoding/json" "net/url" @@ -44,11 +45,49 @@ type IAMRolePolicy struct { // The PolicyDocument to create as an inline policy. // If the PolicyDocument is empty, the policy will be removed. PolicyDocument fi.Resource + // External (non-kops managed) AWS policies to attach to the role + ExternalPolicies *[]string + // Managed tracks the use of ExternalPolicies + Managed bool } func (e *IAMRolePolicy) Find(c *fi.Context) (*IAMRolePolicy, error) { + var actual IAMRolePolicy + cloud := c.Cloud.(awsup.AWSCloud) + // Handle policy overrides + if e.ExternalPolicies != nil { + request := &iam.ListAttachedRolePoliciesInput{ + RoleName: e.Role.Name, + } + + response, err := cloud.IAM().ListAttachedRolePolicies(request) + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() == "NoSuchEntity" { + return nil, nil + } + + return nil, fmt.Errorf("error getting policies for role: %v", err) + } + + var policies []string + if response != nil && len(response.AttachedPolicies) > 0 { + for _, policy := range response.AttachedPolicies { + policies = append(policies, aws.StringValue(policy.PolicyArn)) + } + } + + actual.ID = e.ID + actual.Name = e.Name + actual.Lifecycle = e.Lifecycle + actual.Role = e.Role + actual.Managed = true + actual.ExternalPolicies = &policies + + return &actual, nil + } + request := &iam.GetRolePolicyInput{ RoleName: e.Role.Name, PolicyName: e.Name, @@ -65,7 +104,6 @@ func (e *IAMRolePolicy) Find(c *fi.Context) (*IAMRolePolicy, error) { } p := response - actual := &IAMRolePolicy{} actual.Role = &IAMRole{Name: p.RoleName} if aws.StringValue(e.Role.Name) == aws.StringValue(p.RoleName) { actual.Role.ID = e.Role.ID @@ -87,7 +125,7 @@ func (e *IAMRolePolicy) Find(c *fi.Context) (*IAMRolePolicy, error) { // Avoid spurious changes actual.Lifecycle = e.Lifecycle - return actual, nil + return &actual, nil } func (e *IAMRolePolicy) Run(c *fi.Context) error { @@ -109,9 +147,10 @@ func (_ *IAMRolePolicy) ShouldCreate(a, e, changes *IAMRolePolicy) (bool, error) return false, fmt.Errorf("error rendering PolicyDocument: %v", err) } - if a == nil && ePolicy == "" { + if a == nil && ePolicy == "" && e.ExternalPolicies == nil { return false, nil } + return true, nil } @@ -121,6 +160,55 @@ func (_ *IAMRolePolicy) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *IAMRoleP return fmt.Errorf("error rendering PolicyDocument: %v", err) } + // Handles the full lifecycle of Policy Overrides + if e.Managed { + // Attach policies that are not already attached + AttachPolicies: + for _, policy := range *e.ExternalPolicies { + for _, cloudPolicy := range *a.ExternalPolicies { + if cloudPolicy == policy { + continue AttachPolicies + } + } + + request := &iam.AttachRolePolicyInput{ + RoleName: e.Role.Name, + PolicyArn: s(policy), + } + + _, err = t.Cloud.IAM().AttachRolePolicy(request) + if err != nil { + return fmt.Errorf("error attaching IAMRolePolicy: %v", err) + } + } + + // Clean up unused cloud policies + CheckPolicies: + for _, cloudPolicy := range *a.ExternalPolicies { + for _, policy := range *e.ExternalPolicies { + if policy == cloudPolicy { + continue CheckPolicies + } + } + + klog.V(2).Infof("Detaching unused IAMRolePolicy %s/%s", aws.StringValue(e.Role.Name), cloudPolicy) + + // Detach policy + request := &iam.DetachRolePolicyInput{ + RoleName: e.Role.Name, + PolicyArn: s(cloudPolicy), + } + + _, err := t.Cloud.IAM().DetachRolePolicy(request) + if err != nil { + klog.V(2).Infof("Unable to detach IAMRolePolicy %s/%s", aws.StringValue(e.Role.Name), cloudPolicy) + return err + } + } + + return nil + } + if policy == "" { // A deletion @@ -192,12 +280,33 @@ func (e *IAMRolePolicy) policyDocumentString() (string, error) { } type terraformIAMRolePolicy struct { - Name *string `json:"name"` + Name *string `json:"name,omitempty"` Role *terraform.Literal `json:"role"` - PolicyDocument *terraform.Literal `json:"policy"` + PolicyDocument *terraform.Literal `json:"policy,omitempty"` + PolicyArn *string `json:"policy_arn,omitempty"` } func (_ *IAMRolePolicy) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *IAMRolePolicy) error { + if e.ExternalPolicies != nil && len(*e.ExternalPolicies) > 0 { + for _, policy := range *e.ExternalPolicies { + // create a hash of the arn + h := fnv.New32a() + h.Write([]byte(policy)) + + name := fmt.Sprintf("%s-%d", *e.Name, h.Sum32()) + + tf := &terraformIAMRolePolicy{ + Role: e.Role.TerraformLink(), + PolicyArn: s(policy), + } + + err := t.RenderResource("aws_iam_role_policy_attachment", name, tf) + if err != nil { + return fmt.Errorf("error rendering RolePolicyAttachment: %v", err) + } + } + } + policyString, err := e.policyDocumentString() if err != nil { return fmt.Errorf("error rendering PolicyDocument: %v", err) @@ -233,6 +342,12 @@ type cloudformationIAMRolePolicy struct { } func (_ *IAMRolePolicy) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *IAMRolePolicy) error { + // Currently CloudFormation does not have a reciprocal function to Terraform that allows the modification of a role + // after the fact. In order to make this feature complete we would have to intercept the role task and modify it. + if e.ExternalPolicies != nil && len(*e.ExternalPolicies) > 0 { + return fmt.Errorf("CloudFormation not supported for use with ExternalPolicies.") + } + policyString, err := e.policyDocumentString() if err != nil { return fmt.Errorf("error rendering PolicyDocument: %v", err)