github.com/kyma-project/kyma/components/asset-store-controller-manager@v0.0.0-20191203152857-3792b5df17c5/internal/store/store.go (about)

     1  package store
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"path/filepath"
     8  	"reflect"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/kyma-project/kyma/components/asset-store-controller-manager/pkg/apis/assetstore/v1alpha2"
    15  	"github.com/minio/minio-go"
    16  	"github.com/minio/minio-go/pkg/policy"
    17  	"github.com/pkg/errors"
    18  )
    19  
    20  type Config struct {
    21  	Endpoint           string `envconfig:"default=minio.kyma.local"`
    22  	ExternalEndpoint   string `envconfig:"default=https://minio.kyma.local"`
    23  	AccessKey          string `envconfig:""`
    24  	SecretKey          string `envconfig:""`
    25  	UseSSL             bool   `envconfig:"default=true"`
    26  	UploadWorkersCount int    `envconfig:"default=10"`
    27  }
    28  
    29  //go:generate mockery -name=MinioClient -output=automock -outpkg=automock -case=underscore
    30  type MinioClient interface {
    31  	FPutObjectWithContext(ctx context.Context, bucketName, objectName, filePath string, opts minio.PutObjectOptions) (n int64, err error)
    32  	ListObjects(bucketName, objectPrefix string, recursive bool, doneCh <-chan struct{}) <-chan minio.ObjectInfo
    33  	MakeBucket(bucketName string, location string) error
    34  	BucketExists(bucketName string) (bool, error)
    35  	RemoveBucket(bucketName string) error
    36  	SetBucketPolicy(bucketName, policy string) error
    37  	GetBucketPolicy(bucketName string) (string, error)
    38  	RemoveObjectsWithContext(ctx context.Context, bucketName string, objectsCh <-chan string) <-chan minio.RemoveObjectError
    39  }
    40  
    41  //go:generate mockery -name=Store -output=automock -outpkg=automock -case=underscore
    42  type Store interface {
    43  	CreateBucket(namespace, crName, region string) (string, error)
    44  	BucketExists(name string) (bool, error)
    45  	DeleteBucket(ctx context.Context, name string) error
    46  	SetBucketPolicy(name string, policy v1alpha2.BucketPolicy) error
    47  	CompareBucketPolicy(name string, expected v1alpha2.BucketPolicy) (bool, error)
    48  	ContainsAllObjects(ctx context.Context, bucketName, assetName string, files []string) (bool, error)
    49  	PutObjects(ctx context.Context, bucketName, assetName, sourceBasePath string, files []string) error
    50  	DeleteObjects(ctx context.Context, bucketName, prefix string) error
    51  	ListObjects(ctx context.Context, bucketName, prefix string) ([]string, error)
    52  }
    53  
    54  type store struct {
    55  	client            MinioClient
    56  	uploadWorkerCount int
    57  }
    58  
    59  func New(client MinioClient, uploadWorkerCount int) Store {
    60  	return &store{
    61  		client:            client,
    62  		uploadWorkerCount: uploadWorkerCount,
    63  	}
    64  }
    65  
    66  // Bucket
    67  
    68  func (s *store) CreateBucket(namespace, crName, region string) (string, error) {
    69  	bucketName, err := s.findBucketName(crName)
    70  	if err != nil {
    71  		return "", err
    72  	}
    73  
    74  	err = s.client.MakeBucket(bucketName, region)
    75  	if err != nil {
    76  		return "", errors.Wrapf(err, "while creating bucket %s in region %s", bucketName, region)
    77  	}
    78  
    79  	return bucketName, nil
    80  }
    81  
    82  func (s *store) BucketExists(name string) (bool, error) {
    83  	exists, err := s.client.BucketExists(name)
    84  	if err != nil {
    85  		return false, errors.Wrapf(err, "while checking if bucket %s exists", name)
    86  	}
    87  
    88  	return exists, nil
    89  }
    90  
    91  func (s *store) DeleteBucket(ctx context.Context, name string) error {
    92  	exists, err := s.BucketExists(name)
    93  	if err != nil {
    94  		return err
    95  	}
    96  	if !exists {
    97  		return nil
    98  	}
    99  
   100  	err = s.DeleteObjects(ctx, name, "")
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	err = s.client.RemoveBucket(name)
   106  	if err != nil {
   107  		return errors.Wrapf(err, "while deleting bucket %s", name)
   108  	}
   109  
   110  	return nil
   111  }
   112  
   113  func (s *store) SetBucketPolicy(name string, policy v1alpha2.BucketPolicy) error {
   114  	bucketPolicy := s.prepareBucketPolicy(name, policy)
   115  	marshaled, err := s.marshalBucketPolicy(bucketPolicy)
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	err = s.client.SetBucketPolicy(name, marshaled)
   121  	if err != nil {
   122  		return errors.Wrapf(err, "while setting policy `%s` for bucket %s", policy, name)
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  func (s *store) CompareBucketPolicy(name string, expected v1alpha2.BucketPolicy) (bool, error) {
   129  	expectedPolicy := s.prepareBucketPolicy(name, expected)
   130  	currentPolicy, err := s.getBucketPolicy(name)
   131  	if err != nil {
   132  		return false, err
   133  	}
   134  
   135  	if currentPolicy == nil {
   136  		return false, nil
   137  	}
   138  
   139  	if len(expectedPolicy.Statements) > 1 && len(currentPolicy.Statements) == 1 {
   140  		return s.compareMergedPolicy(expectedPolicy, currentPolicy), nil
   141  	}
   142  
   143  	return reflect.DeepEqual(&expectedPolicy, currentPolicy), nil
   144  }
   145  
   146  // Object
   147  
   148  func (s *store) ContainsAllObjects(ctx context.Context, bucketName, assetName string, files []string) (bool, error) {
   149  	objects, err := s.listObjects(ctx, bucketName, assetName)
   150  	if err != nil {
   151  		return false, err
   152  	}
   153  
   154  	for _, f := range files {
   155  		key := fmt.Sprintf("%s/%s", assetName, f)
   156  
   157  		_, ok := objects[key]
   158  		if !ok {
   159  			return false, nil
   160  		}
   161  	}
   162  
   163  	return true, nil
   164  }
   165  
   166  func iterateSlice(files []string) chan string {
   167  	fileNameChan := make(chan string, len(files))
   168  	defer close(fileNameChan)
   169  	for _, fileName := range files {
   170  		fileNameChan <- fileName
   171  	}
   172  
   173  	return fileNameChan
   174  }
   175  
   176  type objectAttrs struct {
   177  	bucketName, assetName, sourceBasePath string
   178  }
   179  
   180  func (s *store) PutObjects(ctx context.Context, bucketName, assetName, sourceBasePath string, files []string) error {
   181  	fileNameChan := iterateSlice(files)
   182  	errChan := make(chan error)
   183  	go func() {
   184  		defer close(errChan)
   185  		objAttrs := objectAttrs{
   186  			bucketName:     bucketName,
   187  			assetName:      assetName,
   188  			sourceBasePath: sourceBasePath,
   189  		}
   190  		var waitGroup sync.WaitGroup
   191  		for i := 0; i < s.uploadWorkerCount; i++ {
   192  			waitGroup.Add(1)
   193  			go func() {
   194  				defer waitGroup.Done()
   195  				s.putObject(ctx, objAttrs, fileNameChan, errChan)
   196  			}()
   197  		}
   198  		waitGroup.Wait()
   199  	}()
   200  
   201  	var errorMessages []string
   202  	for err := range errChan {
   203  		errorMessages = append(errorMessages, err.Error())
   204  	}
   205  	if len(errorMessages) == 0 {
   206  		return nil
   207  	}
   208  	errMsg := strings.Join(errorMessages, "\n")
   209  	return errors.New(errMsg)
   210  }
   211  
   212  func (s *store) putObject(ctx context.Context, attrs objectAttrs, fileNameChan chan string, errChan chan error) {
   213  	for {
   214  		select {
   215  		case <-ctx.Done():
   216  			return
   217  		case <-errChan:
   218  			return
   219  		case file, ok := <-fileNameChan:
   220  			if !ok {
   221  				return
   222  			}
   223  			bucketPath := filepath.Join(attrs.assetName, file)
   224  			sourcePath := filepath.Join(attrs.sourceBasePath, file)
   225  			_, err := s.client.FPutObjectWithContext(
   226  				ctx, attrs.bucketName, bucketPath, sourcePath, minio.PutObjectOptions{})
   227  			if err != nil {
   228  				errChan <- err
   229  			}
   230  		}
   231  	}
   232  }
   233  
   234  func (s *store) ListObjects(ctx context.Context, bucketName, prefix string) ([]string, error) {
   235  	objects, err := s.listObjects(ctx, bucketName, prefix)
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  
   240  	result := make([]string, 0, len(objects))
   241  	for key := range objects {
   242  		result = append(result, key)
   243  	}
   244  
   245  	return result, nil
   246  }
   247  
   248  func (s *store) DeleteObjects(ctx context.Context, bucketName, prefix string) error {
   249  	objects, err := s.listObjects(ctx, bucketName, prefix)
   250  	if err != nil {
   251  		return err
   252  	}
   253  	if len(objects) == 0 {
   254  		return nil
   255  	}
   256  
   257  	objectsCh := make(chan string)
   258  	go func(objects map[string]minio.ObjectInfo) {
   259  		defer close(objectsCh)
   260  
   261  		for key := range objects {
   262  			objectsCh <- key
   263  		}
   264  	}(objects)
   265  
   266  	errs := make([]error, 0)
   267  	for err := range s.client.RemoveObjectsWithContext(ctx, bucketName, objectsCh) {
   268  		errs = append(errs, err.Err)
   269  	}
   270  
   271  	if len(errs) > 0 {
   272  		messages := s.extractErrorMessages(errs)
   273  		return fmt.Errorf("cannot delete objects from bucket: %+v", messages)
   274  	}
   275  
   276  	return nil
   277  }
   278  
   279  // Helpers
   280  
   281  func (s *store) getBucketPolicy(name string) (*policy.BucketAccessPolicy, error) {
   282  	marshaled, err := s.client.GetBucketPolicy(name)
   283  	if err != nil {
   284  		return nil, errors.Wrapf(err, "while getting policy for bucket %s", name)
   285  	}
   286  	if len(marshaled) == 0 {
   287  		return nil, nil
   288  	}
   289  
   290  	result, err := s.unmarshalBucketPolicy(marshaled)
   291  	if err != nil {
   292  		return nil, errors.Wrapf(err, "while unmarshalling policy for bucket %s", name)
   293  	}
   294  
   295  	return result, nil
   296  }
   297  
   298  func (*store) extractErrorMessages(errs []error) []string {
   299  	messages := make([]string, 0, len(errs))
   300  	for _, err := range errs {
   301  		messages = append(messages, err.Error())
   302  	}
   303  
   304  	return messages
   305  }
   306  
   307  func (s *store) listObjects(ctx context.Context, bucketName, prefix string) (map[string]minio.ObjectInfo, error) {
   308  	result := make(map[string]minio.ObjectInfo)
   309  	errs := make([]error, 0)
   310  	for message := range s.client.ListObjects(bucketName, prefix, true, ctx.Done()) {
   311  		result[message.Key] = message
   312  
   313  		if message.Err != nil {
   314  			errs = append(errs, message.Err)
   315  		}
   316  	}
   317  
   318  	if len(errs) > 0 {
   319  		messages := s.extractErrorMessages(errs)
   320  		return result, fmt.Errorf("cannot list objects in bucket: %+v", messages)
   321  	}
   322  
   323  	return result, nil
   324  }
   325  
   326  func (s *store) findBucketName(name string) (string, error) {
   327  	sleep := time.Millisecond
   328  	for i := 0; i < 10; i++ {
   329  		name := s.generateBucketName(name)
   330  		exists, err := s.BucketExists(name)
   331  		if err != nil {
   332  			return "", errors.Wrap(err, "while checking if bucket name is available")
   333  		}
   334  		if !exists {
   335  			return name, nil
   336  		}
   337  		time.Sleep(sleep)
   338  		sleep *= 2
   339  	}
   340  
   341  	return "", errors.New("cannot find bucket name")
   342  }
   343  
   344  func (s *store) generateBucketName(name string) string {
   345  	unixNano := time.Now().UnixNano()
   346  	suffix := strconv.FormatInt(unixNano, 32)
   347  
   348  	return fmt.Sprintf("%s-%s", name, suffix)
   349  }
   350  
   351  func (s *store) prepareBucketPolicy(bucketName string, bucketPolicy v1alpha2.BucketPolicy) policy.BucketAccessPolicy {
   352  	statements := make([]policy.Statement, 0)
   353  	switch {
   354  	case bucketPolicy == v1alpha2.BucketPolicyReadOnly:
   355  		statements = policy.SetPolicy(statements, policy.BucketPolicyReadOnly, bucketName, "")
   356  	case bucketPolicy == v1alpha2.BucketPolicyWriteOnly:
   357  		statements = policy.SetPolicy(statements, policy.BucketPolicyWriteOnly, bucketName, "")
   358  	case bucketPolicy == v1alpha2.BucketPolicyReadWrite:
   359  		statements = policy.SetPolicy(statements, policy.BucketPolicyReadWrite, bucketName, "")
   360  	default:
   361  		statements = policy.SetPolicy(statements, policy.BucketPolicyNone, bucketName, "")
   362  	}
   363  
   364  	return policy.BucketAccessPolicy{
   365  		Version:    "2012-10-17", // Fixed version
   366  		Statements: statements,
   367  	}
   368  }
   369  
   370  func (s *store) marshalBucketPolicy(policy policy.BucketAccessPolicy) (string, error) {
   371  	bytes, err := json.Marshal(&policy)
   372  	if err != nil {
   373  		return "", errors.Wrap(err, "while marshalling bucket policy")
   374  	}
   375  
   376  	return string(bytes), nil
   377  }
   378  
   379  func (s *store) unmarshalBucketPolicy(marshaledPolicy string) (*policy.BucketAccessPolicy, error) {
   380  	bucketPolicy := &policy.BucketAccessPolicy{}
   381  	err := json.Unmarshal([]byte(marshaledPolicy), bucketPolicy)
   382  	if err != nil {
   383  		return bucketPolicy, errors.Wrap(err, "while unmarshalling bucket access policy")
   384  	}
   385  
   386  	return bucketPolicy, nil
   387  }
   388  
   389  func (s *store) compareMergedPolicy(expected policy.BucketAccessPolicy, current *policy.BucketAccessPolicy) bool {
   390  	if current == nil {
   391  		return false
   392  	}
   393  
   394  	merged := policy.BucketAccessPolicy{
   395  		Version:    expected.Version,
   396  		Statements: []policy.Statement{expected.Statements[0]},
   397  	}
   398  
   399  	for _, statement := range expected.Statements[1:] {
   400  		for action := range statement.Actions {
   401  			merged.Statements[0].Actions.Add(action)
   402  		}
   403  
   404  		for resource := range statement.Resources {
   405  			merged.Statements[0].Resources.Add(resource)
   406  		}
   407  	}
   408  
   409  	return reflect.DeepEqual(&merged, current)
   410  }