sigs.k8s.io/cluster-api-provider-aws@v1.5.5/pkg/cloud/services/s3/s3.go (about)

     1  /*
     2  Copyright 2021 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 s3
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"net/url"
    24  	"path"
    25  
    26  	"github.com/aws/aws-sdk-go/aws"
    27  	"github.com/aws/aws-sdk-go/aws/awserr"
    28  	"github.com/aws/aws-sdk-go/service/s3"
    29  	"github.com/aws/aws-sdk-go/service/s3/s3iface"
    30  	"github.com/aws/aws-sdk-go/service/sts"
    31  	"github.com/aws/aws-sdk-go/service/sts/stsiface"
    32  	"github.com/pkg/errors"
    33  
    34  	iam "sigs.k8s.io/cluster-api-provider-aws/iam/api/v1beta1"
    35  	"sigs.k8s.io/cluster-api-provider-aws/pkg/cloud/scope"
    36  )
    37  
    38  // Service holds a collection of interfaces.
    39  // The interfaces are broken down like this to group functions together.
    40  // One alternative is to have a large list of functions from the ec2 client.
    41  type Service struct {
    42  	scope     scope.S3Scope
    43  	S3Client  s3iface.S3API
    44  	STSClient stsiface.STSAPI
    45  }
    46  
    47  // NewService returns a new service given the api clients.
    48  func NewService(s3Scope scope.S3Scope) *Service {
    49  	s3Client := scope.NewS3Client(s3Scope, s3Scope, s3Scope, s3Scope.InfraCluster())
    50  	STSClient := scope.NewSTSClient(s3Scope, s3Scope, s3Scope, s3Scope.InfraCluster())
    51  
    52  	return &Service{
    53  		scope:     s3Scope,
    54  		S3Client:  s3Client,
    55  		STSClient: STSClient,
    56  	}
    57  }
    58  
    59  func (s *Service) ReconcileBucket() error {
    60  	if !s.bucketManagementEnabled() {
    61  		return nil
    62  	}
    63  
    64  	bucketName := s.bucketName()
    65  
    66  	if err := s.createBucketIfNotExist(bucketName); err != nil {
    67  		return errors.Wrap(err, "ensuring bucket exists")
    68  	}
    69  
    70  	if err := s.ensureBucketPolicy(bucketName); err != nil {
    71  		return errors.Wrap(err, "ensuring bucket policy")
    72  	}
    73  
    74  	return nil
    75  }
    76  
    77  func (s *Service) DeleteBucket() error {
    78  	if !s.bucketManagementEnabled() {
    79  		return nil
    80  	}
    81  
    82  	bucketName := s.bucketName()
    83  
    84  	log := s.scope.WithValues("name", bucketName)
    85  
    86  	log.Info("Deleting S3 Bucket")
    87  
    88  	_, err := s.S3Client.DeleteBucket(&s3.DeleteBucketInput{
    89  		Bucket: aws.String(bucketName),
    90  	})
    91  	if err == nil {
    92  		return nil
    93  	}
    94  
    95  	aerr, ok := err.(awserr.Error)
    96  	if !ok {
    97  		return errors.Wrap(err, "deleting S3 bucket")
    98  	}
    99  
   100  	switch aerr.Code() {
   101  	case s3.ErrCodeNoSuchBucket:
   102  		log.Info("Bucket already removed")
   103  	case "BucketNotEmpty":
   104  		log.Info("Bucket not empty, skipping removal")
   105  	default:
   106  		return errors.Wrap(aerr, "deleting S3 bucket")
   107  	}
   108  
   109  	return nil
   110  }
   111  
   112  func (s *Service) Create(m *scope.MachineScope, data []byte) (string, error) {
   113  	if !s.bucketManagementEnabled() {
   114  		return "", errors.New("requested object creation but bucket management is not enabled")
   115  	}
   116  
   117  	if m == nil {
   118  		return "", errors.New("machine scope can't be nil")
   119  	}
   120  
   121  	if len(data) == 0 {
   122  		return "", errors.New("got empty data")
   123  	}
   124  
   125  	bucket := s.bucketName()
   126  	key := s.bootstrapDataKey(m)
   127  
   128  	s.scope.Info("Creating object", "bucket_name", bucket, "key", key)
   129  
   130  	if _, err := s.S3Client.PutObject(&s3.PutObjectInput{
   131  		Body:                 aws.ReadSeekCloser(bytes.NewReader(data)),
   132  		Bucket:               aws.String(bucket),
   133  		Key:                  aws.String(key),
   134  		ServerSideEncryption: aws.String("aws:kms"),
   135  	}); err != nil {
   136  		return "", errors.Wrap(err, "putting object")
   137  	}
   138  
   139  	objectURL := &url.URL{
   140  		Scheme: "s3",
   141  		Host:   bucket,
   142  		Path:   key,
   143  	}
   144  
   145  	return objectURL.String(), nil
   146  }
   147  
   148  func (s *Service) Delete(m *scope.MachineScope) error {
   149  	if !s.bucketManagementEnabled() {
   150  		return errors.New("requested object creation but bucket management is not enabled")
   151  	}
   152  
   153  	if m == nil {
   154  		return errors.New("machine scope can't be nil")
   155  	}
   156  
   157  	bucket := s.bucketName()
   158  	key := s.bootstrapDataKey(m)
   159  
   160  	s.scope.Info("Deleting object", "bucket_name", bucket, "key", key)
   161  
   162  	_, err := s.S3Client.DeleteObject(&s3.DeleteObjectInput{
   163  		Bucket: aws.String(bucket),
   164  		Key:    aws.String(key),
   165  	})
   166  	if err == nil {
   167  		return nil
   168  	}
   169  
   170  	aerr, ok := err.(awserr.Error)
   171  	if !ok {
   172  		return errors.Wrap(err, "deleting S3 object")
   173  	}
   174  
   175  	switch aerr.Code() {
   176  	case s3.ErrCodeNoSuchBucket:
   177  	default:
   178  		return errors.Wrap(aerr, "deleting S3 object")
   179  	}
   180  
   181  	return nil
   182  }
   183  
   184  func (s *Service) createBucketIfNotExist(bucketName string) error {
   185  	input := &s3.CreateBucketInput{
   186  		Bucket: aws.String(bucketName),
   187  	}
   188  
   189  	_, err := s.S3Client.CreateBucket(input)
   190  	if err == nil {
   191  		s.scope.Info("Created bucket", "bucket_name", bucketName)
   192  
   193  		return nil
   194  	}
   195  
   196  	aerr, ok := err.(awserr.Error)
   197  	if !ok {
   198  		return errors.Wrap(err, "creating S3 bucket")
   199  	}
   200  
   201  	switch aerr.Code() {
   202  	// If bucket already exists, all good.
   203  	//
   204  	// TODO: This will fail if bucket is shared with other cluster.
   205  	case s3.ErrCodeBucketAlreadyOwnedByYou:
   206  		return nil
   207  	default:
   208  		return errors.Wrap(aerr, "creating S3 bucket")
   209  	}
   210  }
   211  
   212  func (s *Service) ensureBucketPolicy(bucketName string) error {
   213  	bucketPolicy, err := s.bucketPolicy(bucketName)
   214  	if err != nil {
   215  		return errors.Wrap(err, "generating Bucket policy")
   216  	}
   217  
   218  	input := &s3.PutBucketPolicyInput{
   219  		Bucket: aws.String(bucketName),
   220  		Policy: aws.String(bucketPolicy),
   221  	}
   222  
   223  	if _, err := s.S3Client.PutBucketPolicy(input); err != nil {
   224  		return errors.Wrap(err, "creating S3 bucket policy")
   225  	}
   226  
   227  	s.scope.V(4).Info("Updated bucket policy", "bucket_name", bucketName)
   228  
   229  	return nil
   230  }
   231  
   232  func (s *Service) bucketPolicy(bucketName string) (string, error) {
   233  	accountID, err := s.STSClient.GetCallerIdentity(&sts.GetCallerIdentityInput{})
   234  	if err != nil {
   235  		return "", errors.Wrap(err, "getting account ID")
   236  	}
   237  
   238  	bucket := s.scope.Bucket()
   239  
   240  	statements := []iam.StatementEntry{
   241  		{
   242  			Sid:    "control-plane",
   243  			Effect: iam.EffectAllow,
   244  			Principal: map[iam.PrincipalType]iam.PrincipalID{
   245  				iam.PrincipalAWS: []string{fmt.Sprintf("arn:aws:iam::%s:role/%s", *accountID.Account, bucket.ControlPlaneIAMInstanceProfile)},
   246  			},
   247  			Action:   []string{"s3:GetObject"},
   248  			Resource: []string{fmt.Sprintf("arn:aws:s3:::%s/control-plane/*", bucketName)},
   249  		},
   250  	}
   251  
   252  	for _, iamInstanceProfile := range bucket.NodesIAMInstanceProfiles {
   253  		statements = append(statements, iam.StatementEntry{
   254  			Sid:    iamInstanceProfile,
   255  			Effect: iam.EffectAllow,
   256  			Principal: map[iam.PrincipalType]iam.PrincipalID{
   257  				iam.PrincipalAWS: []string{fmt.Sprintf("arn:aws:iam::%s:role/%s", *accountID.Account, iamInstanceProfile)},
   258  			},
   259  			Action:   []string{"s3:GetObject"},
   260  			Resource: []string{fmt.Sprintf("arn:aws:s3:::%s/node/*", bucketName)},
   261  		})
   262  	}
   263  
   264  	policy := iam.PolicyDocument{
   265  		Version:   "2012-10-17",
   266  		Statement: statements,
   267  	}
   268  
   269  	policyRaw, err := json.Marshal(policy)
   270  	if err != nil {
   271  		return "", errors.Wrap(err, "building bucket policy")
   272  	}
   273  
   274  	return string(policyRaw), nil
   275  }
   276  
   277  func (s *Service) bucketManagementEnabled() bool {
   278  	return s.scope.Bucket() != nil
   279  }
   280  
   281  func (s *Service) bucketName() string {
   282  	return s.scope.Bucket().Name
   283  }
   284  
   285  func (s *Service) bootstrapDataKey(m *scope.MachineScope) string {
   286  	// Use machine name as object key.
   287  	return path.Join(m.Role(), m.Name())
   288  }