Skip to content

Commit dbd2347

Browse files
author
Ole Markus With
committed
Add irsa support for awslbcontroller
This commit also introduces support for adding token projection volumes for well-known SAs. Slightly less complicated than explicitly parsing the objects for a manifest
1 parent 51adb83 commit dbd2347

File tree

15 files changed

+365
-99
lines changed

15 files changed

+365
-99
lines changed

cmd/kops/integration_test.go

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"encoding/base64"
2626
"encoding/json"
2727
"encoding/pem"
28+
"fmt"
2829
"io"
2930
"io/ioutil"
3031
"os"
@@ -58,10 +59,10 @@ type integrationTest struct {
5859
private bool
5960
zones int
6061
expectPolicies bool
61-
// expectServiceAccountRoles is true if we expect to assign per-ServiceAccount IAM roles (instead of just using the node roles)
62-
expectServiceAccountRoles bool
63-
lifecycleOverrides []string
64-
sshKey bool
62+
// expectServiceAccountRolePolicies is a list of per-ServiceAccount IAM roles (instead of just using the node roles)
63+
expectServiceAccountRolePolicies []string
64+
lifecycleOverrides []string
65+
sshKey bool
6566
// caKey is true if we should use a provided ca.crt & ca.key as our CA
6667
caKey bool
6768
jsonOutput bool
@@ -120,9 +121,12 @@ func (i *integrationTest) withPrivate() *integrationTest {
120121
return i
121122
}
122123

123-
// withServiceAccountRoles indicates we expect to assign per-ServiceAccount IAM roles (instead of just using the node roles)
124-
func (i *integrationTest) withServiceAccountRoles() *integrationTest {
125-
i.expectServiceAccountRoles = true
124+
// withServiceAccountRoles indicates we expect to assign an IAM role for a ServiceAccount (instead of just using the node roles)
125+
func (i *integrationTest) withServiceAccountRole(sa string, inlinePolicy bool) *integrationTest {
126+
i.expectServiceAccountRolePolicies = append(i.expectServiceAccountRolePolicies, fmt.Sprintf("aws_iam_role_%s.sa.%s_policy", sa, i.clusterName))
127+
if inlinePolicy {
128+
i.expectServiceAccountRolePolicies = append(i.expectServiceAccountRolePolicies, fmt.Sprintf("aws_iam_role_policy_%s.sa.%s_policy", sa, i.clusterName))
129+
}
126130
return i
127131
}
128132

@@ -283,7 +287,11 @@ func TestPublicJWKS(t *testing.T) {
283287
defer unsetFeatureFlags()
284288

285289
// We have to use a fixed CA because the fingerprint is inserted into the AWS WebIdentity configuration.
286-
newIntegrationTest("minimal.example.com", "public-jwks").withCAKey().withServiceAccountRoles().runTestTerraformAWS(t)
290+
newIntegrationTest("minimal.example.com", "public-jwks").
291+
withCAKey().
292+
withServiceAccountRole("dns-controller.kube-system", true).
293+
runTestTerraformAWS(t)
294+
287295
}
288296

289297
// TestAWSLBController runs a simple configuration, but with AWS LB controller, UseServiceAccountIAM and PublicJWKS enabled
@@ -295,7 +303,11 @@ func TestAWSLBController(t *testing.T) {
295303
defer unsetFeatureFlags()
296304

297305
// We have to use a fixed CA because the fingerprint is inserted into the AWS WebIdentity configuration.
298-
newIntegrationTest("minimal.example.com", "aws-lb-controller").withCAKey().withServiceAccountRoles().runTestTerraformAWS(t)
306+
newIntegrationTest("minimal.example.com", "aws-lb-controller").
307+
withCAKey().
308+
withServiceAccountRole("dns-controller.kube-system", true).
309+
withServiceAccountRole("aws-load-balancer-controller.kube-system", true).
310+
runTestTerraformAWS(t)
299311
}
300312

301313
// TestSharedSubnet runs the test on a configuration with a shared subnet (and VPC)
@@ -568,12 +580,9 @@ func (i *integrationTest) runTestTerraformAWS(t *testing.T) {
568580
}
569581
}
570582
}
571-
if i.expectServiceAccountRoles {
572-
expectedFilenames = append(expectedFilenames, []string{
573-
"aws_iam_role_dns-controller.kube-system.sa." + i.clusterName + "_policy",
574-
"aws_iam_role_policy_dns-controller.kube-system.sa." + i.clusterName + "_policy",
575-
}...)
576-
}
583+
584+
expectedFilenames = append(expectedFilenames, i.expectServiceAccountRolePolicies...)
585+
577586
i.runTest(t, h, expectedFilenames, tfFileName, tfFileName, nil)
578587
}
579588

