github.com/oam-dev/kubevela@v1.9.11/pkg/controller/core.oam.dev/v1beta1/core/revison.go (about) 1 /* 2 Copyright 2021. The KubeVela 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 core 18 19 import ( 20 "context" 21 "fmt" 22 "sort" 23 "strings" 24 25 "github.com/crossplane/crossplane-runtime/pkg/event" 26 "github.com/pkg/errors" 27 apiequality "k8s.io/apimachinery/pkg/api/equality" 28 apierrors "k8s.io/apimachinery/pkg/api/errors" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/types" 32 "k8s.io/klog/v2" 33 ctrl "sigs.k8s.io/controller-runtime" 34 "sigs.k8s.io/controller-runtime/pkg/client" 35 36 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 37 "github.com/oam-dev/kubevela/apis/core.oam.dev/condition" 38 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 39 "github.com/oam-dev/kubevela/pkg/controller/utils" 40 "github.com/oam-dev/kubevela/pkg/oam" 41 "github.com/oam-dev/kubevela/pkg/oam/util" 42 ) 43 44 // GenerateDefinitionRevision will generate a definition revision the generated revision 45 // will be compare with the last revision to see if there's any difference. 46 func GenerateDefinitionRevision(ctx context.Context, cli client.Client, def runtime.Object) (*v1beta1.DefinitionRevision, bool, error) { 47 isNamedRev, defRevNamespacedName, err := isNamedRevision(def) 48 if err != nil { 49 return nil, false, err 50 } 51 if isNamedRev { 52 return generateNamedDefinitionRevision(ctx, cli, def, defRevNamespacedName) 53 } 54 55 defRev, lastRevision, err := GatherRevisionInfo(def) 56 if err != nil { 57 return defRev, false, err 58 } 59 isNewRev, err := compareWithLastDefRevisionSpec(ctx, cli, defRev, lastRevision) 60 if err != nil { 61 return defRev, isNewRev, err 62 } 63 if isNewRev { 64 defRevName, revNum := getDefNextRevision(defRev, lastRevision) 65 defRev.Name = defRevName 66 defRev.Spec.Revision = revNum 67 } 68 return defRev, isNewRev, nil 69 } 70 71 func isNamedRevision(def runtime.Object) (bool, types.NamespacedName, error) { 72 defMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(def) 73 if err != nil { 74 return false, types.NamespacedName{}, err 75 } 76 unstructuredDef := unstructured.Unstructured{ 77 Object: defMap, 78 } 79 revisionName := unstructuredDef.GetAnnotations()[oam.AnnotationDefinitionRevisionName] 80 if len(revisionName) == 0 { 81 return false, types.NamespacedName{}, nil 82 } 83 defNs := unstructuredDef.GetNamespace() 84 defName := unstructuredDef.GetName() 85 defRevName := ConstructDefinitionRevisionName(defName, revisionName) 86 return true, types.NamespacedName{Name: defRevName, Namespace: defNs}, nil 87 } 88 89 func generateNamedDefinitionRevision(ctx context.Context, cli client.Client, def runtime.Object, defRevNamespacedName types.NamespacedName) (*v1beta1.DefinitionRevision, bool, error) { 90 oldDefRev := new(v1beta1.DefinitionRevision) 91 92 // definitionRevision is immutable, if the requested definitionRevision already exists, return directly. 93 err := cli.Get(ctx, defRevNamespacedName, oldDefRev) 94 if err == nil { 95 return oldDefRev, false, nil 96 } 97 98 if apierrors.IsNotFound(err) { 99 newDefRev, lastRevision, err := GatherRevisionInfo(def) 100 if err != nil { 101 return newDefRev, false, err 102 } 103 _, revNum := getDefNextRevision(newDefRev, lastRevision) 104 newDefRev.Name = defRevNamespacedName.Name 105 newDefRev.Spec.Revision = revNum 106 return newDefRev, true, nil 107 } 108 return nil, false, err 109 } 110 111 // GatherRevisionInfo gather revision information from definition 112 func GatherRevisionInfo(def runtime.Object) (*v1beta1.DefinitionRevision, *common.Revision, error) { 113 defRev := &v1beta1.DefinitionRevision{} 114 var LastRevision *common.Revision 115 switch definition := def.(type) { 116 case *v1beta1.ComponentDefinition: 117 copiedCompDef := definition.DeepCopy() 118 defRev.Spec.DefinitionType = common.ComponentType 119 defRev.Spec.ComponentDefinition = *copiedCompDef 120 LastRevision = copiedCompDef.Status.LatestRevision 121 case *v1beta1.TraitDefinition: 122 copiedTraitDef := definition.DeepCopy() 123 defRev.Spec.DefinitionType = common.TraitType 124 defRev.Spec.TraitDefinition = *copiedTraitDef 125 LastRevision = copiedTraitDef.Status.LatestRevision 126 case *v1beta1.PolicyDefinition: 127 defCopy := definition.DeepCopy() 128 defRev.Spec.DefinitionType = common.PolicyType 129 defRev.Spec.PolicyDefinition = *defCopy 130 LastRevision = defCopy.Status.LatestRevision 131 case *v1beta1.WorkflowStepDefinition: 132 defCopy := definition.DeepCopy() 133 defRev.Spec.DefinitionType = common.WorkflowStepType 134 defRev.Spec.WorkflowStepDefinition = *defCopy 135 LastRevision = defCopy.Status.LatestRevision 136 default: 137 return nil, nil, fmt.Errorf("unsupported type %v", definition) 138 } 139 140 defHash, err := computeDefinitionRevisionHash(defRev) 141 if err != nil { 142 return nil, nil, err 143 } 144 defRev.Spec.RevisionHash = defHash 145 return defRev, LastRevision, nil 146 } 147 148 func computeDefinitionRevisionHash(defRev *v1beta1.DefinitionRevision) (string, error) { 149 var defHash string 150 var err error 151 switch defRev.Spec.DefinitionType { 152 case common.ComponentType: 153 defHash, err = utils.ComputeSpecHash(&defRev.Spec.ComponentDefinition.Spec) 154 if err != nil { 155 return defHash, err 156 } 157 case common.TraitType: 158 defHash, err = utils.ComputeSpecHash(&defRev.Spec.TraitDefinition.Spec) 159 if err != nil { 160 return defHash, err 161 } 162 case common.PolicyType: 163 defHash, err = utils.ComputeSpecHash(&defRev.Spec.PolicyDefinition.Spec) 164 if err != nil { 165 return defHash, err 166 } 167 case common.WorkflowStepType: 168 defHash, err = utils.ComputeSpecHash(&defRev.Spec.WorkflowStepDefinition.Spec) 169 if err != nil { 170 return defHash, err 171 } 172 } 173 return defHash, nil 174 } 175 176 func compareWithLastDefRevisionSpec(ctx context.Context, cli client.Client, 177 newDefRev *v1beta1.DefinitionRevision, lastRevision *common.Revision) (bool, error) { 178 if lastRevision == nil { 179 return true, nil 180 } 181 if lastRevision.RevisionHash != newDefRev.Spec.RevisionHash { 182 return true, nil 183 } 184 185 // check if the DefinitionRevision is deep equal in Spec level 186 // get the last revision from K8s and double check 187 defRev := &v1beta1.DefinitionRevision{} 188 var namespace string 189 switch newDefRev.Spec.DefinitionType { 190 case common.ComponentType: 191 namespace = newDefRev.Spec.ComponentDefinition.Namespace 192 case common.TraitType: 193 namespace = newDefRev.Spec.TraitDefinition.Namespace 194 case common.PolicyType: 195 namespace = newDefRev.Spec.PolicyDefinition.Namespace 196 case common.WorkflowStepType: 197 namespace = newDefRev.Spec.WorkflowStepDefinition.Namespace 198 } 199 if err := cli.Get(ctx, client.ObjectKey{Name: lastRevision.Name, 200 Namespace: namespace}, defRev); err != nil { 201 return false, errors.Wrapf(err, "get the definitionRevision %s", lastRevision.Name) 202 } 203 204 if DeepEqualDefRevision(defRev, newDefRev) { 205 // No difference on spec, will not create a new revision 206 // align the name and resourceVersion 207 newDefRev.Name = defRev.Name 208 newDefRev.Spec.Revision = defRev.Spec.Revision 209 newDefRev.ResourceVersion = defRev.ResourceVersion 210 return false, nil 211 } 212 // if reach here, it's same hash but different spec 213 return true, nil 214 } 215 216 // DeepEqualDefRevision deep compare the spec of definitionRevisions 217 func DeepEqualDefRevision(old, new *v1beta1.DefinitionRevision) bool { 218 if !apiequality.Semantic.DeepEqual(old.Spec.ComponentDefinition.Spec, new.Spec.ComponentDefinition.Spec) { 219 return false 220 } 221 if !apiequality.Semantic.DeepEqual(old.Spec.TraitDefinition.Spec, new.Spec.TraitDefinition.Spec) { 222 return false 223 } 224 if !apiequality.Semantic.DeepEqual(old.Spec.PolicyDefinition.Spec, new.Spec.PolicyDefinition.Spec) { 225 return false 226 } 227 if !apiequality.Semantic.DeepEqual(old.Spec.WorkflowStepDefinition.Spec, new.Spec.WorkflowStepDefinition.Spec) { 228 return false 229 } 230 return true 231 } 232 233 func getDefNextRevision(defRev *v1beta1.DefinitionRevision, lastRevision *common.Revision) (string, int64) { 234 var nextRevision int64 = 1 235 if lastRevision != nil { 236 nextRevision = lastRevision.Revision + 1 237 } 238 var name string 239 switch defRev.Spec.DefinitionType { 240 case common.ComponentType: 241 name = defRev.Spec.ComponentDefinition.Name 242 case common.TraitType: 243 name = defRev.Spec.TraitDefinition.Name 244 case common.PolicyType: 245 name = defRev.Spec.PolicyDefinition.Name 246 case common.WorkflowStepType: 247 name = defRev.Spec.WorkflowStepDefinition.Name 248 } 249 defRevName := strings.Join([]string{name, fmt.Sprintf("v%d", nextRevision)}, "-") 250 return defRevName, nextRevision 251 } 252 253 // ConstructDefinitionRevisionName construct the name of DefinitionRevision. 254 func ConstructDefinitionRevisionName(definitionName, revision string) string { 255 return strings.Join([]string{definitionName, fmt.Sprintf("v%s", revision)}, "-") 256 } 257 258 // CleanUpDefinitionRevision check all definitionRevisions, remove them if the number of them exceed the limit 259 func CleanUpDefinitionRevision(ctx context.Context, cli client.Client, def runtime.Object, revisionLimit int) error { 260 var listOpts []client.ListOption 261 var usingRevision *common.Revision 262 263 switch definition := def.(type) { 264 case *v1beta1.ComponentDefinition: 265 listOpts = []client.ListOption{ 266 client.InNamespace(definition.Namespace), 267 client.MatchingLabels{oam.LabelComponentDefinitionName: definition.Name}, 268 } 269 usingRevision = definition.Status.LatestRevision 270 case *v1beta1.TraitDefinition: 271 listOpts = []client.ListOption{ 272 client.InNamespace(definition.Namespace), 273 client.MatchingLabels{oam.LabelTraitDefinitionName: definition.Name}, 274 } 275 usingRevision = definition.Status.LatestRevision 276 case *v1beta1.PolicyDefinition: 277 listOpts = []client.ListOption{ 278 client.InNamespace(definition.Namespace), 279 client.MatchingLabels{oam.LabelPolicyDefinitionName: definition.Name}, 280 } 281 usingRevision = definition.Status.LatestRevision 282 case *v1beta1.WorkflowStepDefinition: 283 listOpts = []client.ListOption{ 284 client.InNamespace(definition.Namespace), 285 client.MatchingLabels{oam.LabelWorkflowStepDefinitionName: definition.Name}} 286 usingRevision = definition.Status.LatestRevision 287 } 288 289 if usingRevision == nil { 290 return nil 291 } 292 293 defRevList := new(v1beta1.DefinitionRevisionList) 294 if err := cli.List(ctx, defRevList, listOpts...); err != nil { 295 return err 296 } 297 needKill := len(defRevList.Items) - revisionLimit - 1 298 if needKill <= 0 { 299 return nil 300 } 301 klog.InfoS("cleanup old definitionRevision", "needKillNum", needKill) 302 303 sortedRevision := defRevList.Items 304 sort.Sort(historiesByRevision(sortedRevision)) 305 306 for _, rev := range sortedRevision { 307 if needKill <= 0 { 308 break 309 } 310 if rev.Name == usingRevision.Name { 311 continue 312 } 313 if err := cli.Delete(ctx, rev.DeepCopy()); err != nil && !apierrors.IsNotFound(err) { 314 return err 315 } 316 needKill-- 317 } 318 return nil 319 } 320 321 type historiesByRevision []v1beta1.DefinitionRevision 322 323 func (h historiesByRevision) Len() int { return len(h) } 324 func (h historiesByRevision) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 325 func (h historiesByRevision) Less(i, j int) bool { 326 return h[i].Spec.Revision < h[j].Spec.Revision 327 } 328 329 // ReconcileDefinitionRevision generate the definition revision and update it. 330 func ReconcileDefinitionRevision(ctx context.Context, 331 cli client.Client, 332 record event.Recorder, 333 definition util.ConditionedObject, 334 revisionLimit int, 335 updateLatestRevision func(*common.Revision) error, 336 ) (*v1beta1.DefinitionRevision, *ctrl.Result, error) { 337 338 // generate DefinitionRevision from componentDefinition 339 defRev, isNewRevision, err := GenerateDefinitionRevision(ctx, cli, definition) 340 if err != nil { 341 klog.ErrorS(err, "Could not generate DefinitionRevision", "componentDefinition", klog.KObj(definition)) 342 record.Event(definition, event.Warning("Could not generate DefinitionRevision", err)) 343 return nil, &ctrl.Result{}, util.PatchCondition(ctx, cli, definition, 344 condition.ReconcileError(fmt.Errorf(util.ErrGenerateDefinitionRevision, definition.GetName(), err))) 345 } 346 347 if isNewRevision { 348 if err := CreateDefinitionRevision(ctx, cli, definition, defRev.DeepCopy()); err != nil { 349 klog.ErrorS(err, "Could not create DefinitionRevision") 350 record.Event(definition, event.Warning("cannot create DefinitionRevision", err)) 351 return nil, &ctrl.Result{}, util.PatchCondition(ctx, cli, definition, 352 condition.ReconcileError(fmt.Errorf(util.ErrCreateDefinitionRevision, defRev.Name, err))) 353 } 354 klog.InfoS("Successfully created definitionRevision", "definitionRevision", klog.KObj(defRev)) 355 356 if err := updateLatestRevision(&common.Revision{ 357 Name: defRev.Name, 358 Revision: defRev.Spec.Revision, 359 RevisionHash: defRev.Spec.RevisionHash, 360 }); err != nil { 361 klog.ErrorS(err, "Could not update Definition Status") 362 record.Event(definition, event.Warning("cannot update the definition status", err)) 363 return nil, &ctrl.Result{}, util.PatchCondition(ctx, cli, definition, 364 condition.ReconcileError(fmt.Errorf(util.ErrUpdateComponentDefinition, definition.GetName(), err))) 365 } 366 klog.InfoS("Successfully updated the status.latestRevision of the definition", "Definition", klog.KRef(definition.GetNamespace(), definition.GetName()), 367 "Name", defRev.Name, "Revision", defRev.Spec.Revision, "RevisionHash", defRev.Spec.RevisionHash) 368 } 369 370 if err = CleanUpDefinitionRevision(ctx, cli, definition, revisionLimit); err != nil { 371 klog.InfoS("Failed to collect garbage", "err", err) 372 record.Event(definition, event.Warning("failed to garbage collect DefinitionRevision of type ComponentDefinition", err)) 373 } 374 return defRev, nil, nil 375 } 376 377 // CreateDefinitionRevision create the revision of the definition 378 func CreateDefinitionRevision(ctx context.Context, cli client.Client, def util.ConditionedObject, defRev *v1beta1.DefinitionRevision) error { 379 namespace := def.GetNamespace() 380 defRev.SetLabels(def.GetLabels()) 381 382 var labelKey string 383 switch def.(type) { 384 case *v1beta1.ComponentDefinition: 385 labelKey = oam.LabelComponentDefinitionName 386 case *v1beta1.TraitDefinition: 387 labelKey = oam.LabelTraitDefinitionName 388 case *v1beta1.PolicyDefinition: 389 labelKey = oam.LabelPolicyDefinitionName 390 case *v1beta1.WorkflowStepDefinition: 391 labelKey = oam.LabelWorkflowStepDefinitionName 392 } 393 if labelKey != "" { 394 defRev.SetLabels(util.MergeMapOverrideWithDst(defRev.Labels, map[string]string{labelKey: def.GetName()})) 395 } else { 396 defRev.SetLabels(defRev.Labels) 397 } 398 399 defRev.SetNamespace(namespace) 400 401 rev := &v1beta1.DefinitionRevision{} 402 err := cli.Get(ctx, client.ObjectKey{Namespace: namespace, Name: defRev.Name}, rev) 403 if apierrors.IsNotFound(err) { 404 err = cli.Create(ctx, defRev) 405 if apierrors.IsAlreadyExists(err) { 406 return nil 407 } 408 } 409 return err 410 }