Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow auth using IAM Roles for Service Accounts on EKS #126

Merged
merged 4 commits into from
Feb 17, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
negz marked this conversation as resolved.
Show resolved Hide resolved
}

// +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.

55 changes: 52 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,45 @@ 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
hasheddan marked this conversation as resolved.
Show resolved Hide resolved
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")
}
negz marked this conversation as resolved.
Show resolved Hide resolved
cfg.Region = region
svc := sts.New(cfg)

b, err := ioutil.ReadFile(os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE"))
negz marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason behind this interface change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interfaces were renamed somewhere in the bump from v0.5 to v0.19 in the AWS SDK

}

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here - what's the context for this change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

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