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  }