mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-27 16:17:34 +00:00
294 lines
11 KiB
Go
294 lines
11 KiB
Go
/*
|
|
Copyright 2021 The KubeVela Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package application
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/pkg/errors"
|
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/utils/pointer"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
|
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
|
"github.com/oam-dev/kubevela/pkg/oam"
|
|
"github.com/oam-dev/kubevela/pkg/oam/util"
|
|
)
|
|
|
|
// AppRevisionHash is used to compute the hash value of the AppRevision
|
|
type AppRevisionHash struct {
|
|
ApplicationSpecHash string
|
|
WorkloadDefinitionHash map[string]string
|
|
ComponentDefinitionHash map[string]string
|
|
TraitDefinitionHash map[string]string
|
|
ScopeDefinitionHash map[string]string
|
|
}
|
|
|
|
// UpdateRevisionStatus will update the status of Application object mainly for update the revision part
|
|
func (h *appHandler) UpdateRevisionStatus(ctx context.Context, revName, hash string, revision int64) error {
|
|
h.app.Status.LatestRevision = &common.Revision{
|
|
Name: revName,
|
|
Revision: revision,
|
|
RevisionHash: hash,
|
|
}
|
|
// make sure that we persist the latest revision first
|
|
if err := h.r.UpdateStatus(ctx, h.app); err != nil {
|
|
h.logger.Error(err, "update the latest appConfig revision to status", "application name", h.app.GetName(),
|
|
"latest revision", revName)
|
|
return err
|
|
}
|
|
h.logger.Info("recorded the latest appConfig revision", "application name", h.app.GetName(),
|
|
"latest revision", revName)
|
|
return nil
|
|
}
|
|
|
|
// setRevisionMetadata will set the ApplicationRevision with the same annotation/label as the app
|
|
func (h *appHandler) setRevisionMetadata(appRev *v1beta1.ApplicationRevision) {
|
|
appRev.Namespace = h.app.Namespace
|
|
appRev.SetAnnotations(h.app.GetAnnotations())
|
|
appRev.SetLabels(h.app.GetLabels())
|
|
util.AddLabels(appRev, map[string]string{oam.LabelAppRevisionHash: h.revisionHash})
|
|
appRev.SetOwnerReferences([]metav1.OwnerReference{{
|
|
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
|
Kind: v1beta1.ApplicationKind,
|
|
Name: h.app.Name,
|
|
UID: h.app.UID,
|
|
Controller: pointer.BoolPtr(false),
|
|
}})
|
|
}
|
|
|
|
// setRevisionWithRenderedResult will set the ApplicationRevision with the rendered result
|
|
// it's ApplicationConfiguration and Component for now
|
|
func (h *appHandler) setRevisionWithRenderedResult(appRev *v1beta1.ApplicationRevision, ac *v1alpha2.ApplicationConfiguration,
|
|
comps []*v1alpha2.Component) {
|
|
appRev.Spec.Components = ConvertComponent2RawRevision(comps)
|
|
appRev.Spec.ApplicationConfiguration = util.Object2RawExtension(ac)
|
|
}
|
|
|
|
// gatherRevisionSpec will gather all revision spec withouth metadata and rendered result.
|
|
// the gathered Revision spec will be enough to calculate the hash and compare with the old revision
|
|
func (h *appHandler) gatherRevisionSpec() (*v1beta1.ApplicationRevision, string, error) {
|
|
copiedApp := h.app.DeepCopy()
|
|
// We better to remove all object status in the appRevision
|
|
copiedApp.Status = common.AppStatus{}
|
|
appRev := &v1beta1.ApplicationRevision{
|
|
Spec: v1beta1.ApplicationRevisionSpec{
|
|
Application: *copiedApp,
|
|
ComponentDefinitions: make(map[string]v1beta1.ComponentDefinition),
|
|
WorkloadDefinitions: make(map[string]v1beta1.WorkloadDefinition),
|
|
TraitDefinitions: make(map[string]v1beta1.TraitDefinition),
|
|
ScopeDefinitions: make(map[string]v1beta1.ScopeDefinition),
|
|
},
|
|
}
|
|
for _, w := range h.appfile.Workloads {
|
|
if w == nil {
|
|
continue
|
|
}
|
|
if w.FullTemplate.ComponentDefinition != nil {
|
|
cd := w.FullTemplate.ComponentDefinition.DeepCopy()
|
|
cd.Status = v1beta1.ComponentDefinitionStatus{}
|
|
appRev.Spec.ComponentDefinitions[w.FullTemplate.ComponentDefinition.Name] = *cd
|
|
}
|
|
if w.FullTemplate.WorkloadDefinition != nil {
|
|
wd := w.FullTemplate.WorkloadDefinition.DeepCopy()
|
|
wd.Status = v1beta1.WorkloadDefinitionStatus{}
|
|
appRev.Spec.WorkloadDefinitions[w.FullTemplate.WorkloadDefinition.Name] = *wd
|
|
}
|
|
for _, t := range w.Traits {
|
|
if t == nil {
|
|
continue
|
|
}
|
|
if t.FullTemplate.TraitDefinition != nil {
|
|
td := t.FullTemplate.TraitDefinition.DeepCopy()
|
|
td.Status = v1beta1.TraitDefinitionStatus{}
|
|
appRev.Spec.TraitDefinitions[t.FullTemplate.TraitDefinition.Name] = *td
|
|
}
|
|
}
|
|
// TODO(wonderflow): take scope into the revision
|
|
}
|
|
appRevisionHash, err := ComputeAppRevisionHash(appRev)
|
|
if err != nil {
|
|
h.logger.Error(err, "compute hash of appRevision for application", "application name", h.app.GetName())
|
|
return appRev, "", err
|
|
}
|
|
return appRev, appRevisionHash, nil
|
|
}
|
|
|
|
// compareWithLastRevisionSpec will get the last AppRevision from K8s and compare the Application and Definition's Spec
|
|
func (h *appHandler) compareWithLastRevisionSpec(ctx context.Context, newAppRevisionHash string, newAppRevision *v1beta1.ApplicationRevision) (bool, error) {
|
|
|
|
// the last revision doesn't exist.
|
|
if h.app.Status.LatestRevision == nil {
|
|
return true, nil
|
|
}
|
|
// the hash value doesn't align
|
|
if h.app.Status.LatestRevision.RevisionHash != newAppRevisionHash {
|
|
return true, nil
|
|
}
|
|
|
|
// check if the appRevision is deep equal in Spec level
|
|
// get the last revision from K8s and double check
|
|
lastAppRevision := &v1beta1.ApplicationRevision{}
|
|
if err := h.r.Get(ctx, client.ObjectKey{Name: h.app.Status.LatestRevision.Name,
|
|
Namespace: h.app.Namespace}, lastAppRevision); err != nil {
|
|
h.logger.Error(err, "get the last appRevision from K8s", "application name",
|
|
h.app.GetName(), "revision", h.app.Status.LatestRevision.Name)
|
|
return false, errors.Wrapf(err, "fail to get applicationRevision %s", h.app.Status.LatestRevision.Name)
|
|
}
|
|
if DeepEqualRevision(lastAppRevision, newAppRevision) {
|
|
// No difference on spec, will not create a new revision
|
|
// align the name and resourceVersion
|
|
newAppRevision.Name = lastAppRevision.Name
|
|
newAppRevision.ResourceVersion = lastAppRevision.ResourceVersion
|
|
return false, nil
|
|
}
|
|
// if reach here, it's same hash but different spec
|
|
return true, nil
|
|
}
|
|
|
|
// GenerateAppRevision will generate a pure revision without metadata and rendered result
|
|
// the generated revision will be compare with the last revision to see if there's any difference.
|
|
func (h *appHandler) GenerateAppRevision(ctx context.Context) (*v1beta1.ApplicationRevision, error) {
|
|
appRev, appRevisionHash, err := h.gatherRevisionSpec()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isNewRev, err := h.compareWithLastRevisionSpec(ctx, appRevisionHash, appRev)
|
|
if err != nil {
|
|
return appRev, err
|
|
}
|
|
if isNewRev {
|
|
appRev.Name, _ = utils.GetAppNextRevision(h.app)
|
|
}
|
|
h.isNewRevision = isNewRev
|
|
h.revisionHash = appRevisionHash
|
|
return appRev, nil
|
|
}
|
|
|
|
// FinalizeAppRevision will finalize the AppRevision with metadata and rendered result revision for an Application when created/updated
|
|
func (h *appHandler) FinalizeAppRevision(appRev *v1beta1.ApplicationRevision,
|
|
ac *v1alpha2.ApplicationConfiguration, comps []*v1alpha2.Component) {
|
|
|
|
h.setRevisionMetadata(appRev)
|
|
h.setRevisionWithRenderedResult(appRev, ac, comps)
|
|
|
|
}
|
|
|
|
// ConvertComponent2RawRevision convert to ComponentMap
|
|
func ConvertComponent2RawRevision(comps []*v1alpha2.Component) []common.RawComponent {
|
|
var objs []common.RawComponent
|
|
for _, comp := range comps {
|
|
obj := comp.DeepCopy()
|
|
objs = append(objs, common.RawComponent{
|
|
Raw: util.Object2RawExtension(obj),
|
|
})
|
|
}
|
|
return objs
|
|
}
|
|
|
|
// DeepEqualRevision will compare the spec of Application and Definition to see if the Application is the same revision
|
|
// Spec of AC and Component will not be compared as they are generated by the application and definitions
|
|
// Note the Spec compare can only work when the RawExtension are decoded well in the RawExtension.Object instead of in RawExtension.Raw(bytes)
|
|
func DeepEqualRevision(old, new *v1beta1.ApplicationRevision) bool {
|
|
if len(old.Spec.WorkloadDefinitions) != len(new.Spec.WorkloadDefinitions) {
|
|
return false
|
|
}
|
|
if len(old.Spec.TraitDefinitions) != len(new.Spec.TraitDefinitions) {
|
|
return false
|
|
}
|
|
if len(old.Spec.ComponentDefinitions) != len(new.Spec.ComponentDefinitions) {
|
|
return false
|
|
}
|
|
if len(old.Spec.ScopeDefinitions) != len(new.Spec.ScopeDefinitions) {
|
|
return false
|
|
}
|
|
for key, wd := range new.Spec.WorkloadDefinitions {
|
|
if !apiequality.Semantic.DeepEqual(old.Spec.WorkloadDefinitions[key].Spec, wd.Spec) {
|
|
return false
|
|
}
|
|
}
|
|
for key, cd := range new.Spec.ComponentDefinitions {
|
|
if !apiequality.Semantic.DeepEqual(old.Spec.ComponentDefinitions[key].Spec, cd.Spec) {
|
|
return false
|
|
}
|
|
}
|
|
for key, td := range new.Spec.TraitDefinitions {
|
|
if !apiequality.Semantic.DeepEqual(old.Spec.TraitDefinitions[key].Spec, td.Spec) {
|
|
return false
|
|
}
|
|
}
|
|
for key, sd := range new.Spec.ScopeDefinitions {
|
|
if !apiequality.Semantic.DeepEqual(old.Spec.ScopeDefinitions[key].Spec, sd.Spec) {
|
|
return false
|
|
}
|
|
}
|
|
return apiequality.Semantic.DeepEqual(&old.Spec.Application.Spec, &new.Spec.Application.Spec)
|
|
}
|
|
|
|
// ComputeAppRevisionHash computes a single hash value for an appRevision object
|
|
// Spec of Application/WorkloadDefinitions/ComponentDefinitions/TraitDefinitions/ScopeDefinitions will be taken into compute
|
|
func ComputeAppRevisionHash(appRevision *v1beta1.ApplicationRevision) (string, error) {
|
|
// we first constructs a AppRevisionHash structure to store all the meaningful spec hashes
|
|
// and avoid computing the annotations. Those fields are all read from k8s already so their
|
|
// raw extension value are already byte array. Never include any in-memory objects.
|
|
appRevisionHash := AppRevisionHash{
|
|
WorkloadDefinitionHash: make(map[string]string),
|
|
ComponentDefinitionHash: make(map[string]string),
|
|
TraitDefinitionHash: make(map[string]string),
|
|
ScopeDefinitionHash: make(map[string]string),
|
|
}
|
|
var err error
|
|
appRevisionHash.ApplicationSpecHash, err = utils.ComputeSpecHash(&appRevision.Spec.Application.Spec)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for key, wd := range appRevision.Spec.WorkloadDefinitions {
|
|
hash, err := utils.ComputeSpecHash(&wd.Spec)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
appRevisionHash.WorkloadDefinitionHash[key] = hash
|
|
}
|
|
for key, cd := range appRevision.Spec.ComponentDefinitions {
|
|
hash, err := utils.ComputeSpecHash(&cd.Spec)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
appRevisionHash.ComponentDefinitionHash[key] = hash
|
|
}
|
|
for key, td := range appRevision.Spec.TraitDefinitions {
|
|
hash, err := utils.ComputeSpecHash(&td.Spec)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
appRevisionHash.TraitDefinitionHash[key] = hash
|
|
}
|
|
for key, sd := range appRevision.Spec.ScopeDefinitions {
|
|
hash, err := utils.ComputeSpecHash(&sd.Spec)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
appRevisionHash.ScopeDefinitionHash[key] = hash
|
|
}
|
|
// compute the hash of the entire structure
|
|
return utils.ComputeSpecHash(&appRevisionHash)
|
|
}
|