Skip to content

Commit cd9e2b1

Browse files
authored
Add member function to cluster holding the trust relationship (#866)
* add member function to cluster holding the trust relationship Signed-off-by: Martin Linkhorst <[email protected]> * avoid error-returning template functions by calculating values upfront Signed-off-by: Martin Linkhorst <[email protected]> * exclude trust relationships for non-ready clusters Signed-off-by: Martin Linkhorst <[email protected]> --------- Signed-off-by: Martin Linkhorst <[email protected]>
1 parent cef46af commit cd9e2b1

File tree

8 files changed

+288
-7
lines changed

8 files changed

+288
-7
lines changed

api/cluster.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import (
55
"crypto/sha1"
66
"encoding/base64"
77
"encoding/binary"
8+
"encoding/json"
89
"fmt"
910
"sort"
1011
"strings"
1112

1213
"github.com/zalando-incubator/cluster-lifecycle-manager/channel"
14+
"github.com/zalando-incubator/cluster-lifecycle-manager/pkg/cluster-registry/models"
15+
"github.com/zalando-incubator/cluster-lifecycle-manager/pkg/util"
1316
)
1417

1518
// A provider ID is a string that identifies a cluster provider.
@@ -42,6 +45,43 @@ type Cluster struct {
4245
Status *ClusterStatus `json:"status" yaml:"status"`
4346
Owner string `json:"owner" yaml:"owner"`
4447
AccountName string `json:"account_name" yaml:"account_name"`
48+
// Local fields to hold information about the OIDC provider.
49+
AccountClusters []*Cluster
50+
OIDCProvider string
51+
IAMRoleTrustRelationshipTemplate string
52+
}
53+
54+
type AssumeRolePolicyDocument struct {
55+
Version string
56+
Statement []Statement
57+
}
58+
59+
type Statement struct {
60+
Effect string
61+
Principal map[string]string
62+
Action string
63+
Condition map[string]map[string]string `json:",omitempty"`
64+
}
65+
66+
func (cluster *Cluster) InitOIDCProvider() error {
67+
if cluster.Provider == ZalandoEKSProvider {
68+
cluster.OIDCProvider = strings.TrimPrefix(cluster.ConfigItems["eks_oidc_issuer_url"], "https://")
69+
} else {
70+
hostedZone, err := util.GetHostedZone(cluster.APIServerURL)
71+
if err != nil {
72+
return fmt.Errorf("error while getting trust relationship for %s: %v", cluster.Alias, err)
73+
}
74+
cluster.OIDCProvider = fmt.Sprintf("%s.%s", cluster.LocalID, hostedZone)
75+
}
76+
77+
trustRelationship := trustRelationship(cluster.AccountClusters)
78+
trustRelationshipJSON, err := json.Marshal(trustRelationship)
79+
if err != nil {
80+
return fmt.Errorf("error while marshalling trust relationship for %s: %v", cluster.Alias, err)
81+
}
82+
83+
cluster.IAMRoleTrustRelationshipTemplate = string(trustRelationshipJSON)
84+
return nil
4585
}
4686

4787
// Version returns the version derived from a sha1 hash of the cluster struct
@@ -210,3 +250,66 @@ func (cluster Cluster) Name() string {
210250
}
211251
return cluster.ID
212252
}
253+
254+
func (cluster Cluster) InfrastructureAccountID() string {
255+
return strings.TrimPrefix(cluster.InfrastructureAccount, "aws:")
256+
}
257+
258+
func (cluster Cluster) WorkerRoleARN() string {
259+
return fmt.Sprintf("arn:aws:iam::%s:role/%s-worker", cluster.InfrastructureAccountID(), cluster.LocalID)
260+
}
261+
262+
func (cluster Cluster) OIDCProviderARN() string {
263+
return fmt.Sprintf("arn:aws:iam::%s:oidc-provider/%s", cluster.InfrastructureAccountID(), cluster.OIDCProvider)
264+
}
265+
266+
func (cluster Cluster) OIDCSubjectKey() string {
267+
return fmt.Sprintf("%s:sub", cluster.OIDCProvider)
268+
}
269+
270+
func trustRelationship(clusters []*Cluster) AssumeRolePolicyDocument {
271+
policyDocument := AssumeRolePolicyDocument{
272+
Version: "2012-10-17",
273+
Statement: []Statement{
274+
{
275+
Effect: "Allow",
276+
Principal: map[string]string{
277+
"Service": "ec2.amazonaws.com",
278+
},
279+
Action: "sts:AssumeRole",
280+
},
281+
},
282+
}
283+
284+
for _, cluster := range clusters {
285+
if cluster.LifecycleStatus == models.ClusterLifecycleStatusReady {
286+
policyDocument.Statement = append(policyDocument.Statement, policyStatements(cluster.WorkerRoleARN(), cluster.OIDCProviderARN(), cluster.OIDCSubjectKey())...)
287+
}
288+
}
289+
290+
return policyDocument
291+
}
292+
293+
func policyStatements(workerRole string, identityProvider string, subjectKey string) []Statement {
294+
return []Statement{
295+
{
296+
Effect: "Allow",
297+
Principal: map[string]string{
298+
"AWS": workerRole,
299+
},
300+
Action: "sts:AssumeRole",
301+
},
302+
{
303+
Effect: "Allow",
304+
Principal: map[string]string{
305+
"Federated": identityProvider,
306+
},
307+
Action: "sts:AssumeRoleWithWebIdentity",
308+
Condition: map[string]map[string]string{
309+
"StringLike": {
310+
subjectKey: "system:serviceaccount:${SERVICE_ACCOUNT}",
311+
},
312+
},
313+
},
314+
}
315+
}

