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

     1  package asset
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/go-logr/logr"
    10  	"github.com/kyma-project/kyma/components/asset-store-controller-manager/internal/assethook"
    11  	"github.com/kyma-project/kyma/components/asset-store-controller-manager/internal/loader"
    12  	"github.com/kyma-project/kyma/components/asset-store-controller-manager/internal/store"
    13  	"github.com/kyma-project/kyma/components/asset-store-controller-manager/pkg/apis/assetstore/v1alpha2"
    14  	"github.com/pkg/errors"
    15  	"k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/runtime"
    17  	"k8s.io/apimachinery/pkg/runtime/schema"
    18  	"k8s.io/client-go/tools/record"
    19  )
    20  
    21  type Handler interface {
    22  	Do(ctx context.Context, now time.Time, instance MetaAccessor, spec v1alpha2.CommonAssetSpec, status v1alpha2.CommonAssetStatus) (*v1alpha2.CommonAssetStatus, error)
    23  }
    24  
    25  type MetaAccessor interface {
    26  	GetNamespace() string
    27  	GetName() string
    28  	GetGeneration() int64
    29  	GetDeletionTimestamp() *v1.Time
    30  	GetFinalizers() []string
    31  	SetFinalizers(finalizers []string)
    32  	GetObjectKind() schema.ObjectKind
    33  	DeepCopyObject() runtime.Object
    34  }
    35  
    36  var _ Handler = &assetHandler{}
    37  
    38  type FindBucketStatus func(ctx context.Context, namespace, name string) (*v1alpha2.CommonBucketStatus, bool, error)
    39  
    40  type assetHandler struct {
    41  	recorder          record.EventRecorder
    42  	findBucketStatus  FindBucketStatus
    43  	store             store.Store
    44  	loader            loader.Loader
    45  	validator         assethook.Validator
    46  	mutator           assethook.Mutator
    47  	metadataExtractor assethook.MetadataExtractor
    48  	log               logr.Logger
    49  	relistInterval    time.Duration
    50  }
    51  
    52  func New(log logr.Logger, recorder record.EventRecorder, store store.Store, loader loader.Loader, findBucketFnc FindBucketStatus, validator assethook.Validator, mutator assethook.Mutator, metadataExtractor assethook.MetadataExtractor, relistInterval time.Duration) Handler {
    53  	return &assetHandler{
    54  		recorder:          recorder,
    55  		store:             store,
    56  		loader:            loader,
    57  		findBucketStatus:  findBucketFnc,
    58  		validator:         validator,
    59  		mutator:           mutator,
    60  		metadataExtractor: metadataExtractor,
    61  		log:               log,
    62  		relistInterval:    relistInterval,
    63  	}
    64  }
    65  
    66  func (h *assetHandler) Do(ctx context.Context, now time.Time, instance MetaAccessor, spec v1alpha2.CommonAssetSpec, status v1alpha2.CommonAssetStatus) (*v1alpha2.CommonAssetStatus, error) {
    67  	h.logInfof("Start common Asset handling")
    68  	defer h.logInfof("Finish common Asset handling")
    69  
    70  	switch {
    71  	case h.isOnDelete(instance):
    72  		h.logInfof("On delete")
    73  		return h.onDelete(ctx, instance, spec)
    74  	case h.isOnAddOrUpdate(instance, status):
    75  		h.logInfof("On add or update")
    76  		return h.getStatus(instance, v1alpha2.AssetPending, v1alpha2.AssetScheduled), nil
    77  	case h.isOnReady(status, now):
    78  		h.logInfof("On ready")
    79  		return h.onReady(ctx, instance, spec, status)
    80  	case h.isOnPending(status, now):
    81  		h.logInfof("On pending")
    82  		return h.onPending(ctx, instance, spec, status)
    83  	case h.isOnFailed(status):
    84  		h.logInfof("On failed")
    85  		return h.onPending(ctx, instance, spec, status)
    86  	default:
    87  		h.logInfof("Action not taken")
    88  		return nil, nil
    89  	}
    90  }
    91  
    92  func (*assetHandler) isOnAddOrUpdate(object MetaAccessor, status v1alpha2.CommonAssetStatus) bool {
    93  	return status.ObservedGeneration != object.GetGeneration()
    94  }
    95  
    96  func (h *assetHandler) isOnPending(status v1alpha2.CommonAssetStatus, now time.Time) bool {
    97  	if status.Phase == v1alpha2.AssetPending {
    98  		if status.Reason == v1alpha2.AssetBucketNotReady && now.Before(status.LastHeartbeatTime.Add(h.relistInterval)) {
    99  			return false
   100  		}
   101  
   102  		return true
   103  	}
   104  
   105  	return false
   106  }
   107  
   108  func (*assetHandler) isOnDelete(object MetaAccessor) bool {
   109  	return !object.GetDeletionTimestamp().IsZero()
   110  }
   111  
   112  func (*assetHandler) isOnFailed(status v1alpha2.CommonAssetStatus) bool {
   113  	return status.Phase == v1alpha2.AssetFailed &&
   114  		status.Reason != v1alpha2.AssetValidationFailed &&
   115  		status.Reason != v1alpha2.AssetMutationFailed
   116  }
   117  
   118  func (h *assetHandler) isOnReady(status v1alpha2.CommonAssetStatus, now time.Time) bool {
   119  	return status.Phase == v1alpha2.AssetReady && now.After(status.LastHeartbeatTime.Add(h.relistInterval))
   120  }
   121  
   122  func (h *assetHandler) onDelete(ctx context.Context, object MetaAccessor, spec v1alpha2.CommonAssetSpec) (*v1alpha2.CommonAssetStatus, error) {
   123  	h.logInfof("Deleting Asset")
   124  	bucketStatus, isReady, err := h.findBucketStatus(ctx, object.GetNamespace(), spec.BucketRef.Name)
   125  	if err != nil {
   126  		return nil, errors.Wrap(err, "while reading bucket status")
   127  	}
   128  	if !isReady {
   129  		h.logInfof("Nothing to delete, bucket %s is not ready", spec.BucketRef.Name)
   130  		return nil, nil
   131  	}
   132  
   133  	if err := h.deleteRemoteContent(ctx, object, bucketStatus.RemoteName); err != nil {
   134  		return nil, err
   135  	}
   136  	h.logInfof("Asset deleted")
   137  
   138  	return nil, nil
   139  }
   140  
   141  func (h *assetHandler) deleteRemoteContent(ctx context.Context, object MetaAccessor, bucketName string) error {
   142  	h.logInfof("Checking if bucket contains files for asset")
   143  	prefix := object.GetName()
   144  	files, err := h.store.ListObjects(ctx, bucketName, prefix)
   145  	if err != nil {
   146  		return errors.Wrap(err, "while listing files in bucket")
   147  	}
   148  
   149  	if len(files) == 0 {
   150  		h.logInfof("Bucket doesn't contains asset files, nothing to delete")
   151  		return nil
   152  	}
   153  
   154  	h.logInfof("Deleting asset remote content")
   155  	if err := h.store.DeleteObjects(ctx, bucketName, prefix); err != nil {
   156  		return errors.Wrap(err, "while deleting asset content")
   157  	}
   158  	h.logInfof("Remote content deleted")
   159  	h.recordNormalEventf(object, v1alpha2.AssetCleaned)
   160  
   161  	return nil
   162  }
   163  
   164  func (h *assetHandler) onReady(ctx context.Context, object MetaAccessor, spec v1alpha2.CommonAssetSpec, status v1alpha2.CommonAssetStatus) (*v1alpha2.CommonAssetStatus, error) {
   165  	h.logInfof("Checking if bucket %s is ready", spec.BucketRef.Name)
   166  	bucketStatus, isReady, err := h.findBucketStatus(ctx, object.GetNamespace(), spec.BucketRef.Name)
   167  	if err != nil {
   168  		h.recordWarningEventf(object, v1alpha2.AssetBucketError, err.Error())
   169  		return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetBucketError, err.Error()), err
   170  	}
   171  	if !isReady {
   172  		h.logInfof("Bucket %s is not ready", spec.BucketRef.Name)
   173  		h.recordWarningEventf(object, v1alpha2.AssetBucketNotReady)
   174  		return h.getStatus(object, v1alpha2.AssetPending, v1alpha2.AssetBucketNotReady), nil
   175  	}
   176  	h.logInfof("Bucket %s is ready", spec.BucketRef.Name)
   177  
   178  	h.logInfof("Checking if store contains all files")
   179  	exists, err := h.store.ContainsAllObjects(ctx, bucketStatus.RemoteName, object.GetName(), h.extractNames(status.AssetRef.Files))
   180  	if err != nil {
   181  		h.recordWarningEventf(object, v1alpha2.AssetRemoteContentVerificationError, err.Error())
   182  		return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetRemoteContentVerificationError, err.Error()), err
   183  	}
   184  	if !exists {
   185  		h.recordWarningEventf(object, v1alpha2.AssetMissingContent)
   186  		return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetMissingContent), err
   187  	}
   188  
   189  	h.logInfof("Asset is up-to-date")
   190  
   191  	return h.getReadyStatus(object, status.AssetRef.BaseURL, status.AssetRef.Files, v1alpha2.AssetUploaded), nil
   192  }
   193  
   194  func (h *assetHandler) extractNames(files []v1alpha2.AssetFile) []string {
   195  	names := make([]string, 0, len(files))
   196  
   197  	for _, file := range files {
   198  		names = append(names, file.Name)
   199  	}
   200  
   201  	return names
   202  }
   203  
   204  func (h *assetHandler) onPending(ctx context.Context, object MetaAccessor, spec v1alpha2.CommonAssetSpec, status v1alpha2.CommonAssetStatus) (*v1alpha2.CommonAssetStatus, error) {
   205  	h.logInfof("Checking if bucket %s is ready", spec.BucketRef.Name)
   206  	bucketStatus, isReady, err := h.findBucketStatus(ctx, object.GetNamespace(), spec.BucketRef.Name)
   207  	if err != nil {
   208  		h.recordWarningEventf(object, v1alpha2.AssetBucketError, err.Error())
   209  		return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetBucketError, err.Error()), err
   210  	}
   211  	if !isReady {
   212  		h.logInfof("Bucket %s is not ready", spec.BucketRef.Name)
   213  		h.recordWarningEventf(object, v1alpha2.AssetBucketNotReady)
   214  		return h.getStatus(object, v1alpha2.AssetPending, v1alpha2.AssetBucketNotReady), nil
   215  	}
   216  	h.logInfof("Bucket %s is ready", spec.BucketRef.Name)
   217  
   218  	if err := h.deleteRemoteContent(ctx, object, bucketStatus.RemoteName); err != nil {
   219  		h.recordWarningEventf(object, v1alpha2.AssetCleanupError, err.Error())
   220  		return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetCleanupError, err.Error()), err
   221  	}
   222  
   223  	h.logInfof("Loading files from %s", spec.Source.URL)
   224  	basePath, filenames, err := h.loader.Load(spec.Source.URL, object.GetName(), spec.Source.Mode, spec.Source.Filter)
   225  	defer h.loader.Clean(basePath)
   226  	if err != nil {
   227  		h.recordWarningEventf(object, v1alpha2.AssetPullingFailed, err.Error())
   228  		return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetPullingFailed, err.Error()), err
   229  	}
   230  	h.logInfof("Files loaded")
   231  	h.recordNormalEventf(object, v1alpha2.AssetPulled)
   232  
   233  	if len(spec.Source.MutationWebhookService) > 0 {
   234  		h.logInfof("Mutating Asset content")
   235  		result, err := h.mutator.Mutate(ctx, basePath, filenames, spec.Source.MutationWebhookService)
   236  		if err != nil {
   237  			h.recordWarningEventf(object, v1alpha2.AssetMutationFailed, err.Error())
   238  			return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetMutationError, err.Error()), err
   239  		}
   240  		if !result.Success {
   241  			h.recordWarningEventf(object, v1alpha2.AssetMutationFailed, result.Messages)
   242  			return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetMutationFailed, result.Messages), nil
   243  		}
   244  		h.logInfof("Asset content mutated")
   245  		h.recordNormalEventf(object, v1alpha2.AssetMutated)
   246  	}
   247  
   248  	if len(spec.Source.ValidationWebhookService) > 0 {
   249  		h.logInfof("Validating Asset content")
   250  		result, err := h.validator.Validate(ctx, basePath, filenames, spec.Source.ValidationWebhookService)
   251  		if err != nil {
   252  			h.recordWarningEventf(object, v1alpha2.AssetValidationError, err.Error())
   253  			return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetValidationError, err.Error()), err
   254  		}
   255  		if !result.Success {
   256  			h.recordWarningEventf(object, v1alpha2.AssetValidationFailed, result.Messages)
   257  			return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetValidationFailed, result.Messages), nil
   258  		}
   259  		h.logInfof("Asset content validated")
   260  		h.recordNormalEventf(object, v1alpha2.AssetValidated)
   261  	}
   262  
   263  	files := h.populateFiles(filenames)
   264  	if len(spec.Source.MetadataWebhookService) > 0 {
   265  		h.logInfof("Extracting metadata from Assets content")
   266  		result, err := h.metadataExtractor.Extract(ctx, basePath, filenames, spec.Source.MetadataWebhookService)
   267  		if err != nil {
   268  			h.recordWarningEventf(object, v1alpha2.AssetMetadataExtractionFailed, err.Error())
   269  			return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetMetadataExtractionFailed, err.Error()), err
   270  		}
   271  
   272  		files = h.mergeMetadata(files, result)
   273  
   274  		h.logInfof("Metadata extracted")
   275  		h.recordNormalEventf(object, v1alpha2.AssetMetadataExtracted)
   276  	}
   277  
   278  	h.logInfof("Uploading Asset content to Minio")
   279  	if err := h.store.PutObjects(ctx, bucketStatus.RemoteName, object.GetName(), basePath, filenames); err != nil {
   280  		h.recordWarningEventf(object, v1alpha2.AssetUploadFailed, err.Error())
   281  		return h.getStatus(object, v1alpha2.AssetFailed, v1alpha2.AssetUploadFailed, err.Error()), err
   282  	}
   283  	h.logInfof("Asset content uploaded")
   284  	h.recordNormalEventf(object, v1alpha2.AssetUploaded)
   285  
   286  	return h.getReadyStatus(object, h.getBaseUrl(bucketStatus.URL, object.GetName()), files, v1alpha2.AssetUploaded), nil
   287  }
   288  
   289  func (h *assetHandler) populateFiles(filenames []string) []v1alpha2.AssetFile {
   290  	result := make([]v1alpha2.AssetFile, 0, len(filenames))
   291  
   292  	for _, filename := range filenames {
   293  		result = append(result, v1alpha2.AssetFile{Name: filename})
   294  	}
   295  
   296  	return result
   297  }
   298  
   299  func (h *assetHandler) mergeMetadata(files []v1alpha2.AssetFile, metadatas []assethook.File) []v1alpha2.AssetFile {
   300  	metadataMap := make(map[string]*json.RawMessage)
   301  	for _, metadata := range metadatas {
   302  		metadataMap[metadata.Name] = metadata.Metadata
   303  	}
   304  
   305  	result := make([]v1alpha2.AssetFile, 0, len(files))
   306  	for _, file := range files {
   307  		metadata := h.toRawExtension(metadataMap[file.Name])
   308  
   309  		result = append(result, v1alpha2.AssetFile{Name: file.Name, Metadata: metadata})
   310  	}
   311  
   312  	return result
   313  }
   314  
   315  func (h *assetHandler) toRawExtension(message *json.RawMessage) *runtime.RawExtension {
   316  	if message == nil {
   317  		return nil
   318  	}
   319  
   320  	return &runtime.RawExtension{Raw: *message}
   321  }
   322  
   323  func (h *assetHandler) getBaseUrl(bucketUrl, assetName string) string {
   324  	return fmt.Sprintf("%s/%s", bucketUrl, assetName)
   325  }
   326  
   327  func (h *assetHandler) recordNormalEventf(object MetaAccessor, reason v1alpha2.AssetReason, args ...interface{}) {
   328  	h.recordEventf(object, "Normal", reason, args...)
   329  }
   330  
   331  func (h *assetHandler) recordWarningEventf(object MetaAccessor, reason v1alpha2.AssetReason, args ...interface{}) {
   332  	h.recordEventf(object, "Warning", reason, args...)
   333  }
   334  
   335  func (h *assetHandler) logInfof(message string, args ...interface{}) {
   336  	h.log.Info(fmt.Sprintf(message, args...))
   337  }
   338  
   339  func (h *assetHandler) recordEventf(object MetaAccessor, eventType string, reason v1alpha2.AssetReason, args ...interface{}) {
   340  	h.recorder.Eventf(object, eventType, reason.String(), reason.Message(), args...)
   341  }
   342  
   343  func (h *assetHandler) getReadyStatus(object MetaAccessor, baseUrl string, files []v1alpha2.AssetFile, reason v1alpha2.AssetReason, args ...interface{}) *v1alpha2.CommonAssetStatus {
   344  	status := h.getStatus(object, v1alpha2.AssetReady, reason, args...)
   345  	status.AssetRef.BaseURL = baseUrl
   346  	status.AssetRef.Files = files
   347  	return status
   348  }
   349  
   350  func (*assetHandler) getStatus(object MetaAccessor, phase v1alpha2.AssetPhase, reason v1alpha2.AssetReason, args ...interface{}) *v1alpha2.CommonAssetStatus {
   351  	return &v1alpha2.CommonAssetStatus{
   352  		LastHeartbeatTime:  v1.Now(),
   353  		ObservedGeneration: object.GetGeneration(),
   354  		Phase:              phase,
   355  		Reason:             reason,
   356  		Message:            fmt.Sprintf(reason.Message(), args...),
   357  	}
   358  }