pkg/model/components/addonmanifests/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/model/components/addonmanifests/awsloadbalancercontroller/BUILD.bazel

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package awsloadbalancercontroller
18+
19+
import (
20+
"k8s.io/apimachinery/pkg/types"
21+
"k8s.io/kops/pkg/model/iam"
22+
"k8s.io/kops/pkg/util/stringorslice"
23+
)
24+
25+
// ServiceAccount represents the service-account used by the dns-controller.
26+
// It implements iam.Subject to get AWS IAM permissions.
27+
type ServiceAccount struct {
28+
}
29+
30+
var _ iam.Subject = &ServiceAccount{}
31+
32+
// BuildAWSPolicy generates a custom policy for a ServiceAccount IAM role.
33+
func (r *ServiceAccount) BuildAWSPolicy(b *iam.PolicyBuilder) (*iam.Policy, error) {
34+
p := &iam.Policy{
35+
Version: iam.PolicyDefaultVersion,
36+
}
37+
38+
resource := stringorslice.Slice([]string{"*"})
39+
iam.AddAWSLoadbalancerControllerPermissions(p, resource, b.Cluster.ObjectMeta.Name)
40+
41+
return p, nil
42+
}
43+
44+
// ServiceAccount returns the kubernetes service account used.
45+
func (r *ServiceAccount) ServiceAccount() (types.NamespacedName, bool) {
46+
return types.NamespacedName{
47+
Namespace: "kube-system",
48+
Name: "aws-load-balancer-controller",
49+
}, true
50+
}

pkg/model/components/addonmanifests/remap.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ package addonmanifests
1919
import (
2020
"fmt"
2121

22+
corev1 "k8s.io/api/core/v1"
2223
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
2325
"k8s.io/klog/v2"
2426
addonsapi "k8s.io/kops/channels/pkg/api"
2527
"k8s.io/kops/pkg/assets"
2628
"k8s.io/kops/pkg/kubemanifest"
2729
"k8s.io/kops/pkg/model"
30+
"k8s.io/kops/pkg/model/components/addonmanifests/awsloadbalancercontroller"
2831
"k8s.io/kops/pkg/model/components/addonmanifests/dnscontroller"
32+
"k8s.io/kops/pkg/model/iam"
2933
"k8s.io/kops/upup/pkg/fi"
3034
)
3135

@@ -49,6 +53,11 @@ func RemapAddonManifest(addon *addonsapi.AddonSpec, context *model.KopsModelCont
4953
return nil, fmt.Errorf("failed to annotate %q: %w", name, err)
5054
}
5155

