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 }