Files
kubevela/pkg/controller/core.oam.dev/v1alpha2/application/apply.go
2021-05-31 18:44:15 +08:00

850 lines
30 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"
"fmt"
"strconv"
"strings"
"time"
runtimev1alpha1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/logging"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/go-logr/logr"
terraformtypes "github.com/oam-dev/terraform-controller/api/types"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
ctypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/klog/v2"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"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/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/applicationrollout"
"github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/dsl/process"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
func errorCondition(tpy string, err error) runtimev1alpha1.Condition {
return runtimev1alpha1.Condition{
Type: runtimev1alpha1.ConditionType(tpy),
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.NewTime(time.Now()),
Reason: runtimev1alpha1.ReasonReconcileError,
Message: err.Error(),
}
}
func readyCondition(tpy string) runtimev1alpha1.Condition {
return runtimev1alpha1.Condition{
Type: runtimev1alpha1.ConditionType(tpy),
Status: corev1.ConditionTrue,
Reason: runtimev1alpha1.ReasonAvailable,
LastTransitionTime: metav1.NewTime(time.Now()),
}
}
type appHandler struct {
r *Reconciler
app *v1beta1.Application
appfile *appfile.Appfile
logger logr.Logger
inplace bool
isNewRevision bool
revisionHash string
acrossNamespaceResources []v1beta1.TypedReference
resourceTracker *v1beta1.ResourceTracker
autodetect bool
}
// setInplace will mark if the application should upgrade the workload within the same instance(name never changed)
func (h *appHandler) setInplace(isInplace bool) {
h.inplace = isInplace
}
func (h *appHandler) handleErr(err error) (ctrl.Result, error) {
nerr := h.r.UpdateStatus(context.Background(), h.app)
if err == nil && nerr == nil {
return ctrl.Result{}, nil
}
if nerr != nil {
h.logger.Error(nerr, "[Update] application status")
}
return ctrl.Result{
RequeueAfter: time.Second * 10,
}, nil
}
// apply will
// 1. set ownerReference for ApplicationConfiguration and Components
// 2. update AC's components using the component revision name
// 3. update or create the AC with new revision and remember it in the application status
// 4. garbage collect unused components
func (h *appHandler) apply(ctx context.Context, appRev *v1beta1.ApplicationRevision, ac *v1alpha2.ApplicationConfiguration, comps []*v1alpha2.Component) error {
owners := []metav1.OwnerReference{{
APIVersion: v1beta1.SchemeGroupVersion.String(),
Kind: v1beta1.ApplicationKind,
Name: h.app.Name,
UID: h.app.UID,
Controller: pointer.BoolPtr(true),
}}
if _, exist := h.app.GetAnnotations()[oam.AnnotationAppRollout]; !exist && h.app.Spec.RolloutPlan == nil {
h.setInplace(true)
} else {
h.setInplace(false)
}
// don't create components and AC if revision-only annotation is set
if ac.Annotations[oam.AnnotationAppRevisionOnly] == "true" {
h.FinalizeAppRevision(appRev, ac, comps)
return h.createOrUpdateAppRevision(ctx, appRev)
}
var needTracker bool
var err error
for _, comp := range comps {
comp.SetOwnerReferences(owners)
// If the helm mode component doesn't specify the workload
// we just install a helm chart resources
if h.checkAutoDetect(comp) {
if err = h.applyHelmModuleResources(ctx, comp, owners); err != nil {
return errors.Wrap(err, "cannot apply Helm module resources")
}
continue
}
needTracker, err = h.checkAndSetResourceTracker(&comp.Spec.Workload)
if err != nil {
return err
}
newComp := comp.DeepCopy()
// newComp will be updated and return the revision name instead of the component name
revisionName, err := h.createOrUpdateComponent(ctx, newComp)
if err != nil {
return err
}
if needTracker {
if err := h.recodeTrackedWorkload(comp, revisionName); err != nil {
return err
}
}
// find the ACC that contains this component
for i := 0; i < len(ac.Spec.Components); i++ {
// update the AC using the component revision instead of component name
// we have to make AC immutable including the component it's pointing to
if ac.Spec.Components[i].ComponentName == newComp.Name {
ac.Spec.Components[i].RevisionName = revisionName
ac.Spec.Components[i].ComponentName = ""
if err := h.checkResourceTrackerForTrait(ctx, ac.Spec.Components[i], newComp.Name); err != nil {
return err
}
}
}
// isNewRevision indicates app's newly created or spec has changed
// skip applying helm resources if no spec change
if h.isNewRevision && comp.Spec.Helm != nil {
if err = h.applyHelmModuleResources(ctx, comp, owners); err != nil {
return errors.Wrap(err, "cannot apply Helm module resources")
}
}
}
ac.SetOwnerReferences(owners)
h.FinalizeAppRevision(appRev, ac, comps)
if h.autodetect {
// TODO(yangsoon) autodetect is temporarily not implemented
return fmt.Errorf("helm mode component doesn't specify workload, the traits attached to the helm mode component will fail to work")
}
if err := h.createOrUpdateAppRevision(ctx, appRev); err != nil {
return err
}
// `h.inplace`: the rollout will create AppContext which will launch the real K8s resources.
// Otherwise, we should create/update the appContext here when there if no rollout controller to take care of new versions
// In this case, the workload should update with the annotation `app.oam.dev/inplace-upgrade=true`
// `!h.autodetect`: If the workload type of the helm mode component is not clear, an autodetect type workload will be specified by default
// In this case, the traits attached to the helm mode component will fail to generate,
// so we only call applyHelmModuleResources to create the helm resource, don't generate ApplicationContext.
if h.inplace && !h.autodetect {
return h.createOrUpdateAppContext(ctx, owners)
}
return nil
}
func (h *appHandler) createOrUpdateAppRevision(ctx context.Context, appRev *v1beta1.ApplicationRevision) error {
if appRev.Labels == nil {
appRev.Labels = make(map[string]string)
}
appRev.SetLabels(oamutil.MergeMapOverrideWithDst(appRev.Labels, map[string]string{oam.LabelAppName: h.app.Name}))
if h.isNewRevision {
var revisionNum int64
appRev.Name, revisionNum = utils.GetAppNextRevision(h.app)
// only new revision update the status
if err := h.UpdateRevisionStatus(ctx, appRev.Name, h.revisionHash, revisionNum); err != nil {
return err
}
return h.r.Create(ctx, appRev)
}
return h.r.Update(ctx, appRev)
}
func (h *appHandler) statusAggregate(appFile *appfile.Appfile) ([]common.ApplicationComponentStatus, bool, error) {
var appStatus []common.ApplicationComponentStatus
var healthy = true
for _, wl := range appFile.Workloads {
var status = common.ApplicationComponentStatus{
Name: wl.Name,
WorkloadDefinition: wl.FullTemplate.Reference.Definition,
Healthy: true,
}
var (
outputSecretName string
err error
pCtx process.Context
)
if wl.IsCloudResourceProducer() {
outputSecretName, err = appfile.GetOutputSecretNames(wl)
if err != nil {
return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, setting outputSecretName error", appFile.Name, wl.Name)
}
pCtx.InsertSecrets(outputSecretName, wl.RequiredSecrets)
}
switch wl.CapabilityCategory {
case types.TerraformCategory:
pCtx = appfile.NewBasicContext(wl, appFile.Name, appFile.RevisionName, appFile.Namespace)
ctx := context.Background()
var configuration terraformapi.Configuration
if err := h.r.Client.Get(ctx, client.ObjectKey{Name: wl.Name, Namespace: h.app.Namespace}, &configuration); err != nil {
return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, check health error", appFile.Name, wl.Name)
}
if configuration.Status.State != terraformtypes.Available {
healthy = false
status.Healthy = false
} else {
status.Healthy = true
}
status.Message = configuration.Status.Message
default:
pCtx = process.NewContext(h.app.Namespace, wl.Name, appFile.Name, appFile.RevisionName)
if err := wl.EvalContext(pCtx); err != nil {
return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, evaluate context error", appFile.Name, wl.Name)
}
workloadHealth, err := wl.EvalHealth(pCtx, h.r, h.app.Namespace)
if err != nil {
return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, check health error", appFile.Name, wl.Name)
}
if !workloadHealth {
// TODO(wonderflow): we should add a custom way to let the template say why it's unhealthy, only a bool flag is not enough
status.Healthy = false
healthy = false
}
status.Message, err = wl.EvalStatus(pCtx, h.r, h.app.Namespace)
if err != nil {
return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, evaluate workload status message error", appFile.Name, wl.Name)
}
}
var traitStatusList []common.ApplicationTraitStatus
for _, tr := range wl.Traits {
if err := tr.EvalContext(pCtx); err != nil {
return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, evaluate context error", appFile.Name, wl.Name, tr.Name)
}
var traitStatus = common.ApplicationTraitStatus{
Type: tr.Name,
Healthy: true,
}
traitHealth, err := tr.EvalHealth(pCtx, h.r, h.app.Namespace)
if err != nil {
return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, check health error", appFile.Name, wl.Name, tr.Name)
}
if !traitHealth {
// TODO(wonderflow): we should add a custom way to let the template say why it's unhealthy, only a bool flag is not enough
traitStatus.Healthy = false
healthy = false
}
traitStatus.Message, err = tr.EvalStatus(pCtx, h.r, h.app.Namespace)
if err != nil {
return nil, false, errors.WithMessagef(err, "app=%s, comp=%s, trait=%s, evaluate status message error", appFile.Name, wl.Name, tr.Name)
}
traitStatusList = append(traitStatusList, traitStatus)
}
status.Traits = traitStatusList
status.Scopes = generateScopeReference(wl.Scopes)
appStatus = append(appStatus, status)
}
return appStatus, healthy, nil
}
// createOrUpdateComponent creates a component if not exist and update if exists.
// it returns the corresponding component revisionName and if a new component revision is created
func (h *appHandler) createOrUpdateComponent(ctx context.Context, comp *v1alpha2.Component) (string, error) {
curComp := v1alpha2.Component{}
var preRevisionName, curRevisionName string
compName := comp.GetName()
compNameSpace := comp.GetNamespace()
compKey := ctypes.NamespacedName{Name: compName, Namespace: compNameSpace}
err := h.r.Get(ctx, compKey, &curComp)
if err != nil {
if !apierrors.IsNotFound(err) {
return "", err
}
if err = h.r.Create(ctx, comp); err != nil {
return "", err
}
h.logger.Info("Created a new component", "component name", comp.GetName())
} else {
// remember the revision if there is a previous component
if curComp.Status.LatestRevision != nil {
preRevisionName = curComp.Status.LatestRevision.Name
}
comp.ResourceVersion = curComp.ResourceVersion
if err := h.r.Update(ctx, comp); err != nil {
return "", err
}
h.logger.Info("Updated a component", "component name", comp.GetName())
}
// remove the object from the raw extension before we can compare with the existing componentRevision whose
// object is persisted as Raw data after going through api server
updatedComp := comp.DeepCopy()
updatedComp.Spec.Workload.Object = nil
if updatedComp.Spec.Helm != nil {
updatedComp.Spec.Helm.Release.Object = nil
updatedComp.Spec.Helm.Repository.Object = nil
}
if len(preRevisionName) != 0 {
needNewRevision, err := utils.CompareWithRevision(ctx, h.r,
logging.NewLogrLogger(h.logger), compName, compNameSpace, preRevisionName, &updatedComp.Spec)
if err != nil {
return "", errors.Wrap(err, fmt.Sprintf("compare with existing controllerRevision %s failed",
preRevisionName))
}
if !needNewRevision {
h.logger.Info("no need to wait for a new component revision", "component name", updatedComp.GetName(),
"revision", preRevisionName)
return preRevisionName, nil
}
}
h.logger.Info("wait for a new component revision", "component name", compName,
"previous revision", preRevisionName)
// get the new component revision that contains the component with retry
checkForRevision := func() (bool, error) {
if err := h.r.Get(ctx, compKey, &curComp); err != nil {
// retry no matter what
// nolint:nilerr
return false, nil
}
if curComp.Status.LatestRevision == nil || curComp.Status.LatestRevision.Name == preRevisionName {
return false, nil
}
needNewRevision, err := utils.CompareWithRevision(ctx, h.r, logging.NewLogrLogger(h.logger), compName,
compNameSpace, curComp.Status.LatestRevision.Name, &updatedComp.Spec)
if err != nil {
// retry no matter what
// nolint:nilerr
return false, nil
}
// end the loop if we find the revision
if !needNewRevision {
curRevisionName = curComp.Status.LatestRevision.Name
h.logger.Info("get a matching component revision", "component name", compName,
"current revision", curRevisionName)
}
return !needNewRevision, nil
}
if err := wait.ExponentialBackoff(utils.DefaultBackoff, checkForRevision); err != nil {
return "", err
}
return curRevisionName, nil
}
// createOrUpdateAppContext will make sure the appContext points to the latest application revision
// this will only be called in the case of no rollout,
func (h *appHandler) createOrUpdateAppContext(ctx context.Context, owners []metav1.OwnerReference) error {
var curAppContext v1alpha2.ApplicationContext
// AC name is the same as the app name if there is no rollout
appContext := v1alpha2.ApplicationContext{
ObjectMeta: metav1.ObjectMeta{
Name: h.app.Name,
Namespace: h.app.Namespace,
},
Spec: v1alpha2.ApplicationContextSpec{
// new AC always point to the latest app revision
ApplicationRevisionName: h.app.Status.LatestRevision.Name,
},
}
appContext.SetOwnerReferences(owners)
// set the AC label and annotation
appLabel := h.app.GetLabels()
if appLabel == nil {
appLabel = make(map[string]string)
}
appLabel[oam.LabelAppRevisionHash] = h.app.Status.LatestRevision.RevisionHash
appContext.SetLabels(appLabel)
appAnnotation := h.app.GetAnnotations()
if appAnnotation == nil {
appAnnotation = make(map[string]string)
}
appAnnotation[oam.AnnotationInplaceUpgrade] = strconv.FormatBool(h.inplace)
appContext.SetAnnotations(appAnnotation)
key := ctypes.NamespacedName{Name: appContext.Name, Namespace: appContext.Namespace}
if err := h.r.Get(ctx, key, &curAppContext); err != nil {
if !apierrors.IsNotFound(err) {
return err
}
klog.InfoS("create a new appContext", "application name",
appContext.GetName(), "revision it points to", appContext.Spec.ApplicationRevisionName)
return h.r.Create(ctx, &appContext)
}
// we don't need to create another appConfig
klog.InfoS("replace the existing appContext", "application name", appContext.GetName(),
"revision it points to", appContext.Spec.ApplicationRevisionName)
appContext.ResourceVersion = curAppContext.ResourceVersion
return h.r.Update(ctx, &appContext)
}
func (h *appHandler) applyHelmModuleResources(ctx context.Context, comp *v1alpha2.Component, owners []metav1.OwnerReference) error {
klog.Info("Process a Helm module component")
repo, err := oamutil.RawExtension2Unstructured(&comp.Spec.Helm.Repository)
if err != nil {
return err
}
release, err := oamutil.RawExtension2Unstructured(&comp.Spec.Helm.Release)
if err != nil {
return err
}
release.SetOwnerReferences(owners)
repo.SetOwnerReferences(owners)
if err := h.r.applicator.Apply(ctx, repo); err != nil {
return err
}
klog.InfoS("Apply a HelmRepository", "namespace", repo.GetNamespace(), "name", repo.GetName())
if err := h.r.applicator.Apply(ctx, release); err != nil {
return err
}
klog.InfoS("Apply a HelmRelease", "namespace", release.GetNamespace(), "name", release.GetName())
return nil
}
// checkAndSetResourceTracker check if resource's namespace is different with application, if yes set resourceTracker as
// resource's ownerReference
func (h *appHandler) checkAndSetResourceTracker(resource *runtime.RawExtension) (bool, error) {
needTracker := false
u, err := oamutil.RawExtension2Unstructured(resource)
if err != nil {
return false, err
}
inDiffNamespace, err := h.checkCrossNamespace(u)
if err != nil {
return false, err
}
if inDiffNamespace {
needTracker = true
ref := h.genResourceTrackerOwnerReference()
// set resourceTracker as the ownerReference of workload/trait
u.SetOwnerReferences([]metav1.OwnerReference{*ref})
raw := oamutil.Object2RawExtension(u)
*resource = raw
return needTracker, nil
}
return needTracker, nil
}
func (h *appHandler) checkAutoDetect(component *v1alpha2.Component) bool {
if len(component.Spec.Workload.Raw) == 0 && component.Spec.Workload.Object == nil && component.Spec.Helm != nil {
h.autodetect = true
return true
}
return false
}
// genResourceTrackerOwnerReference check the related resourceTracker whether have been created.
// If not, create it. And return the ownerReference of this resourceTracker.
func (h *appHandler) genResourceTrackerOwnerReference() *metav1.OwnerReference {
return metav1.NewControllerRef(h.resourceTracker, v1beta1.ResourceTrackerKindVersionKind)
}
func (h *appHandler) generateResourceTrackerName() string {
return fmt.Sprintf("%s-%s", h.app.Namespace, h.app.Name)
}
// return true if the resource is cluser-scoped or is not in the same namespace
// with application
func (h *appHandler) checkCrossNamespace(u *unstructured.Unstructured) (bool, error) {
gk := u.GetObjectKind().GroupVersionKind().GroupKind()
isNamespacedScope, err := discoverymapper.IsNamespacedScope(h.r.dm, gk)
if err != nil {
return false, err
}
if !isNamespacedScope {
// it's cluster-scoped resource
return true, nil
}
// for a namespace-scoped resource, if its namespace is empty,
// we will set application's namespace to it latter,
// so only check non-empty namespace here
return len(u.GetNamespace()) != 0 && u.GetNamespace() != h.app.Namespace, nil
}
// finalizeResourceTracker func return whether need to update application
func (h *appHandler) removeResourceTracker(ctx context.Context) (bool, error) {
client := h.r.Client
rt := new(v1beta1.ResourceTracker)
trackerName := h.generateResourceTrackerName()
key := ctypes.NamespacedName{Name: trackerName}
err := client.Get(ctx, key, rt)
if err != nil {
if apierrors.IsNotFound(err) {
// for some cases the resourceTracker have been deleted but finalizer still exist
if meta.FinalizerExists(h.app, resourceTrackerFinalizer) {
meta.RemoveFinalizer(h.app, resourceTrackerFinalizer)
return true, nil
}
// for some cases: informer cache haven't sync resourceTracker from k8s, return error trigger reconcile again
if h.app.Status.ResourceTracker != nil {
return false, fmt.Errorf("application status has resouceTracker but cannot get from k8s ")
}
return false, nil
}
return false, err
}
rt = &v1beta1.ResourceTracker{
ObjectMeta: metav1.ObjectMeta{
Name: trackerName,
},
}
err = h.r.Client.Delete(ctx, rt)
if err != nil {
return false, err
}
h.logger.Info("delete application resourceTracker")
meta.RemoveFinalizer(h.app, resourceTrackerFinalizer)
h.app.Status.ResourceTracker = nil
return true, nil
}
func (h *appHandler) recodeTrackedWorkload(comp *v1alpha2.Component, compRevisionName string) error {
workloadName, err := h.getWorkloadName(comp.Spec.Workload, comp.Name, compRevisionName)
if err != nil {
return err
}
if err = h.recodeTrackedResource(workloadName, comp.Spec.Workload); err != nil {
return err
}
return nil
}
// checkResourceTrackerForTrait check component trait namespace, if it's namespace is different with application, set resourceTracker as its ownerReference
// and recode trait in handler acrossNamespace field
func (h *appHandler) checkResourceTrackerForTrait(ctx context.Context, comp v1alpha2.ApplicationConfigurationComponent, compName string) error {
for i, ct := range comp.Traits {
needTracker, err := h.checkAndSetResourceTracker(&comp.Traits[i].Trait)
if err != nil {
return err
}
if needTracker {
traitName, err := h.getTraitName(ctx, compName, comp.Traits[i].DeepCopy(), &ct.Trait)
if err != nil {
return err
}
if err = h.recodeTrackedResource(traitName, ct.Trait); err != nil {
return err
}
}
}
return nil
}
// getWorkloadName generate workload name. By default the workload's name will be generated by applicationContext, this func is for application controller
// get name of crossNamespace workload. The logic of this func is same with the way of appConfig generating workloadName
func (h *appHandler) getWorkloadName(w runtime.RawExtension, componentName string, revisionName string) (string, error) {
workload, err := oamutil.RawExtension2Unstructured(&w)
if err != nil {
return "", err
}
var revision = 0
if len(revisionName) != 0 {
r, err := utils.ExtractRevision(revisionName)
if err != nil {
return "", err
}
revision = r
}
applicationconfiguration.SetAppWorkloadInstanceName(componentName, workload, revision, strconv.FormatBool(h.inplace))
return workload.GetName(), nil
}
// getTraitName generate trait name. By default the trait name will be generated by applicationContext, this func is for application controller
// get name of crossNamespace trait. The logic of this func is same with the way of appConfig generating traitName
func (h *appHandler) getTraitName(ctx context.Context, componentName string, ct *v1alpha2.ComponentTrait, t *runtime.RawExtension) (string, error) {
trait, err := oamutil.RawExtension2Unstructured(t)
if err != nil {
return "", err
}
traitDef, err := oamutil.FetchTraitDefinition(ctx, h.r, h.r.dm, trait)
if err != nil {
if !apierrors.IsNotFound(err) {
return "", errors.Wrapf(err, "cannot find trait definition %q %q %q", trait.GetAPIVersion(), trait.GetKind(), trait.GetName())
}
traitDef = oamutil.GetDummyTraitDefinition(trait)
}
traitType := traitDef.Name
if strings.Contains(traitType, ".") {
traitType = strings.Split(traitType, ".")[0]
}
traitName := oamutil.GenTraitName(componentName, ct, traitType)
return traitName, nil
}
// recodeTrackedResource append cross namespace resource to apphandler's acrossNamespaceResources field
func (h *appHandler) recodeTrackedResource(resourceName string, resource runtime.RawExtension) error {
u, err := oamutil.RawExtension2Unstructured(&resource)
if err != nil {
return err
}
tr := new(v1beta1.TypedReference)
tr.Name = resourceName
tr.Namespace = u.GetNamespace()
tr.APIVersion = u.GetAPIVersion()
tr.Kind = u.GetKind()
h.acrossNamespaceResources = append(h.acrossNamespaceResources, *tr)
return nil
}
type garbageCollectFunc func(ctx context.Context, h *appHandler) error
// 1. collect useless across-namespace resource
// 2. collect appRevision
func garbageCollection(ctx context.Context, h *appHandler) error {
collectFuncs := []garbageCollectFunc{
garbageCollectFunc(gcAcrossNamespaceResource),
garbageCollectFunc(cleanUpApplicationRevision),
}
for _, collectFunc := range collectFuncs {
if err := collectFunc(ctx, h); err != nil {
return err
}
}
return nil
}
// Now if workloads or traits are in the same namespace with application, applicationContext will take over gc workloads and traits.
// Here we cover the case in witch a cross namespace component or one of its cross namespace trait is removed from an application.
func gcAcrossNamespaceResource(ctx context.Context, h *appHandler) error {
rt := new(v1beta1.ResourceTracker)
err := h.r.Get(ctx, ctypes.NamespacedName{Name: h.generateResourceTrackerName()}, rt)
if err != nil {
if apierrors.IsNotFound(err) {
// guarantee app status right
h.app.Status.ResourceTracker = nil
return nil
}
return err
}
applied := map[v1beta1.TypedReference]bool{}
if len(h.acrossNamespaceResources) == 0 {
h.app.Status.ResourceTracker = nil
if err := h.r.Delete(ctx, rt); err != nil {
return client.IgnoreNotFound(err)
}
return nil
}
for _, resource := range h.acrossNamespaceResources {
applied[resource] = true
}
for _, ref := range rt.Status.TrackedResources {
if !applied[ref] {
resource := new(unstructured.Unstructured)
resource.SetAPIVersion(ref.APIVersion)
resource.SetKind(ref.Kind)
resource.SetNamespace(ref.Namespace)
resource.SetName(ref.Name)
err := h.r.Delete(ctx, resource)
if err != nil {
if apierrors.IsNotFound(err) {
continue
}
return err
}
}
}
// update resourceTracker status, recode applied across-namespace resources
rt.Status.TrackedResources = h.acrossNamespaceResources
if err := h.r.Status().Update(ctx, rt); err != nil {
return err
}
h.app.Status.ResourceTracker = &runtimev1alpha1.TypedReference{
Name: rt.Name,
Kind: v1beta1.ResourceTrackerGroupKind,
APIVersion: v1beta1.ResourceTrackerKindAPIVersion,
UID: rt.UID}
return nil
}
// handleResourceTracker check the namespace of all workloads and traits
// if one resource is across-namespace create resourceTracker and set in appHandler field
func (h *appHandler) handleResourceTracker(ctx context.Context, components []*v1alpha2.Component, ac *v1alpha2.ApplicationConfiguration) error {
resourceTracker := new(v1beta1.ResourceTracker)
needTracker := false
for _, c := range components {
if h.checkAutoDetect(c) {
continue
}
u, err := oamutil.RawExtension2Unstructured(&c.Spec.Workload)
if err != nil {
return err
}
inDiffNamespace, err := h.checkCrossNamespace(u)
if err != nil {
return err
}
if inDiffNamespace {
needTracker = true
break
}
}
outLoop:
for _, acComponent := range ac.Spec.Components {
for _, t := range acComponent.Traits {
u, err := oamutil.RawExtension2Unstructured(&t.Trait)
if err != nil {
return err
}
inDiffNamespace, err := h.checkCrossNamespace(u)
if err != nil {
return err
}
if inDiffNamespace {
needTracker = true
break outLoop
}
}
}
if needTracker {
// check weather related resourceTracker is existed, if not create it
err := h.r.Get(ctx, ctypes.NamespacedName{Name: h.generateResourceTrackerName()}, resourceTracker)
if err == nil {
h.resourceTracker = resourceTracker
return nil
}
if apierrors.IsNotFound(err) {
resourceTracker = &v1beta1.ResourceTracker{
ObjectMeta: metav1.ObjectMeta{
Name: h.generateResourceTrackerName(),
},
}
if err = h.r.Client.Create(ctx, resourceTracker); err != nil {
return err
}
h.resourceTracker = resourceTracker
return nil
}
return err
}
return nil
}
func (h *appHandler) handleRollout(ctx context.Context) (reconcile.Result, error) {
var comps []string
for _, component := range h.app.Spec.Components {
comps = append(comps, component.Name)
}
// targetRevision should always points to LatestRevison
targetRevision := h.app.Status.LatestRevision.Name
var srcRevision string
target, _ := oamutil.ExtractRevisionNum(targetRevision, "-")
// if target == 1 this is a initial scale operation, sourceRevision should be empty
// otherwise source revision always is targetRevision - 1
if target > 1 {
srcRevision = utils.ConstructRevisionName(h.app.Name, int64(target-1))
}
appRollout := v1beta1.AppRollout{
ObjectMeta: metav1.ObjectMeta{
Name: h.app.Name,
Namespace: h.app.Namespace,
UID: h.app.UID,
},
Spec: v1beta1.AppRolloutSpec{
SourceAppRevisionName: srcRevision,
TargetAppRevisionName: targetRevision,
ComponentList: comps,
RolloutPlan: *h.app.Spec.RolloutPlan,
},
Status: h.app.Status.Rollout,
}
// construct a fake rollout object and call rollout.DoReconcile
r := applicationrollout.NewReconciler(h.r.Client, h.r.dm, h.r.Recorder, h.r.Scheme)
res, err := r.DoReconcile(ctx, &appRollout)
if err != nil {
return reconcile.Result{}, err
}
// write back rollout status to application
h.app.Status.Rollout = appRollout.Status
return res, nil
}
func generateScopeReference(scopes []appfile.Scope) []runtimev1alpha1.TypedReference {
var references []runtimev1alpha1.TypedReference
for _, scope := range scopes {
references = append(references, runtimev1alpha1.TypedReference{
APIVersion: scope.GVK.GroupVersion().String(),
Kind: scope.GVK.Kind,
Name: scope.Name,
})
}
return references
}