56+
err = addServiceAccountRole(context, objects)
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to add service account for %q: %w", name, err)
59+
}
60+
5261
b, err := objects.ToYAML()
5362
if err != nil {
5463
return nil, err
@@ -68,6 +77,48 @@ func RemapAddonManifest(addon *addonsapi.AddonSpec, context *model.KopsModelCont
6877
return manifest, nil
6978
}
7079

80+
func addServiceAccountRole(context *model.KopsModelContext, objects kubemanifest.ObjectList) error {
81+
for _, object := range objects {
82+
if object.Kind() != "Deployment" {
83+
continue
84+
}
85+
if object.APIVersion() != "apps/v1" {
86+
continue
87+
}
88+
podSpec := &corev1.PodSpec{}
89+
90+
if err := object.Reparse(podSpec, "spec", "template", "spec"); err != nil {
91+
return fmt.Errorf("failed to parse spec.template.spec from Deployment: %v", err)
92+
}
93+
containers := podSpec.Containers
94+
sa := podSpec.ServiceAccountName
95+
subject := getWellknownServiceAccount(sa)
96+
if subject == nil {
97+
continue
98+
}
99+
for k, container := range containers {
100+
if err := iam.AddServiceAccountRole(&context.IAMModelContext, podSpec, &container, subject); err != nil {
101+
return err
102+
}
103+
containers[k] = container
104+
}
105+
if err := object.Set(podSpec, "spec", "template", "spec"); err != nil {
106+
return fmt.Errorf("failed to set object: %w", err)
107+
}
108+
109+
}
110+
return nil
111+
}
112+
113+
func getWellknownServiceAccount(name string) iam.Subject {
114+
switch name {
115+
case "aws-load-balancer-controller":
116+
return &awsloadbalancercontroller.ServiceAccount{}
117+
default:
118+
return nil
119+
}
120+
}
121+
71122
func addLabels(addon *addonsapi.AddonSpec, objects kubemanifest.ObjectList) error {
72123

73124
for _, object := range objects {

pkg/model/iam/iam_builder.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,6 @@ func (r *NodeRoleAPIServer) BuildAWSPolicy(b *PolicyBuilder) (*Policy, error) {
259259
if b.Cluster.Spec.IAM.Legacy {
260260
addLegacyDNSControllerPermissions(b, p)
261261
}
262-
AddDNSControllerPermissions(b, p)
263262

264263
if b.Cluster.Spec.IAM.Legacy || b.Cluster.Spec.IAM.AllowContainerRegistry {
265264
addECRPermissions(p)
@@ -311,6 +310,10 @@ func (r *NodeRoleMaster) BuildAWSPolicy(b *PolicyBuilder) (*Policy, error) {
311310
addLegacyDNSControllerPermissions(b, p)
312311
}
313312
AddDNSControllerPermissions(b, p)
313+
314+
if b.Cluster.Spec.AWSLoadBalancerController != nil && fi.BoolValue(b.Cluster.Spec.AWSLoadBalancerController.Enabled) {
315+
AddAWSLoadbalancerControllerPermissions(p, resource, b.Cluster.GetName())
316+
}
314317
}
315318

316319
if b.Cluster.Spec.IAM.Legacy || b.Cluster.Spec.IAM.AllowContainerRegistry {
@@ -333,10 +336,6 @@ func (r *NodeRoleMaster) BuildAWSPolicy(b *PolicyBuilder) (*Policy, error) {
333336
addCalicoSrcDstCheckPermissions(p)
334337
}
335338

336-
if b.Cluster.Spec.AWSLoadBalancerController != nil && fi.BoolValue(b.Cluster.Spec.AWSLoadBalancerController.Enabled) {
337-
addAWSLoadbalancerControllerPermissions(p, b.Cluster.GetName())
338-
}
339-
340339
return p, nil
341340
}
342341

@@ -722,15 +721,18 @@ func addCalicoSrcDstCheckPermissions(p *Policy) {
722721
})
723722
}
724723

725-
func addAWSLoadbalancerControllerPermissions(p *Policy, clusterName string) {
724+
// AddAWSLoadbalancerControllerPermissions adds the permissions needed for the aws load balancer controller to the givnen policy
725+
func AddAWSLoadbalancerControllerPermissions(p *Policy, resource stringorslice.StringOrSlice, clusterName string) {
726+
addMasterEC2Policies(p, resource, false, clusterName)
727+
addMasterELBPolicies(p, resource, false)
726728
p.Statement = append(p.Statement, &Statement{
727729
Effect: StatementEffectAllow,
728730
Action: stringorslice.Of(
729731
"ec2:AuthorizeSecurityGroupIngress", // aws.go
730732
"ec2:DeleteSecurityGroup", // aws.go
731733
"ec2:RevokeSecurityGroupIngress", // aws.go
732734
),
733-
Resource: stringorslice.Slice([]string{"*"}),
735+
Resource: resource,
734736
Condition: Condition{
735737
"StringEquals": map[string]string{
736738
"ec2:ResourceTag/elbv2.k8s.aws/cluster": clusterName,
@@ -748,7 +750,7 @@ func addAWSLoadbalancerControllerPermissions(p *Policy, clusterName string) {
748750
"elasticloadbalancing:DescribeListenerCertificates",
749751
"elasticloadbalancing:CreateRule",
750752
),
751-
Resource: stringorslice.Slice([]string{"*"}),
753+
Resource: resource,
752754
})
753755
}
754756

tests/integration/update_cluster/apiservernodes/cloudformation.json

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,35 +1169,6 @@
11691169
"Resource": [
11701170
"*"
11711171
]
1172-
},
1173-
{
1174-
"Action": [
1175-
"route53:ChangeResourceRecordSets",
1176-
"route53:ListResourceRecordSets",
1177-
"route53:GetHostedZone"
1178-
],
1179-
"Effect": "Allow",
1180-
"Resource": [
1181-
"arn:aws:route53:::hostedzone/Z1AFAKE1ZON3YO"
1182-
]
1183-
},
1184-
{
1185-
"Action": [
1186-
"route53:GetChange"
1187-
],
1188-
"Effect": "Allow",
1189-
"Resource": [
1190-
"arn:aws:route53:::change/*"
1191-
]
1192-
},
1193-
{
1194-
"Action": [
1195-
"route53:ListHostedZones"
1196-
],
1197-
"Effect": "Allow",
1198-
"Resource": [
1199-
"*"
1200-
]
12011172
}
12021173
],
12031174
"Version": "2012-10-17"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"Statement": [
3+
{
4+
"Action": "sts:AssumeRoleWithWebIdentity",
5+
"Condition": {
6+
"StringEquals": {
7+
"discovery.example.com/minimal.example.com/oidc:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller"
8+
}
9+
},
10+
"Effect": "Allow",
11+
"Principal": {
12+
"Federated": "arn:aws:iam::123456789012:oidc-provider/discovery.example.com/minimal.example.com/oidc"
13+
}
14+
}
15+
],
16+
"Version": "2012-10-17"
17+
}

0 commit comments

Comments
 (0)