Skip to content

Commit

Permalink
Merge pull request crossplane-contrib#126 from hasheddan/usesaiam
Browse files Browse the repository at this point in the history
Allow auth using IAM Roles for Service Accounts on EKS
  • Loading branch information
negz authored Feb 17, 2020
2 parents dc2ac11 + 3ab827d commit 2773e55
Show file tree
Hide file tree
Showing 41 changed files with 493 additions and 254 deletions.
8 changes: 8 additions & 0 deletions apis/v1alpha3/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ type ProviderSpec struct {

// Region for managed resources created using this AWS provider.
Region string `json:"region"`

// UseServiceAccount indicates to use an IAM Role associated Kubernetes
// ServiceAccount for authentication instead of a credentials Secret.
// https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html
//
// If set to true, credentialsSecretRef will be ignored.
// +optional
UseServiceAccount *bool `json:"useServiceAccount,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
9 changes: 7 additions & 2 deletions apis/v1alpha3/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions apis/v1alpha3/zz_generated.provider.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion config/crd/aws.crossplane.io_providers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,13 @@ spec:
region:
description: Region for managed resources created using this AWS provider.
type: string
useServiceAccount:
description: "UseServiceAccount indicates to use an IAM Role associated
Kubernetes ServiceAccount for authentication instead of a credentials
Secret. https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html
\n If set to true, credentialsSecretRef will be ignored."
type: boolean
required:
- credentialsSecretRef
- region
type: object
required:
Expand Down
10 changes: 7 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ module github.com/crossplaneio/stack-aws
go 1.13

require (
github.com/aws/aws-sdk-go-v2 v0.5.0
github.com/aws/aws-sdk-go-v2 v0.19.0
github.com/crossplaneio/crossplane v0.7.0-rc.0.20200211212229-e3c5715e39d8
github.com/crossplaneio/crossplane-runtime v0.4.1-0.20200201005410-a6bb086be888
github.com/crossplaneio/crossplane-tools v0.0.0-20191220202319-9033bd8a02ce
github.com/crossplaneio/crossplane-runtime v0.4.1-0.20200213015649-e59980916293
github.com/crossplaneio/crossplane-tools v0.0.0-20200214190114-c7c4365eeb95
github.com/evanphx/json-patch v4.5.0+incompatible
github.com/ghodss/yaml v1.0.0
github.com/go-ini/ini v1.46.0
github.com/google/go-cmp v0.3.1
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/onsi/gomega v1.7.0
github.com/pkg/errors v0.8.1
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf // indirect
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
github.com/stretchr/testify v1.4.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/ini.v1 v1.47.0 // indirect
Expand Down
33 changes: 25 additions & 8 deletions go.sum

Large diffs are not rendered by default.

59 changes: 56 additions & 3 deletions pkg/clients/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@ limitations under the License.
package aws

import (
"context"
"encoding/json"
"io/ioutil"
"os"
"strconv"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/external"
"github.com/aws/aws-sdk-go-v2/service/sts"
jsonpatch "github.com/evanphx/json-patch"
"github.com/go-ini/ini"
"github.com/pkg/errors"
)

// DefaultSection for INI files.
Expand Down Expand Up @@ -71,11 +78,14 @@ func CredentialsIDSecret(data []byte, profile string) (string, string, error) {
return id.Value(), secret.Value(), err
}

// LoadConfig - AWS configuration which can be used to issue requests against AWS API
func LoadConfig(data []byte, profile, region string) (*aws.Config, error) {
// AuthMethod is a method of authenticating to the AWS API
type AuthMethod func(context.Context, []byte, string, string) (*aws.Config, error)

// UseProviderSecret - AWS configuration which can be used to issue requests against AWS API
func UseProviderSecret(_ context.Context, data []byte, profile, region string) (*aws.Config, error) {
id, secret, err := CredentialsIDSecret(data, profile)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "unable to parse credentials")
}

creds := aws.Credentials{
Expand All @@ -92,6 +102,49 @@ func LoadConfig(data []byte, profile, region string) (*aws.Config, error) {
return &config, err
}

// UsePodServiceAccount assumes an IAM role configured via a ServiceAccount.
// https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html
//
// TODO(hasheddan): This should be replaced by the implementation of the Web
// Identity Token Provider in the following PR after merge and subsequent
// release of AWS SDK: /~https://github.com/aws/aws-sdk-go-v2/pull/488
func UsePodServiceAccount(ctx context.Context, _ []byte, _, region string) (*aws.Config, error) {
cfg, err := external.LoadDefaultAWSConfig()
if err != nil {
return nil, errors.Wrap(err, "failed to load default AWS config")
}
cfg.Region = region
svc := sts.New(cfg)

b, err := ioutil.ReadFile(os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE"))
if err != nil {
return nil, errors.Wrap(err, "unable to read web identity token file in pod")
}
token := string(b)
sess := strconv.FormatInt(time.Now().UnixNano(), 10)
role := os.Getenv("AWS_ROLE_ARN")
resp, err := svc.AssumeRoleWithWebIdentityRequest(
&sts.AssumeRoleWithWebIdentityInput{
RoleSessionName: &sess,
WebIdentityToken: &token,
RoleArn: &role,
}).Send(ctx)
if err != nil {
return nil, err
}
creds := aws.Credentials{
AccessKeyID: aws.StringValue(resp.Credentials.AccessKeyId),
SecretAccessKey: aws.StringValue(resp.Credentials.SecretAccessKey),
SessionToken: aws.StringValue(resp.Credentials.SessionToken),
}
shared := external.SharedConfig{
Credentials: creds,
Region: region,
}
config, err := external.LoadDefaultAWSConfig(shared)
return &config, err
}

// TODO(muvaf): All the types that use CreateJSONPatch are known during
// development time. In order to avoid unnecessary panic checks, we can generate
// the code that creates a patch between two objects that share the same type.
Expand Down
5 changes: 3 additions & 2 deletions pkg/clients/aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package aws

import (
"context"
"fmt"
"testing"

Expand Down Expand Up @@ -48,7 +49,7 @@ func TestCredentialsIdSecret(t *testing.T) {
g.Expect(secret).To(Equal(""))
}

func TestLoadConfig(t *testing.T) {
func TestUseProviderSecret(t *testing.T) {
g := NewGomegaWithT(t)

testProfile := "default"
Expand All @@ -57,7 +58,7 @@ func TestLoadConfig(t *testing.T) {
testRegion := "us-west-2"
credentials := []byte(fmt.Sprintf(awsCredentialsFileFormat, testProfile, testID, testSecret))

config, err := LoadConfig(credentials, testProfile, testRegion)
config, err := UseProviderSecret(context.TODO(), credentials, testProfile, testRegion)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(config).NotTo(BeNil())
}
9 changes: 5 additions & 4 deletions pkg/clients/cloudformation/cloudformation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package cloudformation

import (
"context"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
Expand All @@ -33,7 +34,7 @@ type Client interface {
}

type cloudFormationClient struct {
cloudformation cfiface.CloudFormationAPI
cloudformation cfiface.ClientAPI
}

// NewClient return new instance of the crossplane client for a specific AWS configuration
Expand All @@ -50,7 +51,7 @@ func (c *cloudFormationClient) CreateStack(stackName *string, templateBody *stri
}
}

createStackResponse, err := c.cloudformation.CreateStackRequest(&cf.CreateStackInput{Capabilities: []cf.Capability{cf.CapabilityCapabilityIam}, StackName: stackName, TemplateBody: templateBody, Parameters: cfParams}).Send()
createStackResponse, err := c.cloudformation.CreateStackRequest(&cf.CreateStackInput{Capabilities: []cf.Capability{cf.CapabilityCapabilityIam}, StackName: stackName, TemplateBody: templateBody, Parameters: cfParams}).Send(context.TODO())
if err != nil {
return nil, err
}
Expand All @@ -59,7 +60,7 @@ func (c *cloudFormationClient) CreateStack(stackName *string, templateBody *stri

// GetStack info
func (c *cloudFormationClient) GetStack(stackID *string) (stack *cf.Stack, err error) {
describeStackResponse, err := c.cloudformation.DescribeStacksRequest(&cf.DescribeStacksInput{StackName: stackID}).Send()
describeStackResponse, err := c.cloudformation.DescribeStacksRequest(&cf.DescribeStacksInput{StackName: stackID}).Send(context.TODO())
if err != nil {
return nil, err
}
Expand All @@ -75,7 +76,7 @@ func (c *cloudFormationClient) GetStack(stackID *string) (stack *cf.Stack, err e

// DeleteStack deletes a stack
func (c *cloudFormationClient) DeleteStack(stackID *string) error {
_, err := c.cloudformation.DeleteStackRequest(&cf.DeleteStackInput{StackName: stackID}).Send()
_, err := c.cloudformation.DeleteStackRequest(&cf.DeleteStackInput{StackName: stackID}).Send(context.TODO())
return err
}

Expand Down
13 changes: 7 additions & 6 deletions pkg/clients/eks/eks.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package eks

import (
"context"
"encoding/base64"
"errors"
"fmt"
Expand Down Expand Up @@ -105,9 +106,9 @@ type AMIClient interface {
}

type eksClient struct {
eks eksiface.EKSAPI
eks eksiface.ClientAPI
amiClient AMIClient
sts *sts.STS
sts *sts.Client
cloudformation cfc.Client
}

Expand All @@ -131,7 +132,7 @@ func (e *eksClient) Create(name string, spec awscomputev1alpha3.EKSClusterSpec)
input.Version = aws.String(spec.ClusterVersion)
}

output, err := e.eks.CreateClusterRequest(input).Send()
output, err := e.eks.CreateClusterRequest(input).Send(context.TODO())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -184,7 +185,7 @@ func (e *eksClient) CreateWorkerNodes(name string, clusterVersion string, spec a
// Get an existing EKS cluster
func (e *eksClient) Get(name string) (*Cluster, error) {
input := &eks.DescribeClusterInput{Name: aws.String(name)}
output, err := e.eks.DescribeClusterRequest(input).Send()
output, err := e.eks.DescribeClusterRequest(input).Send(context.TODO())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -215,7 +216,7 @@ func (e *eksClient) GetWorkerNodes(stackID string) (*ClusterWorkers, error) {
// Delete a EKS cluster
func (e *eksClient) Delete(name string) error {
input := &eks.DeleteClusterInput{Name: aws.String(name)}
_, err := e.eks.DeleteClusterRequest(input).Send()
_, err := e.eks.DeleteClusterRequest(input).Send(context.TODO())
return err
}

Expand Down Expand Up @@ -286,7 +287,7 @@ func (e *eksClient) getAvailableImages(clusterVersion string) ([]*ec2.Image, err
request := e.amiClient.DescribeImagesRequest(&ec2.DescribeImagesInput{
Filters: ec2Filters,
})
out, err := request.Send()
out, err := request.Send(context.TODO())

if err != nil {
return nil, err
Expand Down
14 changes: 7 additions & 7 deletions pkg/clients/elasticache/elasticache.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package elasticache

import (
"context"
"reflect"
"strconv"

Expand All @@ -26,24 +27,23 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/elasticache"
"github.com/aws/aws-sdk-go-v2/service/elasticache/elasticacheiface"
"github.com/pkg/errors"

"github.com/crossplaneio/stack-aws/apis/cache/v1beta1"
clients "github.com/crossplaneio/stack-aws/pkg/clients"
)

// A Client handles CRUD operations for ElastiCache resources. This interface is
// compatible with the upstream AWS redis client.
type Client elasticacheiface.ElastiCacheAPI
type Client elasticacheiface.ClientAPI

// NewClient returns a new ElastiCache client. Credentials must be passed as
// JSON encoded data.
func NewClient(credentials []byte, region string) (Client, error) {
cfg, err := clients.LoadConfig(credentials, clients.DefaultSection, region)
if err != nil {
return nil, errors.Wrap(err, "cannot create new AWS configuration")
func NewClient(ctx context.Context, credentials []byte, region string, auth clients.AuthMethod) (Client, error) {
cfg, err := auth(ctx, credentials, clients.DefaultSection, region)
if cfg == nil {
return nil, err
}
return elasticache.New(*cfg), nil
return elasticache.New(*cfg), err
}

// TODO(negz): Determine whether we have to handle converting zero values to
Expand Down
3 changes: 0 additions & 3 deletions pkg/clients/elasticache/elasticache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
package elasticache

import (
"fmt"
"strconv"
"testing"

Expand Down Expand Up @@ -610,8 +609,6 @@ func TestReplicationGroupNeedsUpdate(t *testing.T) {

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
name := tc.name
fmt.Println(name)
got := ReplicationGroupNeedsUpdate(tc.kube, tc.rg, tc.ccList)
if got != tc.want {
t.Errorf("ReplicationGroupNeedsUpdate(...): want %t, got %t", tc.want, got)
Expand Down
4 changes: 2 additions & 2 deletions pkg/clients/elasticache/fake/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import (
"github.com/aws/aws-sdk-go-v2/service/elasticache/elasticacheiface"
)

var _ elasticacheiface.ElastiCacheAPI = &MockClient{}
var _ elasticacheiface.ClientAPI = &MockClient{}

// MockClient is a fake implementation of cloudmemorystore.Client.
type MockClient struct {
elasticacheiface.ElastiCacheAPI
elasticacheiface.ClientAPI

MockDescribeReplicationGroupsRequest func(*elasticache.DescribeReplicationGroupsInput) elasticache.DescribeReplicationGroupsRequest
MockCreateReplicationGroupRequest func(*elasticache.CreateReplicationGroupInput) elasticache.CreateReplicationGroupRequest
Expand Down
Loading

0 comments on commit 2773e55

Please sign in to comment.