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 }