api/cluster_test.go

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import (
88
"testing"
99

1010
"github.com/sirupsen/logrus"
11+
"github.com/stretchr/testify/assert"
1112
"github.com/stretchr/testify/require"
1213
"github.com/zalando-incubator/cluster-lifecycle-manager/channel"
14+
"github.com/zalando-incubator/cluster-lifecycle-manager/pkg/cluster-registry/models"
1315
)
1416

1517
func fieldNames(value interface{}) ([]string, error) {
@@ -83,7 +85,8 @@ func TestVersion(t *testing.T) {
8385
require.NoError(t, err)
8486

8587
for _, field := range fields {
86-
if field == "Alias" || field == "NodePools" || field == "Owner" || field == "AccountName" || field == "Status" {
88+
switch field {
89+
case "Alias", "NodePools", "Owner", "AccountName", "AccountClusters", "OIDCProvider", "IAMRoleTrustRelationshipTemplate", "Status":
8790
continue
8891
}
8992

@@ -134,3 +137,111 @@ func TestName(t *testing.T) {
134137

135138
require.Equal(t, cluster.LocalID, cluster.Name())
136139
}
140+
141+
func TestInfrastructureAccountID(t *testing.T) {
142+
cluster := &Cluster{InfrastructureAccount: "aws:123456789012"}
143+
assert.Equal(t, "123456789012", cluster.InfrastructureAccountID())
144+
}
145+
func TestWorkerRoleARN(t *testing.T) {
146+
cluster := &Cluster{InfrastructureAccount: "aws:123456789012", LocalID: "kube-1"}
147+
assert.Equal(t, "arn:aws:iam::123456789012:role/kube-1-worker", cluster.WorkerRoleARN())
148+
}
149+
150+
func TestOIDCProvider(t *testing.T) {
151+
cluster := &Cluster{
152+
Provider: ZalandoAWSProvider,
153+
LocalID: "kube-1",
154+
APIServerURL: "https://kube-1.example.zalan.do",
155+
}
156+
err := cluster.InitOIDCProvider()
157+
require.NoError(t, err)
158+
159+
assert.Equal(t, "kube-1.example.zalan.do", cluster.OIDCProvider)
160+
161+
cluster = &Cluster{
162+
Provider: ZalandoEKSProvider,
163+
ConfigItems: map[string]string{
164+
"eks_oidc_issuer_url": "https://oidc.eks.eu-central-1.amazonaws.com/id/11112222333344445555666677778888",
165+
},
166+
}
167+
err = cluster.InitOIDCProvider()
168+
require.NoError(t, err)
169+
170+
assert.Equal(t, "oidc.eks.eu-central-1.amazonaws.com/id/11112222333344445555666677778888", cluster.OIDCProvider)
171+
}
172+
173+
func TestOIDCProviderARN(t *testing.T) {
174+
cluster := &Cluster{
175+
InfrastructureAccount: "aws:123456789012",
176+
OIDCProvider: "kube-1.example.zalan.do",
177+
}
178+
assert.Equal(t, "arn:aws:iam::123456789012:oidc-provider/kube-1.example.zalan.do", cluster.OIDCProviderARN())
179+
}
180+
181+
func TestOIDCSubjectKey(t *testing.T) {
182+
cluster := &Cluster{OIDCProvider: "kube-1.example.zalan.do"}
183+
assert.Equal(t, "kube-1.example.zalan.do:sub", cluster.OIDCSubjectKey())
184+
}
185+
186+
func TestIAMRoleTrustRelationshipTemplate(t *testing.T) {
187+
legacyCluster := &Cluster{
188+
Provider: ZalandoAWSProvider,
189+
LocalID: "kube-1",
190+
InfrastructureAccount: "aws:123456789012",
191+
APIServerURL: "https://kube-1.example.zalan.do",
192+
LifecycleStatus: models.ClusterLifecycleStatusReady,
193+
}
194+
legacyCluster.AccountClusters = []*Cluster{legacyCluster}
195+
err := legacyCluster.InitOIDCProvider()
196+
require.NoError(t, err)
197+
198+
legacyTrustRelationship := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"},{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:role/kube-1-worker"},"Action":"sts:AssumeRole"},{"Effect":"Allow","Principal":{"Federated":"arn:aws:iam::123456789012:oidc-provider/kube-1.example.zalan.do"},"Action":"sts:AssumeRoleWithWebIdentity","Condition":{"StringLike":{"kube-1.example.zalan.do:sub":"system:serviceaccount:${SERVICE_ACCOUNT}"}}}]}`
199+
assert.Equal(t, legacyTrustRelationship, legacyCluster.IAMRoleTrustRelationshipTemplate)
200+
201+
eksCluster := &Cluster{
202+
Provider: ZalandoEKSProvider,
203+
LocalID: "teapot-euc1",
204+
InfrastructureAccount: "aws:123456789012",
205+
ConfigItems: map[string]string{
206+
"eks_oidc_issuer_url": "https://oidc.eks.eu-central-1.amazonaws.com/id/11112222333344445555666677778888",
207+
},
208+
LifecycleStatus: models.ClusterLifecycleStatusReady,
209+
}
210+
eksCluster.AccountClusters = []*Cluster{eksCluster}
211+
err = eksCluster.InitOIDCProvider()
212+
require.NoError(t, err)
213+
214+
eksTrustRelationship := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"},{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:role/teapot-euc1-worker"},"Action":"sts:AssumeRole"},{"Effect":"Allow","Principal":{"Federated":"arn:aws:iam::123456789012:oidc-provider/oidc.eks.eu-central-1.amazonaws.com/id/11112222333344445555666677778888"},"Action":"sts:AssumeRoleWithWebIdentity","Condition":{"StringLike":{"oidc.eks.eu-central-1.amazonaws.com/id/11112222333344445555666677778888:sub":"system:serviceaccount:${SERVICE_ACCOUNT}"}}}]}`
215+
assert.Equal(t, eksTrustRelationship, eksCluster.IAMRoleTrustRelationshipTemplate)
216+
217+
combinedAccountClusters := []*Cluster{legacyCluster, eksCluster}
218+
combinedTrustRelationship := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"},{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:role/kube-1-worker"},"Action":"sts:AssumeRole"},{"Effect":"Allow","Principal":{"Federated":"arn:aws:iam::123456789012:oidc-provider/kube-1.example.zalan.do"},"Action":"sts:AssumeRoleWithWebIdentity","Condition":{"StringLike":{"kube-1.example.zalan.do:sub":"system:serviceaccount:${SERVICE_ACCOUNT}"}}},{"Effect":"Allow","Principal":{"AWS":"arn:aws:iam::123456789012:role/teapot-euc1-worker"},"Action":"sts:AssumeRole"},{"Effect":"Allow","Principal":{"Federated":"arn:aws:iam::123456789012:oidc-provider/oidc.eks.eu-central-1.amazonaws.com/id/11112222333344445555666677778888"},"Action":"sts:AssumeRoleWithWebIdentity","Condition":{"StringLike":{"oidc.eks.eu-central-1.amazonaws.com/id/11112222333344445555666677778888:sub":"system:serviceaccount:${SERVICE_ACCOUNT}"}}}]}`
219+
220+
legacyCluster.AccountClusters = combinedAccountClusters
221+
err = legacyCluster.InitOIDCProvider()
222+
require.NoError(t, err)
223+
224+
assert.Equal(t, combinedTrustRelationship, legacyCluster.IAMRoleTrustRelationshipTemplate)
225+
226+
eksCluster.AccountClusters = combinedAccountClusters
227+
err = eksCluster.InitOIDCProvider()
228+
require.NoError(t, err)
229+
230+
assert.Equal(t, combinedTrustRelationship, eksCluster.IAMRoleTrustRelationshipTemplate)
231+
232+
withDecommissionedClusters := append(combinedAccountClusters, &Cluster{
233+
LifecycleStatus: models.ClusterLifecycleStatusDecommissioned,
234+
})
235+
236+
legacyCluster.AccountClusters = withDecommissionedClusters
237+
err = legacyCluster.InitOIDCProvider()
238+
require.NoError(t, err)
239+
240+
assert.Equal(t, combinedTrustRelationship, legacyCluster.IAMRoleTrustRelationshipTemplate)
241+
242+
eksCluster.AccountClusters = withDecommissionedClusters
243+
err = eksCluster.InitOIDCProvider()
244+
require.NoError(t, err)
245+
246+
assert.Equal(t, combinedTrustRelationship, eksCluster.IAMRoleTrustRelationshipTemplate)
247+
}

provisioner/hack.go renamed to pkg/util/hosted_zone.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
1-
package provisioner
1+
package util
22

33
import (
44
"fmt"
55
"net/url"
66
"strings"
77
)
88

9-
// getHostedZone gets derrive hosted zone from an APIServerURL.
10-
func getHostedZone(APIServerURL string) (string, error) {
11-
url, err := url.Parse(APIServerURL)
9+
// GetHostedZone gets the hosted zone from an APIServerURL.
10+
func GetHostedZone(apiServerURL string) (string, error) {
11+
url, err := url.Parse(apiServerURL)
1212
if err != nil {
1313
return "", err
1414
}
1515

1616
split := strings.Split(url.Host, ".")
1717
if len(split) < 2 {
18-
return "", fmt.Errorf("can't derive hosted zone from URL %s", APIServerURL)
18+
return "", fmt.Errorf("can't derive hosted zone from URL %s", apiServerURL)
1919
}
2020

2121
return strings.Join(split[1:], "."), nil

provisioner/clusterpy.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/zalando-incubator/cluster-lifecycle-manager/pkg/decrypter"
2525
"github.com/zalando-incubator/cluster-lifecycle-manager/pkg/kubernetes"
2626
"github.com/zalando-incubator/cluster-lifecycle-manager/pkg/updatestrategy"
27+
"github.com/zalando-incubator/cluster-lifecycle-manager/pkg/util"
2728
"github.com/zalando-incubator/cluster-lifecycle-manager/pkg/util/command"
2829
"github.com/zalando-incubator/kube-ingress-aws-controller/certs"
2930
"golang.org/x/oauth2"
@@ -246,7 +247,7 @@ func (p *clusterpyProvisioner) provision(
246247
}
247248

248249
// TODO: should this be done like this or via a config item?
249-
hostedZone, err := getHostedZone(cluster.APIServerURL)
250+
hostedZone, err := util.GetHostedZone(cluster.APIServerURL)
250251
if err != nil {
251252
return err
252253
}
@@ -339,6 +340,10 @@ func (p *clusterpyProvisioner) provision(
339340
}
340341
}
341342

343+
if err := cluster.InitOIDCProvider(); err != nil {
344+
return err
345+
}
346+
342347
// TODO: having it this late means late feedback on invalid manifests
343348
manifests, err := renderManifests(
344349
channelConfig,

provisioner/template_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,3 +1623,42 @@ func TestClusterName(t *testing.T) {
16231623
})
16241624
}
16251625
}
1626+
1627+
func TestClusterOIDCProvider(t *testing.T) {
1628+
for _, tc := range []struct {
1629+
name string
1630+
cluster api.Cluster
1631+
input string
1632+
expected string
1633+
}{
1634+
{
1635+
name: "zalando-aws cluster",
1636+
cluster: api.Cluster{
1637+
Provider: api.ZalandoAWSProvider,
1638+
LocalID: "kube-1",
1639+
APIServerURL: "https://kube-1.example.zalan.do",
1640+
},
1641+
expected: "kube-1.example.zalan.do",
1642+
input: `{{ .Values.data.cluster.OIDCProvider }}`,
1643+
},
1644+
{
1645+
name: "zalando-eks cluster",
1646+
cluster: api.Cluster{
1647+
Provider: api.ZalandoEKSProvider,
1648+
ConfigItems: map[string]string{
1649+
"eks_oidc_issuer_url": "https://oidc.eks.eu-central-1.amazonaws.com/id/11112222333344445555666677778888",
1650+
},
1651+
},
1652+
expected: "oidc.eks.eu-central-1.amazonaws.com/id/11112222333344445555666677778888",
1653+
input: `{{ .Values.data.cluster.OIDCProvider }}`,
1654+
},
1655+
} {
1656+
t.Run(tc.name, func(t *testing.T) {
1657+
err := tc.cluster.InitOIDCProvider()
1658+
require.NoError(t, err)
1659+
result, err := renderSingle(t, tc.input, map[string]interface{}{"cluster": tc.cluster})
1660+
require.NoError(t, err)
1661+
require.EqualValues(t, tc.expected, result)
1662+
})
1663+
}
1664+
}

registry/file.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ func (r *fileRegistry) ListClusters(_ Filter) ([]*api.Cluster, error) {
3838
return nil, err
3939
}
4040

41+
clustersByAccount := map[string][]*api.Cluster{}
42+
4143
for _, cluster := range fileClusters.Clusters {
4244
for _, nodePool := range cluster.NodePools {
4345
if nodePool.Profile == "worker-karpenter" && len(nodePool.InstanceTypes) == 0 {
@@ -49,6 +51,13 @@ func (r *fileRegistry) ListClusters(_ Filter) ([]*api.Cluster, error) {
4951
}
5052
nodePool.InstanceType = nodePool.InstanceTypes[0]
5153
}
54+
clustersByAccount[cluster.InfrastructureAccount] = append(clustersByAccount[cluster.InfrastructureAccount], cluster)
55+
}
56+
for _, cluster := range fileClusters.Clusters {
57+
cluster.AccountClusters = clustersByAccount[cluster.InfrastructureAccount]
58+
if err := cluster.InitOIDCProvider(); err != nil {
59+
return nil, err
60+
}
5261
}
5362

5463
return fileClusters.Clusters, nil

0 commit comments

Comments
 (0)