remove appContext from app/appRollout controller (#1774)

* refine  assemble and dispatch

Signed-off-by: roy wang <seiwy2010@gmail.com>

* remove app context in app controller

modify clean up app revision

remove old resource tracker related logic

fix unit tests

Signed-off-by: roy wang <seiwy2010@gmail.com>

* fix e2e-test

- get rid of appCtx in test cases
- fix test cases according other logic changes in app controller

remove whole appcontext_test.go file

disable rollout related e2e test provisionally

disable resource tracker related e2e test provisionally

Signed-off-by: roy wang <seiwy2010@gmail.com>

* add finalizer logic for app controller

Signed-off-by: roywang <seiwy2010@gmail.com>

* add new apply option MustBeControllableByAny

make dispatch idempotent

Signed-off-by: roywang <seiwy2010@gmail.com>

* refactor rollout

* fix rollout finalize succeed

Signed-off-by: roywang <seiwy2010@gmail.com>

* add update trait and gc test

fix lint

* fix flaky e2e test

Signed-off-by: roywang <seiwy2010@gmail.com>

* fix comment

* fix comments and add sourceRevision dispatch

delete useless

Signed-off-by: Yue Wang <seiwy2010@gmail.com>

* fix app finalizer backward compatible

Signed-off-by: roywang <seiwy2010@gmail.com>

* fix backward compatability for deprecation of appContext

add unit test for apply option

add e2e test

Signed-off-by: Yue Wang <seiwy2010@gmail.com>

* fix app controller unit test

Signed-off-by: Yue Wang <seiwy2010@gmail.com>

* refine app controller apply logic

Signed-off-by: Yue Wang <seiwy2010@gmail.com>

* fix e2e test of resource tracker

fix e2e test of rollout plan

fix flaky e2e tests

Signed-off-by: Yue Wang <seiwy2010@gmail.com>

* refine comments and remove useless codes

Signed-off-by: Yue Wang <seiwy2010@gmail.com>

* disable appCtx controller

add Component handler into app controller

Signed-off-by: Yue Wang <seiwy2010@gmail.com>

Co-authored-by: wangyike <wangyike.wyk@alibaba-inc.com>
This commit is contained in:
Yue Wang
2021-06-12 14:46:32 +08:00
committed by GitHub
co-authored by wangyike
parent 9de6aea5ab
commit 889e38e984
41 changed files with 1983 additions and 2098 deletions
@@ -18,13 +18,13 @@ package applicationrollout
import (
"context"
"strconv"
"time"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/util/retry"
"k8s.io/klog/v2"
@@ -33,13 +33,10 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
oamv1alpha2 "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/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/controller/common/rollout"
oamctrl "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
@@ -110,119 +107,86 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (res reconcile.Result, retErr e
}
// DoReconcile is real reconcile logic for appRollout.
// 1.prepare rollout info: use assemble module in application pkg to generate manifest with appRevision
// 2.determine which component is the common component between source and target AppRevision
// 3.if target workload isn't exist yet, template the targetAppRevision to apply target manifest
// 4.extract target workload and source workload(if sourceAppRevision not empty)
// 5.generate a rolloutPlan controller with source and target workload and call rolloutPlan's reconcile func
// 6.handle output status
// !!! Note the AppRollout object should not be updated in this function as it could be logically used in Application reconcile loop which does not have real AppRollout object.
func (r *Reconciler) DoReconcile(ctx context.Context, appRollout *v1beta1.AppRollout) (res reconcile.Result, retErr error) {
func (r *Reconciler) DoReconcile(ctx context.Context, appRollout *v1beta1.AppRollout) (reconcile.Result, error) {
if len(appRollout.Status.RollingState) == 0 {
appRollout.Status.ResetStatus()
}
targetAppRevisionName := appRollout.Spec.TargetAppRevisionName
sourceAppRevisionName := appRollout.Spec.SourceAppRevisionName
var err error
// no need to proceed if rollout is already in a terminal state and there is no source/target change
doneReconcile := r.handleRollingTerminated(*appRollout, targetAppRevisionName, sourceAppRevisionName)
doneReconcile := r.handleRollingTerminated(*appRollout)
if doneReconcile {
return reconcile.Result{}, nil
}
h := rolloutHandler{Reconciler: r, appRollout: appRollout}
// handle rollout target/source change (only if it's not deleting already)
if isRolloutModified(*appRollout) {
klog.InfoS("rollout target changed, restart the rollout", "new source", sourceAppRevisionName,
"new target", targetAppRevisionName)
r.record.Event(appRollout, event.Normal("Rollout Restarted",
"rollout target changed, restart the rollout", "new source", sourceAppRevisionName,
"new target", targetAppRevisionName))
// we are okay to move directly to restart the rollout since we are at the terminal state
// however, we need to make sure we properly finalizing the existing rollout before restart if it's
// still in the middle of rolling out
if appRollout.Status.RollingState != v1alpha1.RolloutSucceedState &&
appRollout.Status.RollingState != v1alpha1.RolloutFailedState {
// continue to handle the previous resources until we are okay to move forward
targetAppRevisionName = appRollout.Status.LastUpgradedTargetAppRevision
sourceAppRevisionName = appRollout.Status.LastSourceAppRevision
} else {
// mark so that we don't think we are modified again
appRollout.Status.LastUpgradedTargetAppRevision = targetAppRevisionName
appRollout.Status.LastSourceAppRevision = sourceAppRevisionName
}
appRollout.Status.StateTransition(v1alpha1.RollingModifiedEvent)
h.handleRolloutModified()
} else {
// except modified in middle of one rollout, in most cases use real source/target in appRollout and revision as this round reconcile
h.sourceRevName = appRollout.Spec.SourceAppRevisionName
h.targetRevName = appRollout.Spec.TargetAppRevisionName
}
// Get the source application first
var sourceApRev, targetAppRev *oamv1alpha2.ApplicationRevision
var sourceApp, targetApp *oamv1alpha2.ApplicationContext
var err error
// call assemble func generate source and target manifest
if err = h.prepareRollout(ctx); err != nil {
return reconcile.Result{}, err
}
if appRollout.Status.RollingState == v1alpha1.RolloutDeletingState {
if sourceAppRevisionName == "" {
klog.InfoS("source app fields not filled, this is a scale operation", "appRollout", klog.KRef(appRollout.Namespace, appRollout.Name))
} else {
sourceApRev, sourceApp, err = r.getSourceAppContexts(ctx,
appRollout.Spec.ComponentList, appRollout.Status.RollingState, sourceAppRevisionName)
if err != nil && !apierrors.IsNotFound(err) {
return ctrl.Result{}, err
}
}
// Get the
targetAppRev, targetApp, err = r.getTargetApps(ctx, appRollout.Spec.ComponentList,
appRollout.Status.RollingState, targetAppRevisionName)
if err != nil && !apierrors.IsNotFound(err) {
return ctrl.Result{}, err
}
if sourceApp == nil && targetApp == nil {
// we only support one workload rollout now, so here is determine witch component is need to rollout
if err = h.determineRolloutComponent(); err != nil {
return reconcile.Result{}, err
}
var sourceWorkload, targetWorkload *unstructured.Unstructured
// we should handle two special cases before call rolloutPlan Reconcile
switch h.appRollout.Status.RollingState {
case v1alpha1.RolloutDeletingState:
// application has been deleted, the related appRev haven't removed
if h.sourceAppRevision == nil && h.targetAppRevision == nil {
klog.InfoS("Both the target and the source app are gone", "appRollout",
klog.KRef(appRollout.Namespace, appRollout.Name), "rolling state", appRollout.Status.RollingState)
appRollout.Status.StateTransition(v1alpha1.RollingFinalizedEvent)
h.appRollout.Status.StateTransition(v1alpha1.RollingFinalizedEvent)
// update the appRollout status
return ctrl.Result{}, nil
}
} else {
// TODO: try to refactor this into a method with reasonable number of parameters and output
if sourceAppRevisionName == "" {
klog.Info("source app fields not filled, this is a scale operation")
} else {
sourceApRev, sourceApp, err = r.getSourceAppContexts(ctx,
appRollout.Spec.ComponentList, appRollout.Status.RollingState, sourceAppRevisionName)
if err != nil {
return ctrl.Result{}, err
}
// check if the app is templated
if sourceApp.Status.RollingStatus != types.RollingTemplated {
klog.Info("source app revision is not ready for rolling yet", "application revision", sourceAppRevisionName)
r.record.Event(appRollout, event.Normal("Rollout Paused",
"source app revision is not ready for rolling yet", "application revision", sourceApp.GetName()))
return ctrl.Result{RequeueAfter: 3 * time.Second}, nil
}
}
// Get the target application revision after the source app is templated
targetAppRev, targetApp, err = r.getTargetApps(ctx, appRollout.Spec.ComponentList,
appRollout.Status.RollingState, targetAppRevisionName)
case v1alpha1.LocatingTargetAppState:
// dispatch sourceWorkload
err = h.templateSourceManifest(ctx)
if err != nil {
return ctrl.Result{}, err
return reconcile.Result{}, err
}
// this ensures that we handle the target app init only once
appRollout.Status.StateTransition(v1alpha1.AppLocatedEvent)
// check if the app is templated
if targetApp.Status.RollingStatus != types.RollingTemplated {
r.record.Event(appRollout, event.Normal("Rollout Paused",
"target app revision is not ready for rolling yet", "application revision", targetApp.GetName()))
return ctrl.Result{RequeueAfter: 3 * time.Second}, nil
// target manifest haven't template yet, call dispatch template target manifest firstly
err = h.templateTargetManifest(ctx)
if err != nil {
return reconcile.Result{}, err
}
// this ensures that we template workload only once
h.appRollout.Status.StateTransition(v1alpha1.AppLocatedEvent)
return reconcile.Result{RequeueAfter: 3 * time.Second}, nil
default:
// in other cases there is no need do anything
}
// we get the real workloads from the spec of the revisions
targetWorkload, sourceWorkload, err := r.extractWorkloads(ctx, appRollout.Spec.ComponentList, targetAppRev, sourceApRev)
sourceWorkload, targetWorkload, err = h.fetchSourceAndTargetWorkload(ctx)
if err != nil {
klog.ErrorS(err, "cannot fetch the workloads to upgrade", "target application",
klog.KRef(appRollout.Namespace, targetAppRevisionName), "source application", klog.KRef(appRollout.Namespace, sourceAppRevisionName),
"commonComponent", appRollout.Spec.ComponentList)
return ctrl.Result{RequeueAfter: 5 * time.Second}, err
return reconcile.Result{}, err
}
klog.InfoS("get the target workload we need to work on", "targetWorkload", klog.KObj(targetWorkload))
if sourceWorkload != nil {
klog.InfoS("get the source workload we need to work on", "sourceWorkload", klog.KObj(sourceWorkload))
}
// reconcile the rollout part of the spec given the target and source workload
rolloutPlanController := rollout.NewRolloutPlanController(r, appRollout, r.record,
&appRollout.Spec.RolloutPlan, &appRollout.Status.RolloutStatus, targetWorkload, sourceWorkload)
@@ -234,18 +198,19 @@ func (r *Reconciler) DoReconcile(ctx context.Context, appRollout *v1beta1.AppRol
appRollout.Status.LastUpgradedTargetAppRevision = appRollout.Spec.TargetAppRevisionName
appRollout.Status.LastSourceAppRevision = appRollout.Spec.SourceAppRevisionName
}
if rolloutStatus.RollingState == v1alpha1.RolloutSucceedState {
klog.InfoS("rollout succeeded, record the source and target app revision", "source", sourceAppRevisionName,
"target", targetAppRevisionName)
if err = r.finalizeRollingSucceeded(ctx, sourceApp, targetApp); err != nil {
return ctrl.Result{}, err
err = h.finalizeRollingSucceeded(ctx)
if err != nil {
return reconcile.Result{}, err
}
klog.InfoS("rollout succeeded, record the source and target app revision", "source", appRollout.Spec.SourceAppRevisionName,
"target", appRollout.Spec.TargetAppRevisionName)
} else if rolloutStatus.RollingState == v1alpha1.RolloutFailedState {
klog.InfoS("rollout failed, record the source and target app revision", "source", sourceAppRevisionName,
"target", targetAppRevisionName, "revert on deletion", appRollout.Spec.RevertOnDelete)
klog.InfoS("rollout failed, record the source and target app revision", "source", appRollout.Spec.SourceAppRevisionName,
"target", appRollout.Spec.TargetAppRevisionName, "revert on deletion", appRollout.Spec.RevertOnDelete)
}
// update the appRollout status
return result, nil
}
@@ -289,43 +254,20 @@ func (r *Reconciler) handleFinalizer(ctx context.Context, appRollout *v1beta1.Ap
return false, reconcile.Result{}, nil
}
func (r *Reconciler) handleRollingTerminated(appRollout v1beta1.AppRollout, targetAppRevisionName string,
sourceAppRevisionName string) bool {
func (r *Reconciler) handleRollingTerminated(appRollout v1beta1.AppRollout) bool {
// handle rollout completed
if appRollout.Status.RollingState == v1alpha1.RolloutSucceedState ||
appRollout.Status.RollingState == v1alpha1.RolloutFailedState {
if appRollout.Status.LastUpgradedTargetAppRevision == targetAppRevisionName &&
appRollout.Status.LastSourceAppRevision == sourceAppRevisionName {
klog.InfoS("rollout completed, no need to reconcile", "source", sourceAppRevisionName,
"target", targetAppRevisionName)
if appRollout.Status.LastUpgradedTargetAppRevision == appRollout.Spec.TargetAppRevisionName &&
appRollout.Status.LastSourceAppRevision == appRollout.Spec.TargetAppRevisionName {
klog.InfoS("rollout completed, no need to reconcile", "source", appRollout.Spec.SourceAppRevisionName,
"target", appRollout.Spec.TargetAppRevisionName)
return true
}
}
return false
}
func (r *Reconciler) finalizeRollingSucceeded(ctx context.Context, sourceApp *oamv1alpha2.ApplicationContext,
targetApp *oamv1alpha2.ApplicationContext) error {
if sourceApp != nil {
// mark the source app as an application revision only so that it stop being reconciled
oamutil.RemoveAnnotations(sourceApp, []string{oam.AnnotationAppRollout})
oamutil.AddAnnotations(sourceApp, map[string]string{oam.AnnotationAppRevision: strconv.FormatBool(true)})
if err := r.Update(ctx, sourceApp); err != nil {
klog.ErrorS(err, "cannot add the app revision annotation", "source application",
klog.KRef(sourceApp.Namespace, sourceApp.GetName()))
return err
}
}
// remove the rollout annotation so that the target appConfig controller can take over the rest of the work
oamutil.RemoveAnnotations(targetApp, []string{oam.AnnotationAppRollout, oam.AnnotationRollingComponent})
if err := r.Update(ctx, targetApp); err != nil {
klog.ErrorS(err, "cannot remove the rollout annotation", "target application",
klog.KRef(targetApp.Namespace, targetApp.GetName()))
return err
}
return nil
}
// UpdateStatus updates v1alpha2.AppRollout's Status with retry.RetryOnConflict
func (r *Reconciler) updateStatus(ctx context.Context, appRollout *v1beta1.AppRollout) error {
status := appRollout.DeepCopy().Status
@@ -1,258 +0,0 @@
/*
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 applicationrollout
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
ktypes "k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
"k8s.io/utils/pointer"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/controller/common"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration"
"github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/oam"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
appUtil "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/applicationrollout"
)
// getTargetApps try to locate the target appRevision and appContext that is responsible for the target
// we will create a new appContext when it's not found
func (r *Reconciler) getTargetApps(ctx context.Context, componentList []string, rollingState v1alpha1.RollingState,
targetAppRevisionName string) (*v1alpha2.ApplicationRevision, *v1alpha2.ApplicationContext, error) {
var appRevision v1alpha2.ApplicationRevision
var appContext v1alpha2.ApplicationContext
namespaceName := oamutil.GetDefinitionNamespaceWithCtx(ctx)
if err := r.Get(ctx, ktypes.NamespacedName{Namespace: namespaceName, Name: targetAppRevisionName},
&appRevision); err != nil {
klog.ErrorS(err, "cannot locate target application revision", "target application revision",
klog.KRef(namespaceName, targetAppRevisionName))
return nil, nil, err
}
if err := r.Get(ctx, ktypes.NamespacedName{Namespace: namespaceName, Name: targetAppRevisionName},
&appContext); err != nil {
if apierrors.IsNotFound(err) && rollingState == v1alpha1.LocatingTargetAppState {
klog.InfoS("target application context does not exist yet, create one", "target application revision",
klog.KRef(namespaceName, targetAppRevisionName))
appContext, err = r.createAppContext(ctx, componentList, &appRevision)
if err != nil {
return nil, nil, err
}
return &appRevision, &appContext, nil
}
// the appContext has to exist by now
klog.ErrorS(err, "cannot locate target application context", "target application name",
klog.KRef(namespaceName, targetAppRevisionName), "rollingState", rollingState)
return nil, nil, err
}
// special handle the first time we locate the appContext
if rollingState == v1alpha1.LocatingTargetAppState {
if appContext.Status.RollingStatus == types.RollingTemplated {
// force template the target app
klog.InfoS("force templating an already templated target application",
"target application revision", klog.KRef(namespaceName, targetAppRevisionName))
appContext.Status.RollingStatus = types.RollingTemplating
if err := r.Status().Update(ctx, &appContext); err != nil {
klog.ErrorS(err, "failed to force update target application context to be templating",
"target application name", klog.KRef(namespaceName, targetAppRevisionName))
return nil, nil, err
}
}
err := r.prepareAppContext(ctx, componentList, &appContext)
if err != nil {
return nil, nil, err
}
}
return &appRevision, &appContext, nil
}
// getTargetApps try to locate the source appRevision and appContext that is responsible for the source
func (r *Reconciler) getSourceAppContexts(ctx context.Context, componentList []string, rollingState v1alpha1.RollingState,
sourceAppRevisionName string) (*v1alpha2.ApplicationRevision, *v1alpha2.ApplicationContext, error) {
var appRevision v1alpha2.ApplicationRevision
var appContext v1alpha2.ApplicationContext
namespaceName := oamutil.GetDefinitionNamespaceWithCtx(ctx)
if err := r.Get(ctx, ktypes.NamespacedName{Namespace: namespaceName, Name: sourceAppRevisionName},
&appRevision); err != nil {
klog.ErrorS(err, "cannot locate source application revision", "source application revision",
klog.KRef(namespaceName, sourceAppRevisionName))
return nil, nil, err
}
// the source app has to exist or there is nothing for us to upgrade from
if err := r.Get(ctx, ktypes.NamespacedName{Namespace: namespaceName, Name: sourceAppRevisionName},
&appContext); err != nil {
// TODO: use the app name as the source Context to upgrade from none-rolling application to rolling
klog.ErrorS(err, "cannot locate source application revision", "source application name",
klog.KRef(namespaceName, sourceAppRevisionName))
return nil, nil, err
}
// set the AC as rolling if we are still at locating state
if rollingState == v1alpha1.LocatingTargetAppState {
err := r.prepareAppContext(ctx, componentList, &appContext)
if err != nil {
return nil, nil, err
}
}
return &appRevision, &appContext, nil
}
func (r *Reconciler) prepareAppContext(ctx context.Context, componentList []string,
appContext *v1alpha2.ApplicationContext) error {
oamutil.RemoveAnnotations(appContext, []string{oam.AnnotationAppRevision})
// pass the rolling component to the app
oamutil.AddAnnotations(appContext, map[string]string{oam.AnnotationAppRollout: strconv.FormatBool(true)})
if len(componentList) != 0 {
oamutil.AddAnnotations(appContext, map[string]string{
oam.AnnotationRollingComponent: strings.Join(componentList, common.RollingComponentsSep)})
}
return r.Update(ctx, appContext)
}
func (r *Reconciler) createAppContext(ctx context.Context, componentList []string,
appRevision *v1alpha2.ApplicationRevision) (v1alpha2.ApplicationContext, error) {
namespaceName := oamutil.GetDefinitionNamespaceWithCtx(ctx)
appContext := v1alpha2.ApplicationContext{
ObjectMeta: metav1.ObjectMeta{
Name: appRevision.GetName(),
Namespace: namespaceName,
Labels: appRevision.GetLabels(),
Annotations: appRevision.GetAnnotations(),
OwnerReferences: appRevision.GetOwnerReferences(),
},
Spec: v1alpha2.ApplicationContextSpec{
ApplicationRevisionName: appRevision.GetName(),
},
}
if metav1.GetControllerOf(&appContext) == nil {
for i, owner := range appContext.GetOwnerReferences() {
if owner.Kind == v1alpha2.ApplicationKind {
appContext.GetOwnerReferences()[i].Controller = pointer.BoolPtr(true)
}
}
}
// set the AC as rolling
oamutil.AddAnnotations(&appContext, map[string]string{oam.AnnotationAppRollout: strconv.FormatBool(true)})
// pass the rolling component to the app
if len(componentList) != 0 {
oamutil.AddAnnotations(&appContext, map[string]string{
oam.AnnotationRollingComponent: strings.Join(componentList, common.RollingComponentsSep)})
}
err := r.Create(ctx, &appContext)
return appContext, err
}
// extractWorkloads extracts the workloads from the source and target applicationConfig
func (r *Reconciler) extractWorkloads(ctx context.Context, componentList []string, targetAppRevision,
sourceAppRevision *v1alpha2.ApplicationRevision) (*unstructured.Unstructured, *unstructured.Unstructured, error) {
var componentName string
var sourceApp *v1alpha2.ApplicationConfiguration
targetApp, err := oamutil.RawExtension2AppConfig(targetAppRevision.Spec.ApplicationConfiguration)
if err != nil {
return nil, nil, err
}
if sourceAppRevision != nil {
sourceApp, err = oamutil.RawExtension2AppConfig(sourceAppRevision.Spec.ApplicationConfiguration)
if err != nil {
return nil, nil, err
}
}
if len(componentList) == 0 {
// we need to find a default component
commons := appUtil.FindCommonComponent(targetApp, sourceApp)
if len(commons) != 1 {
return nil, nil, fmt.Errorf("cannot find a default component, too many common components: %+v", commons)
}
componentName = commons[0]
} else {
// assume that the validator webhook has already guaranteed that there is no more than one component for now
// and the component exists in both the target and source app
componentName = componentList[0]
}
// get the workload definition
// the validator webhook has checked that source and the target are the same type
targetWorkload, err := r.fetchWorkload(ctx, componentName, targetApp)
if err != nil {
return nil, nil, err
}
klog.InfoS("successfully get the target workload we need to work on", "targetWorkload", klog.KObj(targetWorkload))
if sourceApp != nil {
sourceWorkload, err := r.fetchWorkload(ctx, componentName, sourceApp)
if err != nil {
return nil, nil, err
}
klog.InfoS("successfully get the source workload we need to work on", "sourceWorkload",
klog.KObj(sourceWorkload))
return targetWorkload, sourceWorkload, nil
}
return targetWorkload, nil, nil
}
// fetchWorkload based on the component and the appConfig
func (r *Reconciler) fetchWorkload(ctx context.Context, componentName string,
targetApp *v1alpha2.ApplicationConfiguration) (*unstructured.Unstructured, error) {
var targetAcc *v1alpha2.ApplicationConfigurationComponent
for _, acc := range targetApp.Spec.Components {
if utils.ExtractComponentName(acc.RevisionName) == componentName {
targetAcc = acc.DeepCopy()
}
}
// can't happen as we just searched the appConfig
if targetAcc == nil {
klog.Error("The component does not belong to the application",
"components", targetApp.Spec.Components, "component to upgrade", componentName)
return nil, fmt.Errorf("the component %s does not belong to the application with components %+v",
componentName, targetApp.Spec.Components)
}
revision, err := utils.ExtractRevision(targetAcc.RevisionName)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to get revision given revision name %s",
targetAcc.RevisionName))
}
// get the component given the component revision
component, _, err := oamutil.GetComponent(ctx, r, *targetAcc, targetApp.GetNamespace())
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to get component given its revision %s",
targetAcc.RevisionName))
}
// get the workload template in the component
w, err := oamutil.RawExtension2Unstructured(&component.Spec.Workload)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to get component given revision %s", targetAcc.RevisionName))
}
// reuse the same appConfig controller logic that determines the workload name given an ACC
// inplaceUpgrade not used in rollout now
applicationconfiguration.SetAppWorkloadInstanceName(componentName, w, revision, "")
// get the real workload object from api-server given GVK and name
workload, err := oamutil.GetObjectGivenGVKAndName(ctx, r, w.GroupVersionKind(), targetApp.GetNamespace(), w.GetName())
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to get workload %s with gvk %+v ", w.GetName(), w.GroupVersionKind()))
}
return workload, nil
}
@@ -0,0 +1,80 @@
/*
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 applicationrollout
import (
"reflect"
"k8s.io/utils/pointer"
"github.com/openkruise/kruise-api/apps/v1alpha1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/klog/v2"
"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/core.oam.dev/v1alpha2/application/assemble"
"github.com/oam-dev/kubevela/pkg/oam"
)
func rolloutWorkloadName() assemble.WorkloadOption {
return assemble.WorkloadOptionFn(func(w *unstructured.Unstructured, component *v1alpha2.Component, definition *v1beta1.ComponentDefinition) error {
// we hard code the behavior depends on the workload group/kind for now. The only in-place upgradable resources
// we support is cloneset/statefulset for now. We can easily add more later.
if w.GroupVersionKind().Group == v1alpha1.GroupVersion.Group {
if w.GetKind() == reflect.TypeOf(v1alpha1.CloneSet{}).Name() ||
w.GetKind() == reflect.TypeOf(v1alpha1.StatefulSet{}).Name() {
// we use the component name alone for those resources that do support in-place upgrade
klog.InfoS("we reuse the component name for resources that support in-place upgrade",
"GVK", w.GroupVersionKind(), "instance name", component.Name)
w.SetName(component.Name)
return nil
}
}
// we assume that the rest of the resources do not support in-place upgrade
compRevName := w.GetLabels()[oam.LabelAppComponentRevision]
w.SetName(compRevName)
klog.InfoS("we encountered an unknown resources, assume that it does not support in-place upgrade",
"GVK", w.GroupVersionKind(), "instance name", compRevName)
return nil
})
}
// appRollout should take over updating workload, so disable previous controller owner(resourceTracker)
func disableControllerOwner(workload *unstructured.Unstructured) {
if workload == nil {
return
}
ownerRefs := workload.GetOwnerReferences()
for i, ref := range ownerRefs {
if ref.Controller != nil && *ref.Controller {
ownerRefs[i].Controller = pointer.BoolPtr(false)
}
}
workload.SetOwnerReferences(ownerRefs)
}
// enableControllerOwner yield controller owner back to resourceTracker
func enableControllerOwner(workload *unstructured.Unstructured) {
owners := workload.GetOwnerReferences()
for i, owner := range owners {
if owner.Kind == v1beta1.ResourceTrackerKind && owner.Controller != nil && !*owner.Controller {
owners[i].Controller = pointer.BoolPtr(true)
}
}
workload.SetOwnerReferences(owners)
}
@@ -0,0 +1,61 @@
/*
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 applicationrollout
import (
"testing"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"gotest.tools/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/utils/pointer"
)
func TestDisableControllerOwner(t *testing.T) {
w := &unstructured.Unstructured{}
owners := []metav1.OwnerReference{
{Name: "test-1", Controller: pointer.BoolPtr(false)},
{Name: "test-2", Controller: pointer.BoolPtr(true)},
}
w.SetOwnerReferences(owners)
disableControllerOwner(w)
assert.Equal(t, 2, len(w.GetOwnerReferences()))
for _, reference := range w.GetOwnerReferences() {
assert.Equal(t, false, *reference.Controller)
}
}
func TestEnableControllerOwner(t *testing.T) {
w := &unstructured.Unstructured{}
owners := []metav1.OwnerReference{
{Name: "test-1", Controller: pointer.BoolPtr(false), Kind: v1beta1.ResourceTrackerKind},
{Name: "test-2", Controller: pointer.BoolPtr(false), Kind: v1alpha2.ApplicationKind},
}
w.SetOwnerReferences(owners)
enableControllerOwner(w)
assert.Equal(t, 2, len(w.GetOwnerReferences()))
for _, reference := range w.GetOwnerReferences() {
if reference.Kind == v1beta1.ResourceTrackerKind {
assert.Equal(t, true, *reference.Controller)
} else {
assert.Equal(t, false, *reference.Controller)
}
}
}
@@ -0,0 +1,290 @@
/*
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 applicationrollout
import (
"context"
"fmt"
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/types"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/pkg/event"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/dispatch"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/assemble"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
appUtil "github.com/oam-dev/kubevela/pkg/webhook/core.oam.dev/v1alpha2/applicationrollout"
)
type rolloutHandler struct {
*Reconciler
appRollout *v1beta1.AppRollout
// source/targetRevName represent this round reconcile using source and target revision
// in most cases they are equal to appRollout.spec.target/sourceRevName but if roll forward or revert in middle of rollout
// source/targetRevName are equal to previous rollout
sourceRevName string
targetRevName string
sourceAppRevision *v1beta1.ApplicationRevision
targetAppRevision *v1beta1.ApplicationRevision
// sourceWorkloads is assembled by appRevision in assemble phase
// please be aware that they are not real status in k8s, they are just generate from appRevision include GVK+namespace+name
sourceWorkloads map[string]*unstructured.Unstructured
targetWorkloads map[string]*unstructured.Unstructured
// targetManifests used by dispatch(template targetRevision) and handleSucceed(GC) phase
targetManifests []*unstructured.Unstructured
// sourceManifests used by dispatch(template targetRevision) and handleSucceed(GC) phase
sourceManifests []*unstructured.Unstructured
// needRollComponent is find common component between source and target revision
needRollComponent string
}
// prepareRollout call assemble func to prepare info needed in whole reconcile loop
func (h *rolloutHandler) prepareRollout(ctx context.Context) error {
var err error
h.targetAppRevision = new(v1beta1.ApplicationRevision)
if err := h.Get(ctx, types.NamespacedName{Namespace: h.appRollout.Namespace, Name: h.targetRevName}, h.targetAppRevision); err != nil {
return err
}
// construct a assemble manifest for targetAppRevision
targetAssemble := assemble.NewAppManifests(h.targetAppRevision).
WithWorkloadOption(rolloutWorkloadName()).
WithWorkloadOption(assemble.PrepareWorkloadForRollout())
// in template phase, we should use targetManifests including target workloads/traits to
h.targetManifests, err = targetAssemble.AssembledManifests()
if err != nil {
klog.Error("appRollout targetAppRevision failed to assemble manifest", "appRollout", klog.KRef(h.appRollout.Namespace, h.appRollout.Name))
return err
}
// we only use workloads group by component name to find common workloads in source and target revision
h.targetWorkloads, _, _, err = targetAssemble.GroupAssembledManifests()
if err != nil {
klog.Error("appRollout targetAppRevision failed to assemble target workload", "appRollout", klog.KRef(h.appRollout.Namespace, h.appRollout.Name))
return err
}
if len(h.sourceRevName) != 0 {
h.sourceAppRevision = new(v1beta1.ApplicationRevision)
if err := h.Get(ctx, types.NamespacedName{Namespace: h.appRollout.Namespace, Name: h.sourceRevName}, h.sourceAppRevision); err != nil {
return err
}
// construct a assemble manifest for sourceAppRevision
sourceAssemble := assemble.NewAppManifests(h.sourceAppRevision).
WithWorkloadOption(assemble.PrepareWorkloadForRollout()).
WithWorkloadOption(rolloutWorkloadName())
h.sourceWorkloads, _, _, err = sourceAssemble.GroupAssembledManifests()
if err != nil {
klog.Error("appRollout sourceAppRevision failed to assemble workloads", "appRollout", klog.KRef(h.appRollout.Namespace, h.appRollout.Name))
return err
}
}
return nil
}
// we only support one workload now, so this func is to determine witch component is need to rollout
func (h *rolloutHandler) determineRolloutComponent() error {
componentList := h.appRollout.Spec.ComponentList
// if user not set ComponentList in AppRollout we also find a common component between source and target
if len(componentList) == 0 {
// we need to find a default component
commons := appUtil.FindCommonComponentWithManifest(h.targetWorkloads, h.sourceWorkloads)
if len(commons) != 1 {
return fmt.Errorf("cannot find a default component, too many common components: %+v", commons)
}
h.needRollComponent = commons[0]
} else {
// assume that the validator webhook has already guaranteed that there is no more than one component for now
// and the component exists in both the target and source app
h.needRollComponent = componentList[0]
}
return nil
}
// fetch source and target workload
func (h *rolloutHandler) fetchSourceAndTargetWorkload(ctx context.Context) (*unstructured.Unstructured, *unstructured.Unstructured, error) {
var sourceWorkload, targetWorkload *unstructured.Unstructured
var err error
if len(h.sourceRevName) == 0 {
klog.Info("source app fields not filled, this is a scale operation")
} else if sourceWorkload, err = h.extractWorkload(ctx, *h.sourceWorkloads[h.needRollComponent]); err != nil {
klog.Errorf("specified sourceRevName but cannot fetch source workload %s: %v",
h.appRollout.Spec.SourceAppRevisionName, err)
return nil, nil, err
}
if targetWorkload, err = h.extractWorkload(ctx, *h.targetWorkloads[h.needRollComponent]); err != nil {
klog.Errorf("cannot fetch target workload %s: %v", h.appRollout.Spec.TargetAppRevisionName, err)
return nil, nil, err
}
return sourceWorkload, targetWorkload, nil
}
// extractWorkload use GVK and name of workload(assembled result) to fetch real workload in cluster
func (h *rolloutHandler) extractWorkload(ctx context.Context, workload unstructured.Unstructured) (*unstructured.Unstructured, error) {
wl, err := oamutil.GetObjectGivenGVKAndName(ctx, h, workload.GroupVersionKind(), workload.GetNamespace(), workload.GetName())
if err != nil {
return nil, err
}
return wl, nil
}
// if in middle of previous rollout, continue use previous source and target appRevision as this round rollout
func (h *rolloutHandler) handleRolloutModified() {
klog.InfoS("rollout target changed, restart the rollout", "new source", h.appRollout.Spec.SourceAppRevisionName,
"new target", h.appRollout.Spec.TargetAppRevisionName)
h.record.Event(h.appRollout, event.Normal("Rollout Restarted",
"rollout target changed, restart the rollout", "new source", h.appRollout.Spec.SourceAppRevisionName,
"new target", h.appRollout.Spec.TargetAppRevisionName))
// we are okay to move directly to restart the rollout since we are at the terminal state
// however, we need to make sure we properly finalizing the existing rollout before restart if it's
// still in the middle of rolling out
if h.appRollout.Status.RollingState != v1alpha1.RolloutSucceedState &&
h.appRollout.Status.RollingState != v1alpha1.RolloutFailedState {
// happen when roll forward or revert in middle of rollout, previous rollout haven't finished
// continue to handle the previous resources until we are okay to move forward
h.targetRevName = h.appRollout.Status.LastUpgradedTargetAppRevision
h.sourceRevName = h.appRollout.Status.LastSourceAppRevision
} else {
// previous rollout have finished, go ahead using new source/target revision
h.targetRevName = h.appRollout.Spec.TargetAppRevisionName
h.sourceRevName = h.appRollout.Spec.SourceAppRevisionName
// mark so that we don't think we are modified again
h.appRollout.Status.LastUpgradedTargetAppRevision = h.appRollout.Spec.TargetAppRevisionName
h.appRollout.Status.LastSourceAppRevision = h.appRollout.Spec.SourceAppRevisionName
}
h.appRollout.Status.StateTransition(v1alpha1.RollingModifiedEvent)
}
// templateTargetManifest call dispatch to template target app revision's manifests to cluster
func (h *rolloutHandler) templateTargetManifest(ctx context.Context) error {
var rt *v1beta1.ResourceTracker
// if sourceAppRevision is not nil, we should upgrade existing resources which are also needed by target app
// revision
if h.sourceAppRevision != nil {
rt = new(v1beta1.ResourceTracker)
err := h.Get(ctx, types.NamespacedName{Name: dispatch.ConstructResourceTrackerName(h.appRollout.Spec.SourceAppRevisionName, h.appRollout.Namespace)}, rt)
if err != nil {
klog.Errorf("specified sourceAppRevisionName %s but cannot fetch the sourceResourceTracker %v",
h.appRollout.Spec.SourceAppRevisionName, err)
return err
}
}
// use source resourceTracker to handle same resource owner transfer
dispatcher := dispatch.NewAppManifestsDispatcher(h, h.targetAppRevision).EnableUpgradeAndSkipGC(rt)
_, err := dispatcher.Dispatch(ctx, h.targetManifests)
if err != nil {
klog.Errorf("dispatch targetRevision error %s:%v", h.appRollout.Spec.TargetAppRevisionName, err)
return err
}
workload, err := h.extractWorkload(ctx, *h.targetWorkloads[h.needRollComponent])
if err != nil {
return err
}
ref := metav1.GetControllerOfNoCopy(workload)
if ref != nil && ref.Kind == v1beta1.ResourceTrackerKind {
wlPatch := client.MergeFrom(workload.DeepCopy())
// guarantee resourceTracker isn't controller owner of workload
disableControllerOwner(workload)
if err = h.Client.Patch(ctx, workload, wlPatch, client.FieldOwner(h.appRollout.UID)); err != nil {
return err
}
}
return nil
}
// templateTargetManifest call dispatch to template source app revision's manifests to cluster
func (h *rolloutHandler) templateSourceManifest(ctx context.Context) error {
// only when sourceAppRevision is not nil, we need template sourceRevision revision
if h.sourceAppRevision == nil {
return nil
}
// use source resourceTracker to handle same resource owner transfer
dispatcher := dispatch.NewAppManifestsDispatcher(h, h.sourceAppRevision)
_, err := dispatcher.Dispatch(ctx, h.sourceManifests)
if err != nil {
klog.Errorf("dispatch sourceRevision error %s:%v", h.appRollout.Spec.TargetAppRevisionName, err)
return err
}
workload, err := h.extractWorkload(ctx, *h.sourceWorkloads[h.needRollComponent])
if err != nil {
return err
}
ref := metav1.GetControllerOfNoCopy(workload)
if ref != nil && ref.Kind == v1beta1.ResourceTrackerKind {
wlPatch := client.MergeFrom(workload.DeepCopy())
// guarantee resourceTracker isn't controller owner of workload
disableControllerOwner(workload)
if err = h.Client.Patch(ctx, workload, wlPatch, client.FieldOwner(h.appRollout.UID)); err != nil {
return err
}
}
return nil
}
// handle rollout succeed work left
func (h *rolloutHandler) finalizeRollingSucceeded(ctx context.Context) error {
// yield controller owner back to resourceTracker
workload, err := h.extractWorkload(ctx, *h.targetWorkloads[h.needRollComponent])
if err != nil {
return err
}
wlPatch := client.MergeFrom(workload.DeepCopy())
enableControllerOwner(workload)
if err = h.Client.Patch(ctx, workload, wlPatch, client.FieldOwner(h.appRollout.UID)); err != nil {
return err
}
// only when sourceAppRevision is not nil, we need gc old revision resources
if h.sourceAppRevision != nil {
oldRT := &v1beta1.ResourceTracker{}
err := h.Client.Get(ctx, client.ObjectKey{
Name: dispatch.ConstructResourceTrackerName(h.sourceAppRevision.Name, h.sourceAppRevision.Namespace)}, oldRT)
if err != nil && apierrors.IsNotFound(err) {
// end finalizing if source revision's tracker is already gone
// this guarantees finalizeRollingSucceeded will only GC once
return nil
}
if err != nil {
return err
}
d := dispatch.NewAppManifestsDispatcher(h.Client, h.targetAppRevision).
EnableUpgradeAndGC(oldRT)
// no need to dispatch manifest again, just do GC
if _, err := d.Dispatch(ctx, nil); err != nil {
return err
}
}
return nil
}