@@ -23,6 +23,7 @@ import (
2323 "crypto/sha256"
2424 "encoding/base64"
2525 "encoding/hex"
26+ "encoding/json"
2627 "encoding/xml"
2728 "errors"
2829 "fmt"
@@ -32,6 +33,7 @@ import (
3233 "net/http"
3334 "net/url"
3435 "os"
36+ "regexp"
3537 "sort"
3638 "strconv"
3739 "strings"
@@ -41,17 +43,24 @@ import (
4143)
4244
4345type Storage struct {
44- endpoint string
45- accessKeyID string
46- secretKey string
47- bucket string
48- prefix string
49- location string
50- httpClient * http.Client
46+ endpoint string
47+ S3RoleEnabled bool
48+ s3Role string
49+ accessKeyID string
50+ secretKey string
51+ bucket string
52+ prefix string
53+ location string
54+ httpClient * http.Client
55+
56+ awsInstanceMetadataURL string
57+ awsCredsRefreshPeriod time.Duration
5158}
5259
5360var (
5461 ErrInvalidArguments = errors .New ("invalid arguments" )
62+ ErrKeyCredentialsProvided = errors .New ("remote storage configuration already includes access key and/or secret key" )
63+ ErrCredentialsCannotBeFound = errors .New ("cannot find credentials based on instance role remote storage" )
5564 ErrInvalidArgumentsOffsSize = fmt .Errorf ("%w: negative offset or zero size" , ErrInvalidArguments )
5665 ErrInvalidArgumentsNameStartSlash = fmt .Errorf ("%w: name can not start with /" , ErrInvalidArguments )
5766 ErrInvalidArgumentsNameEndSlash = fmt .Errorf ("%w: name can not end with /" , ErrInvalidArguments )
@@ -73,17 +82,22 @@ var (
7382 ErrInvalidResponseSubPathUnescape = fmt .Errorf ("%w: error un-escaping object name" , ErrInvalidResponse )
7483
7584 ErrTooManyRedirects = errors .New ("too many redirects" )
85+
86+ arnRoleRegex = regexp .MustCompile (`arn:.*\/(.*)` )
7687)
7788
7889const maxRedirects = 5
7990
8091func Open (
8192 endpoint string ,
93+ S3RoleEnabled bool ,
94+ s3Role string ,
8295 accessKeyID string ,
8396 secretKey string ,
8497 bucket string ,
8598 location string ,
8699 prefix string ,
100+ awsInstanceMetadataURL string ,
87101) (remotestorage.Storage , error ) {
88102
89103 // Endpoint must always end with '/'
@@ -106,19 +120,30 @@ func Open(
106120 prefix = prefix + "/"
107121 }
108122
109- return & Storage {
110- endpoint : endpoint ,
111- accessKeyID : accessKeyID ,
112- secretKey : secretKey ,
113- bucket : bucket ,
114- location : location ,
115- prefix : prefix ,
123+ s3storage := & Storage {
124+ endpoint : endpoint ,
125+ S3RoleEnabled : S3RoleEnabled ,
126+ s3Role : s3Role ,
127+ accessKeyID : accessKeyID ,
128+ secretKey : secretKey ,
129+ bucket : bucket ,
130+ location : location ,
131+ prefix : prefix ,
116132 httpClient : & http.Client {
117133 CheckRedirect : func (req * http.Request , via []* http.Request ) error {
118134 return http .ErrUseLastResponse
119135 },
120136 },
121- }, nil
137+ awsInstanceMetadataURL : awsInstanceMetadataURL ,
138+ awsCredsRefreshPeriod : time .Minute ,
139+ }
140+
141+ err := s3storage .getRoleCredentials ()
142+ if err != nil {
143+ return nil , err
144+ }
145+
146+ return s3storage , nil
122147}
123148
124149func (s * Storage ) Kind () string {
@@ -691,4 +716,124 @@ func (s *Storage) scanObjectNames(ctx context.Context, prefix string, limit int)
691716 return entries , subPaths , nil
692717}
693718
719+ func (s * Storage ) getRoleCredentials () error {
720+ if ! s .S3RoleEnabled {
721+ return nil
722+ }
723+
724+ var err error
725+ s .accessKeyID , s .secretKey , err = s .requestCredentials ()
726+ if err != nil {
727+ return err
728+ }
729+
730+ s3CredentialsRefreshTicker := time .NewTicker (s .awsCredsRefreshPeriod )
731+ go func () {
732+ for {
733+ select {
734+ case _ = <- s3CredentialsRefreshTicker .C :
735+ accessKeyID , secretKey , err := s .requestCredentials ()
736+ if err != nil {
737+ log .Printf ("S3 role credentials lookup failed with an error: %v" , err )
738+ continue
739+ }
740+ s .accessKeyID , s .secretKey = accessKeyID , secretKey
741+ }
742+ }
743+ }()
744+
745+ return nil
746+ }
747+
748+ func (s * Storage ) requestCredentials () (string , string , error ) {
749+ tokenReq , err := http .NewRequest ("PUT" , fmt .Sprintf ("%s%s" ,
750+ s .awsInstanceMetadataURL ,
751+ "/latest/api/token" ,
752+ ), nil )
753+ if err != nil {
754+ return "" , "" , errors .New ("cannot form metadata token request" )
755+ }
756+
757+ tokenReq .Header .Set ("X-aws-ec2-metadata-token-ttl-seconds" , "21600" )
758+
759+ tokenResp , err := http .DefaultClient .Do (tokenReq )
760+ if err != nil {
761+ return "" , "" , errors .New ("cannot get metadata token" )
762+ }
763+ defer tokenResp .Body .Close ()
764+
765+ token , err := ioutil .ReadAll (tokenResp .Body )
766+ if err != nil {
767+ return "" , "" , errors .New ("cannot read metadata token" )
768+ }
769+
770+ role := s .s3Role
771+ if s .s3Role == "" {
772+ roleReq , err := http .NewRequest ("GET" , fmt .Sprintf ("%s%s" ,
773+ s .awsInstanceMetadataURL ,
774+ "/latest/meta-data/iam/info" ,
775+ ), nil )
776+ if err != nil {
777+ return "" , "" , errors .New ("cannot form role name request" )
778+ }
779+
780+ roleReq .Header .Set ("X-aws-ec2-metadata-token" , string (token ))
781+ roleResp , err := http .DefaultClient .Do (roleReq )
782+ if err != nil {
783+ return "" , "" , errors .New ("cannot get role name" )
784+ }
785+ defer roleResp .Body .Close ()
786+
787+ creds , err := ioutil .ReadAll (roleResp .Body )
788+ if err != nil {
789+ return "" , "" , errors .New ("cannot read role name" )
790+ }
791+
792+ var metadata struct {
793+ InstanceProfileArn string `json:"InstanceProfileArn"`
794+ }
795+ if err := json .Unmarshal (creds , & metadata ); err != nil {
796+ return "" , "" , errors .New ("cannot parse role name" )
797+ }
798+
799+ match := arnRoleRegex .FindStringSubmatch (metadata .InstanceProfileArn )
800+ if len (match ) < 2 {
801+ return "" , "" , ErrCredentialsCannotBeFound
802+ }
803+
804+ role = match [1 ]
805+ }
806+
807+ credsReq , err := http .NewRequest ("GET" , fmt .Sprintf ("%s%s/%s" ,
808+ s .awsInstanceMetadataURL ,
809+ "/latest/meta-data/iam/security-credentials" ,
810+ role ,
811+ ), nil )
812+ if err != nil {
813+ return "" , "" , errors .New ("cannot form role credentials request" )
814+ }
815+
816+ credsReq .Header .Set ("X-aws-ec2-metadata-token" , string (token ))
817+ credsResp , err := http .DefaultClient .Do (credsReq )
818+ if err != nil {
819+ return "" , "" , errors .New ("cannot get role credentials" )
820+ }
821+ defer credsResp .Body .Close ()
822+
823+ creds , err := ioutil .ReadAll (credsReq .Body )
824+ if err != nil {
825+ return "" , "" , errors .New ("cannot read role credentials" )
826+ }
827+
828+ var credentials struct {
829+ AccessKeyID string `json:"AccessKeyId"`
830+ SecretAccessKey string `json:"SecretAccessKey"`
831+ }
832+ if err := json .Unmarshal (creds , & credentials ); err != nil {
833+ return "" , "" , errors .New ("cannot parse role credentials" )
834+ }
835+
836+ return credentials .AccessKeyID , credentials .SecretAccessKey , nil
837+ }
838+
694839var _ remotestorage.Storage = (* Storage )(nil )
0 commit comments