add abandoning state to the rollout (#1290)

* add abandoning state

* fine tunee2e timing and improve logging

* add back one application UT and fine tune timing in e2e and improve test robustness
This commit is contained in:
Ryan Zhang
2021-03-24 23:04:06 -07:00
committed by GitHub
parent 33dae5e36e
commit c5d81c975a
12 changed files with 301 additions and 242 deletions

View File

@@ -34,22 +34,24 @@ const (
type RollingState string
const (
// VerifyingSpecState verify that the rollout setting is valid and the controller can locate both the
// target and the source
// VerifyingSpecState indicates that the rollout is in the stage of verifying the rollout settings
// and the controller can locate both the target and the source
VerifyingSpecState RollingState = "verifyingSpec"
// InitializingState rollout is initializing all the new resources
// InitializingState indicates that the rollout is initializing all the new resources
InitializingState RollingState = "initializing"
// RollingInBatchesState rolling out
// RollingInBatchesState indicates that the rollout starts rolling
RollingInBatchesState RollingState = "rollingInBatches"
// FinalisingState finalize the rolling, possibly clean up the old resources, adjust traffic
// FinalisingState indicates that the rollout is finalizing, possibly clean up the old resources, adjust traffic
FinalisingState RollingState = "finalising"
// RolloutSucceedState rollout successfully completed to match the desired target state
RolloutSucceedState RollingState = "rolloutSucceed"
// RolloutFailingState finalize the rollout before giving up, possibly clean up the old resources, adjust traffic
// RolloutFailingState indicates that the rollout is failing
// one needs to finalize it before mark it as failed by cleaning up the old resources, adjust traffic
RolloutFailingState RollingState = "rolloutFailing"
// RolloutFailedState rollout is failed, the target replica is not reached
// we can not move forward anymore
// we will let the client to decide when or whether to revert
// RolloutSucceedState indicates that rollout successfully completed to match the desired target state
RolloutSucceedState RollingState = "rolloutSucceed"
// RolloutAbandoningState indicates that the rollout is abandoned, can be restarted. This is a terminal state
RolloutAbandoningState RollingState = "rolloutAbandoned"
// RolloutFailedState indicates that rollout is failed, the target replica is not reached
// we can not move forward anymore, we will let the client to decide when or whether to revert.
RolloutFailedState RollingState = "rolloutFailed"
)

View File

@@ -20,6 +20,9 @@ const (
// RollingRetriableFailureEvent indicates that we encountered an unexpected but retriable error
RollingRetriableFailureEvent RolloutEvent = "RollingRetriableFailureEvent"
// RollingModifiedEvent indicates that the rolling target or source has changed
RollingModifiedEvent RolloutEvent = "RollingModifiedEvent"
// RollingSpecVerifiedEvent indicates that we have successfully verified that the rollout spec
RollingSpecVerifiedEvent RolloutEvent = "RollingSpecVerifiedEvent"
@@ -65,6 +68,8 @@ const (
RolloutFinalizing runtimev1alpha1.ConditionType = "RolloutFinalizing"
// RolloutFailing means the rollout is failing
RolloutFailing runtimev1alpha1.ConditionType = "RolloutFailing"
// RolloutAbandoning means that the rollout is being abandoned.
RolloutAbandoning runtimev1alpha1.ConditionType = "RolloutAbandoning"
// RolloutFailed means that the rollout failed.
RolloutFailed runtimev1alpha1.ConditionType = "RolloutFailed"
// RolloutSucceed means that the rollout is done.
@@ -142,6 +147,9 @@ func (r *RolloutStatus) getRolloutConditionType() runtimev1alpha1.ConditionType
case RolloutFailingState:
return RolloutFailing
case RolloutAbandoningState:
return RolloutAbandoning
case RolloutFailedState:
return RolloutFailed
@@ -159,12 +167,6 @@ func (r *RolloutStatus) RolloutRetry(reason string) {
r.SetConditions(NewNegativeCondition(r.getRolloutConditionType(), reason))
}
// RolloutModified is special state transition as we allow it to happen at any time
func (r *RolloutStatus) RolloutModified() {
r.SetRolloutCondition(NewNegativeCondition(r.getRolloutConditionType(), "Rollout Spec is modified"))
r.ResetStatus()
}
// RolloutFailed is a special state transition since we need an error message
func (r *RolloutStatus) RolloutFailed(reason string) {
// set the condition first which depends on the state
@@ -233,10 +235,20 @@ func (r *RolloutStatus) StateTransition(event RolloutEvent) {
"post batch rolling state", r.BatchRollingState)
}()
// we have special transition for these two types of event
// we have special transition for these types of event since they require additional info
if event == RollingFailedEvent || event == RollingRetriableFailureEvent {
panic(fmt.Errorf(invalidRollingStateTransition, rollingState, event))
}
// special handle modified event here
if event == RollingModifiedEvent {
if r.RollingState == RolloutFailedState || r.RollingState == RolloutSucceedState {
r.ResetStatus()
} else {
r.SetRolloutCondition(NewNegativeCondition(r.getRolloutConditionType(), "Rollout Spec is modified"))
r.RollingState = RolloutAbandoningState
}
return
}
switch rollingState {
case VerifyingSpecState:
@@ -260,6 +272,14 @@ func (r *RolloutStatus) StateTransition(event RolloutEvent) {
r.batchStateTransition(event)
return
case RolloutAbandoningState:
if event == RollingFinalizedEvent {
r.SetRolloutCondition(NewPositiveCondition(r.getRolloutConditionType()))
r.ResetStatus()
return
}
panic(fmt.Errorf(invalidRollingStateTransition, rollingState, event))
case FinalisingState:
if event == RollingFinalizedEvent {
r.SetRolloutCondition(NewPositiveCondition(r.getRolloutConditionType()))

View File

@@ -63,7 +63,7 @@ func NewRolloutPlanController(client client.Client, parentController oam.Object,
// Reconcile reconciles a rollout plan
func (r *Controller) Reconcile(ctx context.Context) (res reconcile.Result, status *v1alpha1.RolloutStatus) {
klog.InfoS("Reconcile the rollout plan", "rollout Spec", r.rolloutSpec,
klog.InfoS("Reconcile the rollout plan", "rollout status", r.rolloutStatus,
"target workload", klog.KObj(r.targetWorkload))
if r.sourceWorkload != nil {
klog.InfoS("We will do rolling upgrades", "source workload", klog.KObj(r.sourceWorkload))
@@ -122,7 +122,7 @@ func (r *Controller) Reconcile(ctx context.Context) (res reconcile.Result, statu
case v1alpha1.RollingInBatchesState:
r.reconcileBatchInRolling(ctx, workloadController)
case v1alpha1.RolloutFailingState:
case v1alpha1.RolloutFailingState, v1alpha1.RolloutAbandoningState:
if succeed := workloadController.Finalize(ctx, false); succeed {
r.finalizeRollout(ctx)
}

View File

@@ -165,21 +165,21 @@ func (c *CloneSetController) CheckOneBatchPods(ctx context.Context) (bool, error
if currentBatch.MaxUnavailable != nil {
unavail, _ = intstr.GetValueFromIntOrPercent(currentBatch.MaxUnavailable, int(cloneSetSize), true)
}
klog.InfoS("checking the rolling out progress", "current batch", currentBatch,
klog.InfoS("checking the rolling out progress", "current batch", c.rolloutStatus.CurrentBatch,
"new pod count target", newPodTarget, "new ready pod count", readyPodCount,
"max unavailable pod allowed", unavail)
c.rolloutStatus.UpgradedReadyReplicas = int32(readyPodCount)
// we could overshoot in the revert case when many pods are already upgraded
if unavail+readyPodCount >= newPodTarget {
// record the successful upgrade
klog.InfoS("all pods in current batch are ready", "current batch", currentBatch)
klog.InfoS("all pods in current batch are ready", "current batch", c.rolloutStatus.CurrentBatch)
c.recorder.Event(c.parentController, event.Normal("Batch Available",
fmt.Sprintf("Batch %d is available", c.rolloutStatus.CurrentBatch)))
c.rolloutStatus.LastAppliedPodTemplateIdentifier = c.rolloutStatus.NewPodTemplateIdentifier
return true, nil
}
// continue to verify
klog.InfoS("the batch is not ready yet", "current batch", currentBatch)
klog.InfoS("the batch is not ready yet", "current batch", c.rolloutStatus.CurrentBatch)
c.rolloutStatus.RolloutRetry("the batch is not ready yet")
return false, nil
}

View File

@@ -115,8 +115,8 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
oamutil.PassLabelAndAnnotation(app, ac)
app.Status.SetConditions(readyCondition("Built"))
applog.Info("apply appConfig & component to the cluster")
// apply appConfig & component to the cluster
applog.Info("apply application revision & component to the cluster")
// apply application revision & component to the cluster
if err := handler.apply(ctx, ac, comps); err != nil {
applog.Error(err, "[Handle apply]")
app.Status.SetConditions(errorCondition("Applied", err))

View File

@@ -891,8 +891,7 @@ var _ = Describe("Test Application Controller", func() {
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
})
// Fix rollout related test in next PR
PIt("app generate appConfigs with annotation", func() {
It("app with rollout annotation", func() {
By("create application with rolling out annotation")
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
@@ -916,26 +915,23 @@ var _ = Describe("Test Application Controller", func() {
}
reconcileRetry(reconciler, reconcile.Request{NamespacedName: appKey})
By("Check Application Created with the correct revision")
curApp := &v1beta1.Application{}
Expect(k8sClient.Get(ctx, appKey, curApp)).Should(BeNil())
Expect(curApp.Status.Phase).Should(Equal(common.ApplicationRunning))
Expect(curApp.Status.LatestRevision).ShouldNot(BeNil())
Expect(curApp.Status.LatestRevision.Revision).Should(BeEquivalentTo(1))
By("Check AppRevision created as expected")
Expect(k8sClient.Get(ctx, appKey, rolloutApp)).Should(Succeed())
appRevision := &v1beta1.ApplicationRevision{}
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: rolloutApp.Namespace,
Name: curApp.Status.LatestRevision.Name,
Name: utils.ConstructRevisionName(rolloutApp.Name, 1),
}, appRevision)).Should(BeNil())
By("Check ApplicationContext not created")
appContext := &v1alpha2.ApplicationContext{}
// no appContext same name as app exist
Expect(k8sClient.Get(ctx, appKey, appContext)).ShouldNot(Succeed())
// no appContext same name as apprevision exist
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: rolloutApp.Namespace,
Name: utils.ConstructRevisionName(rolloutApp.Name, 1),
}, appContext)).Should(HaveOccurred())
}, appContext)).ShouldNot(Succeed())
By("Check Component Created with the expected workload spec")
var component v1alpha2.Component
@@ -945,27 +941,29 @@ var _ = Describe("Test Application Controller", func() {
}, &component)).Should(BeNil())
Expect(component.Status.LatestRevision).ShouldNot(BeNil())
Expect(component.Status.LatestRevision.Revision).Should(BeEquivalentTo(1))
// check that the new appconfig has the correct annotation and labels
// check that the appconfig has the correct annotation and labels
ac, err := util.RawExtension2AppConfig(appRevision.Spec.ApplicationConfiguration)
Expect(err).Should(BeNil())
Expect(ac.GetAnnotations()[oam.AnnotationAppRollout]).Should(Equal(strconv.FormatBool(true)))
Expect(ac.GetAnnotations()["keep"]).Should(Equal("true"))
Expect(ac.GetLabels()[oam.LabelAppRevisionHash]).ShouldNot(BeEmpty())
Expect(ac.Spec.Components[0].ComponentName).Should(BeEmpty())
Expect(ac.Spec.Components[0].RevisionName).Should(Equal(component.Status.LatestRevision.Name))
By("Reconcile again to make sure we are not creating more appConfigs")
reconcileRetry(reconciler, reconcile.Request{NamespacedName: appKey})
Expect(k8sClient.Get(ctx, appKey, curApp)).Should(BeNil())
Expect(curApp.Status.Phase).Should(Equal(common.ApplicationRunning))
Expect(curApp.Status.LatestRevision).ShouldNot(BeNil())
Expect(curApp.Status.LatestRevision.Revision).Should(BeEquivalentTo(1))
By("Check no new ApplicationConfiguration created")
By("Verify that no new AppRevision created")
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: rolloutApp.Namespace,
Name: utils.ConstructRevisionName(rolloutApp.Name, 2),
}, appRevision)).ShouldNot(Succeed())
// no appContext same name as app exist
Expect(k8sClient.Get(ctx, appKey, appContext)).ShouldNot(Succeed())
// no appContext same name as apprevision exist
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: rolloutApp.Namespace,
Name: utils.ConstructRevisionName(rolloutApp.Name, 1),
}, appContext)).Should(HaveOccurred())
}, appContext)).ShouldNot(Succeed())
By("Check no new Component created")
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: rolloutApp.Namespace,
@@ -975,20 +973,26 @@ var _ = Describe("Test Application Controller", func() {
Expect(component.Status.LatestRevision.Revision).ShouldNot(BeNil())
Expect(component.Status.LatestRevision.Revision).Should(BeEquivalentTo(1))
By("Remove rollout annotation which should not trigger any change")
By("Remove rollout annotation should lead to new appContext created")
Expect(k8sClient.Get(ctx, appKey, rolloutApp)).Should(Succeed())
rolloutApp.SetAnnotations(map[string]string{
"keep": "true",
})
Expect(k8sClient.Update(ctx, rolloutApp)).Should(BeNil())
reconcileRetry(reconciler, reconcile.Request{NamespacedName: appKey})
Expect(k8sClient.Get(ctx, appKey, curApp)).Should(BeNil())
Expect(curApp.Status.Phase).Should(Equal(common.ApplicationRunning))
Expect(curApp.Status.LatestRevision).ShouldNot(BeNil())
Expect(curApp.Status.LatestRevision.Revision).Should(BeEquivalentTo(1))
// check v2 is not created
// app should create an appContext
Expect(k8sClient.Get(ctx, appKey, appContext)).Should(Succeed())
Expect(appContext.Spec.ApplicationRevisionName).Should(Equal(utils.ConstructRevisionName(rolloutApp.Name, 1)))
By("Verify that no new AppRevision created")
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: rolloutApp.Namespace,
Name: utils.ConstructRevisionName(rolloutApp.Name, 2),
}, appContext)).Should(HaveOccurred())
}, appRevision)).ShouldNot(Succeed())
// no appContext same name as apprevision exist
Expect(k8sClient.Get(ctx, client.ObjectKey{
Namespace: rolloutApp.Namespace,
Name: utils.ConstructRevisionName(rolloutApp.Name, 1),
}, appContext)).ShouldNot(Succeed())
By("Delete Application, clean the resource")
Expect(k8sClient.Delete(ctx, rolloutApp)).Should(BeNil())
})

View File

@@ -40,7 +40,7 @@ var _ = Describe("test generate revision ", func() {
ctx := context.Background()
BeforeEach(func() {
namespaceName = "apply-test-" + strconv.Itoa(rand.Intn(1000))
namespaceName = "apply-test-" + strconv.FormatInt(rand.Int63n(10000000), 16)
ns = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespaceName,

View File

@@ -41,7 +41,6 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
core "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
@@ -91,12 +90,6 @@ func Setup(mgr ctrl.Manager, args core.Args, l logging.Logger) error {
return ctrl.NewControllerManagedBy(mgr).
Named(name).
For(&v1alpha2.ApplicationConfiguration{}).
Watches(&source.Kind{Type: &v1alpha2.Component{}}, &ComponentHandler{
Client: mgr.GetClient(),
Logger: l,
RevisionLimit: args.RevisionLimit,
CustomRevisionHookURL: args.CustomRevisionHookURL,
}).
Complete(NewReconciler(mgr, args.DiscoveryMapper,
l.WithValues("controller", name),
WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))),

View File

@@ -80,8 +80,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (res reconcile.Result, retErr e
r.handleFinalizer(&appRollout)
targetAppRevisionName := appRollout.Spec.TargetAppRevisionName
sourceAppRevisionName := appRollout.Spec.SourceAppRevisionName
// handle rollout target/source change
// handle rollout completed
if appRollout.Status.RollingState == v1alpha1.RolloutSucceedState ||
appRollout.Status.RollingState == v1alpha1.RolloutFailedState {
if appRollout.Status.LastUpgradedTargetAppRevision == targetAppRevisionName &&
@@ -91,7 +90,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (res reconcile.Result, retErr e
return ctrl.Result{}, nil
}
}
// handle rollout target/source change
if appRollout.Status.LastUpgradedTargetAppRevision != "" &&
appRollout.Status.LastUpgradedTargetAppRevision != targetAppRevisionName ||
(appRollout.Status.LastSourceAppRevision != "" && appRollout.Status.LastSourceAppRevision != sourceAppRevisionName) {
@@ -100,13 +99,16 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (res reconcile.Result, retErr e
r.record.Event(&appRollout, event.Normal("Rollout Restarted",
"rollout target changed, restart the rollout", "new source", sourceAppRevisionName,
"new target", targetAppRevisionName))
if err := r.finalizeRollingAborted(ctx, appRollout.Status.LastUpgradedTargetAppRevision,
appRollout.Status.LastSourceAppRevision); err != nil {
klog.ErrorS(err, "failed to finalize the previous rolling resources ", "old source",
appRollout.Status.LastSourceAppRevision, "old target", appRollout.Status.LastUpgradedTargetAppRevision)
// 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
}
appRollout.Status.RolloutModified()
appRollout.Status.StateTransition(v1alpha1.RollingModifiedEvent)
}
// Get the source application
@@ -162,8 +164,11 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (res reconcile.Result, retErr e
result, rolloutStatus := rolloutPlanController.Reconcile(ctx)
// make sure that the new status is copied back
appRollout.Status.RolloutStatus = *rolloutStatus
appRollout.Status.LastUpgradedTargetAppRevision = targetAppRevisionName
appRollout.Status.LastSourceAppRevision = sourceAppRevisionName
// do not update the last with new revision if we are still trying to abandon the previous rollout
if rolloutStatus.RollingState != v1alpha1.RolloutAbandoningState {
appRollout.Status.LastUpgradedTargetAppRevision = appRollout.Spec.TargetAppRevisionName
appRollout.Status.LastSourceAppRevision = appRollout.Spec.SourceAppRevisionName
}
if rolloutStatus.RollingState == v1alpha1.RolloutSucceedState {
if err = r.finalizeRollingSucceeded(ctx, sourceApp, targetApp); err != nil {
return ctrl.Result{}, err
@@ -197,11 +202,6 @@ func (r *Reconciler) finalizeRollingSucceeded(ctx context.Context, sourceApp *oa
return nil
}
func (r *Reconciler) finalizeRollingAborted(ctx context.Context, sourceRevision, targetRevision string) error {
// TODO: finalize the previous appcontext the best we can
return nil
}
// UpdateStatus updates v1alpha2.AppRollout's Status with retry.RetryOnConflict
func (r *Reconciler) updateStatus(ctx context.Context, appRollout *v1beta1.AppRollout, opts ...client.UpdateOption) error {
status := appRollout.DeepCopy().Status

View File

@@ -19,14 +19,14 @@ func DefaultRolloutPlan(rollout *v1alpha1.RolloutPlan) {
totalSize := int(*rollout.TargetSize)
// create the batch array
rollout.RolloutBatches = make([]v1alpha1.RolloutBatch, int(*rollout.NumBatches))
avg := intstr.FromInt(totalSize / numBatches)
total := 0
for i := 0; i < numBatches-1; i++ {
rollout.RolloutBatches[i].Replicas = avg
total += avg.IntValue()
total := totalSize
for total > 0 {
for i := numBatches - 1; i >= 0 && total > 0; i-- {
replica := rollout.RolloutBatches[i].Replicas.IntValue() + 1
rollout.RolloutBatches[i].Replicas = intstr.FromInt(replica)
total--
}
}
// fill out the last batch
rollout.RolloutBatches[numBatches-1].Replicas = intstr.FromInt(totalSize - total)
}
}

View File

@@ -0,0 +1,99 @@
/*
Copyright 2021 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 rollout
import (
"testing"
"k8s.io/utils/pointer"
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
)
func TestDefaultRolloutPlan_EvenlyDivid(t *testing.T) {
var numBatch int32 = 5
rollout := &v1alpha1.RolloutPlan{
TargetSize: &numBatch,
NumBatches: &numBatch,
}
DefaultRolloutPlan(rollout)
if len(rollout.RolloutBatches) != int(numBatch) {
t.Errorf("number of batch %d does not equal to %d ", len(rollout.RolloutBatches), numBatch)
}
for i, batch := range rollout.RolloutBatches {
if batch.Replicas.IntVal != int32(1) {
t.Errorf("batch %d replica does not equal to 1", i)
}
}
}
func TestDefaultRolloutPlan_HasRemanence(t *testing.T) {
var numBatch int32 = 5
rollout := &v1alpha1.RolloutPlan{
TargetSize: pointer.Int32Ptr(8),
NumBatches: &numBatch,
}
DefaultRolloutPlan(rollout)
if len(rollout.RolloutBatches) != int(numBatch) {
t.Errorf("number of batch %d does not equal to %d ", len(rollout.RolloutBatches), numBatch)
}
if rollout.RolloutBatches[0].Replicas.IntValue() != 1 {
t.Errorf("batch 0's replica %d does not equal to 1", rollout.RolloutBatches[0].Replicas.IntValue())
}
if rollout.RolloutBatches[1].Replicas.IntValue() != 1 {
t.Errorf("batch 1's replica %d does not equal to 1", rollout.RolloutBatches[1].Replicas.IntValue())
}
if rollout.RolloutBatches[2].Replicas.IntValue() != 2 {
t.Errorf("batch 2's replica %d does not equal to 2", rollout.RolloutBatches[2].Replicas.IntValue())
}
if rollout.RolloutBatches[3].Replicas.IntValue() != 2 {
t.Errorf("batch 3's replica %d does not equal to 2", rollout.RolloutBatches[3].Replicas.IntValue())
}
if rollout.RolloutBatches[4].Replicas.IntValue() != 2 {
t.Errorf("batch 4's replica %d does not equal to 2", rollout.RolloutBatches[4].Replicas.IntValue())
}
}
func TestDefaultRolloutPlan_NotEnough(t *testing.T) {
var numBatch int32 = 5
rollout := &v1alpha1.RolloutPlan{
TargetSize: pointer.Int32Ptr(4),
NumBatches: &numBatch,
}
DefaultRolloutPlan(rollout)
if len(rollout.RolloutBatches) != int(numBatch) {
t.Errorf("number of batch %d does not equal to %d ", len(rollout.RolloutBatches), numBatch)
}
if rollout.RolloutBatches[0].Replicas.IntValue() != 0 {
t.Errorf("batch 0's replica %d does not equal to 0", rollout.RolloutBatches[0].Replicas.IntValue())
}
if rollout.RolloutBatches[1].Replicas.IntValue() != 1 {
t.Errorf("batch 1's replica %d does not equal to 1", rollout.RolloutBatches[1].Replicas.IntValue())
}
if rollout.RolloutBatches[2].Replicas.IntValue() != 1 {
t.Errorf("batch 2's replica %d does not equal to 1", rollout.RolloutBatches[2].Replicas.IntValue())
}
if rollout.RolloutBatches[3].Replicas.IntValue() != 1 {
t.Errorf("batch 3's replica %d does not equal to 1", rollout.RolloutBatches[3].Replicas.IntValue())
}
if rollout.RolloutBatches[4].Replicas.IntValue() != 1 {
t.Errorf("batch 4's replica %d does not equal to 1", rollout.RolloutBatches[4].Replicas.IntValue())
}
}

View File

@@ -3,15 +3,11 @@ package controllers_test
import (
"context"
"fmt"
"strconv"
"time"
"github.com/oam-dev/kubevela/apis/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
cpv1 "github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -20,10 +16,10 @@ import (
oamcomm "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"
oamstd "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
@@ -32,10 +28,9 @@ var _ = Describe("Cloneset based rollout tests", func() {
ctx := context.Background()
var namespace string
var ns corev1.Namespace
var app v1alpha2.Application
var appConfig1, appConfig2 v1alpha2.ApplicationContext
var app v1beta1.Application
var kc kruise.CloneSet
var appRollout v1alpha2.AppRollout
var appRollout v1beta1.AppRollout
createNamespace := func(namespace string) {
ns = corev1.Namespace{
@@ -68,7 +63,7 @@ var _ = Describe("Cloneset based rollout tests", func() {
CreateClonesetDef := func() {
By("Install CloneSet based componentDefinition")
var cd v1alpha2.ComponentDefinition
var cd v1beta1.ComponentDefinition
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/clonesetDefinition.yaml", &cd)).Should(BeNil())
// create the componentDefinition if not exist
Eventually(
@@ -78,37 +73,9 @@ var _ = Describe("Cloneset based rollout tests", func() {
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
}
VerifyAppConfigTemplated := func(revision int64) {
var appConfigName string
By("Get Application latest status after AppConfig created")
Eventually(
func() int64 {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: app.Name}, &app)
return app.Status.LatestRevision.Revision
},
time.Second*30, time.Millisecond*500).Should(BeEquivalentTo(revision))
appConfigName = app.Status.LatestRevision.Name
By(fmt.Sprintf("Wait for AppConfig %s synced", appConfigName))
var appConfig v1alpha2.ApplicationContext
Eventually(
func() corev1.ConditionStatus {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appConfigName}, &appConfig)
return appConfig.Status.GetCondition(cpv1.TypeSynced).Status
},
time.Second*30, time.Millisecond*500).Should(BeEquivalentTo(corev1.ConditionTrue))
By(fmt.Sprintf("Wait for AppConfig %s to be templated", appConfigName))
Eventually(
func() types.RollingStatus {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appConfigName}, &appConfig)
return appConfig.Status.RollingStatus
},
time.Second*60, time.Millisecond*500).Should(BeEquivalentTo(types.RollingTemplated))
}
ApplySourceApp := func() {
applySourceApp := func() {
By("Apply an application")
var newApp v1alpha2.Application
var newApp v1beta1.Application
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/app-source.yaml", &newApp)).Should(BeNil())
newApp.Namespace = namespace
Expect(k8sClient.Create(ctx, &newApp)).Should(Succeed())
@@ -125,23 +92,9 @@ var _ = Describe("Cloneset based rollout tests", func() {
time.Second*30, time.Millisecond*500).ShouldNot(BeNil())
}
MarkAppRolling := func(revision int64) {
By("Mark the application as rolling")
Eventually(
func() error {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: app.Name}, &app)
app.SetAnnotations(util.MergeMapOverrideWithDst(app.GetAnnotations(),
map[string]string{oam.AnnotationRollingComponent: app.Spec.Components[0].Name,
oam.AnnotationAppRollout: strconv.FormatBool(true)}))
return k8sClient.Update(ctx, &app)
}, time.Second*5, time.Millisecond*500).Should(Succeed())
VerifyAppConfigTemplated(revision)
}
ApplyTargetApp := func() {
applyTargetApp := func() {
By("Update the application to target spec during rolling")
var targetApp v1alpha2.Application
var targetApp v1beta1.Application
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/app-target.yaml", &targetApp)).Should(BeNil())
Eventually(
@@ -152,7 +105,15 @@ var _ = Describe("Cloneset based rollout tests", func() {
}, time.Second*5, time.Millisecond*500).Should(Succeed())
}
VerifyRolloutOwnsCloneset := func() {
createAppRolling := func(newAppRollout *v1beta1.AppRollout) {
By("Mark the application as rolling")
Eventually(
func() error {
return k8sClient.Create(ctx, newAppRollout)
}, time.Second*5, time.Millisecond).Should(Succeed())
}
verifyRolloutOwnsCloneset := func() {
By("Verify that rollout controller owns the cloneset")
clonesetName := appRollout.Spec.ComponentList[0]
Eventually(
@@ -163,12 +124,12 @@ var _ = Describe("Cloneset based rollout tests", func() {
return ""
}
return clonesetOwner.Kind
}, time.Second*10, time.Second).Should(BeEquivalentTo(v1alpha2.AppRolloutKind))
}, time.Second*10, time.Second).Should(BeEquivalentTo(v1beta1.AppRolloutKind))
clonesetOwner := metav1.GetControllerOf(&kc)
Expect(clonesetOwner.APIVersion).Should(BeEquivalentTo(v1alpha2.SchemeGroupVersion.String()))
Expect(clonesetOwner.APIVersion).Should(BeEquivalentTo(v1beta1.SchemeGroupVersion.String()))
}
VerifyRolloutSucceeded := func(targetAppName string) {
verifyRolloutSucceeded := func(targetAppName string) {
By("Wait for the rollout phase change to succeed")
Eventually(
func() oamstd.RollingState {
@@ -210,7 +171,7 @@ var _ = Describe("Cloneset based rollout tests", func() {
Expect(kc.Status.UpdatedReadyReplicas).Should(BeEquivalentTo(*kc.Spec.Replicas))
}
VerifyAppConfigInactive := func(appConfigName string) {
verifyAppConfigInactive := func(appConfigName string) {
var appConfig v1alpha2.ApplicationContext
By("Verify AppConfig is inactive")
Eventually(
@@ -221,15 +182,15 @@ var _ = Describe("Cloneset based rollout tests", func() {
time.Second*30, time.Millisecond*500).Should(BeEquivalentTo(types.InactiveAfterRollingCompleted))
}
ApplyTwoAppVersion := func() {
applyTwoAppVersion := func() {
CreateClonesetDef()
ApplySourceApp()
ApplyTargetApp()
applySourceApp()
applyTargetApp()
}
RevertBackToSource := func() {
revertBackToSource := func() {
By("Revert the application back to source")
var sourceApp v1alpha2.Application
var sourceApp v1beta1.Application
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/app-source.yaml", &sourceApp)).Should(BeNil())
Eventually(
@@ -251,7 +212,6 @@ var _ = Describe("Cloneset based rollout tests", func() {
},
time.Second*5, time.Millisecond*500).Should(Succeed())
By("Verify AppConfig rolling status")
By("Wait for the rollout phase change to rolling in batches")
Eventually(
func() oamstd.RollingState {
@@ -260,9 +220,9 @@ var _ = Describe("Cloneset based rollout tests", func() {
},
time.Second*10, time.Millisecond*10).Should(BeEquivalentTo(oamstd.RollingInBatchesState))
VerifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
verifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
VerifyAppConfigInactive(appRollout.Spec.SourceAppRevisionName)
verifyAppConfigInactive(appRollout.Spec.SourceAppRevisionName)
// Clean up
k8sClient.Delete(ctx, &appRollout)
@@ -276,8 +236,6 @@ var _ = Describe("Cloneset based rollout tests", func() {
AfterEach(func() {
By("Clean up resources after a test")
k8sClient.Delete(ctx, &appConfig2)
k8sClient.Delete(ctx, &appConfig1)
k8sClient.Delete(ctx, &app)
By(fmt.Sprintf("Delete the entire namespace %s", ns.Name))
// delete the namespace with all its resources
@@ -287,32 +245,32 @@ var _ = Describe("Cloneset based rollout tests", func() {
It("Test cloneset rollout first time (no source)", func() {
CreateClonesetDef()
ApplySourceApp()
applySourceApp()
By("Apply the application rollout go directly to the target")
var newAppRollout v1alpha2.AppRollout
var newAppRollout v1beta1.AppRollout
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/app-rollout.yaml", &newAppRollout)).Should(BeNil())
newAppRollout.Namespace = namespace
newAppRollout.Spec.SourceAppRevisionName = ""
newAppRollout.Spec.TargetAppRevisionName = utils.ConstructRevisionName(app.GetName(), 1)
Expect(k8sClient.Create(ctx, &newAppRollout)).Should(Succeed())
createAppRolling(&newAppRollout)
appRollout.Name = newAppRollout.Name
VerifyRolloutSucceeded(newAppRollout.Spec.TargetAppRevisionName)
verifyRolloutSucceeded(newAppRollout.Spec.TargetAppRevisionName)
// Clean up
k8sClient.Delete(ctx, &appRollout)
})
It("Test cloneset rollout with a manual check", func() {
ApplyTwoAppVersion()
applyTwoAppVersion()
By("Apply the application rollout to deploy the source")
var newAppRollout v1alpha2.AppRollout
var newAppRollout v1beta1.AppRollout
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/app-rollout.yaml", &newAppRollout)).Should(BeNil())
newAppRollout.Namespace = namespace
newAppRollout.Spec.SourceAppRevisionName = ""
newAppRollout.Spec.TargetAppRevisionName = utils.ConstructRevisionName(app.GetName(), 1)
Expect(k8sClient.Create(ctx, &newAppRollout)).Should(Succeed())
createAppRolling(&newAppRollout)
appRollout.Name = newAppRollout.Name
VerifyRolloutSucceeded(newAppRollout.Spec.TargetAppRevisionName)
verifyRolloutSucceeded(newAppRollout.Spec.TargetAppRevisionName)
By("Apply the application rollout that stops after the first batch")
batchPartition := 0
@@ -356,24 +314,16 @@ var _ = Describe("Cloneset based rollout tests", func() {
Expect(appRollout.Status.BatchRollingState).Should(BeEquivalentTo(oamstd.BatchReadyState))
Expect(appRollout.Status.CurrentBatch).Should(BeEquivalentTo(batchPartition))
VerifyRolloutOwnsCloneset()
verifyRolloutOwnsCloneset()
By("Finish the application rollout")
// set the partition as the same size as the array
appRollout.Spec.RolloutPlan.BatchPartition = pointer.Int32Ptr(int32(len(appRollout.Spec.RolloutPlan.
RolloutBatches) - 1))
Expect(k8sClient.Update(ctx, &appRollout)).Should(Succeed())
By("Wait for the rollout phase change to rolling in batches")
Eventually(
func() oamstd.RollingState {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appRollout.Name}, &appRollout)
return appRollout.Status.RollingState
},
time.Second*10, time.Millisecond*10).Should(BeEquivalentTo(oamstd.RollingInBatchesState))
verifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
VerifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
VerifyAppConfigInactive(appRollout.Spec.SourceAppRevisionName)
verifyAppConfigInactive(appRollout.Spec.SourceAppRevisionName)
// Clean up
k8sClient.Delete(ctx, &appRollout)
@@ -381,14 +331,19 @@ var _ = Describe("Cloneset based rollout tests", func() {
It("Test pause and modify rollout plan after rolling succeeded", func() {
CreateClonesetDef()
ApplySourceApp()
applySourceApp()
By("Apply the application rollout go directly to the target")
var newAppRollout v1alpha2.AppRollout
var newAppRollout v1beta1.AppRollout
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/app-rollout.yaml", &newAppRollout)).Should(BeNil())
newAppRollout.Namespace = namespace
newAppRollout.Spec.SourceAppRevisionName = ""
newAppRollout.Spec.TargetAppRevisionName = utils.ConstructRevisionName(app.GetName(), 1)
Expect(k8sClient.Create(ctx, &newAppRollout)).Should(Succeed())
newAppRollout.Spec.RolloutPlan.BatchPartition = nil
newAppRollout.Spec.RolloutPlan.RolloutBatches = nil
// webhook would fill the batches
newAppRollout.Spec.RolloutPlan.TargetSize = pointer.Int32Ptr(5)
newAppRollout.Spec.RolloutPlan.NumBatches = pointer.Int32Ptr(5)
createAppRolling(&newAppRollout)
By("Wait for the rollout phase change to initialize")
Eventually(
@@ -396,7 +351,7 @@ var _ = Describe("Cloneset based rollout tests", func() {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: newAppRollout.Name}, &appRollout)
return appRollout.Status.RollingState
},
time.Second*10, time.Millisecond*50).Should(BeEquivalentTo(oamstd.RollingInBatchesState))
time.Second*10, time.Millisecond).Should(BeEquivalentTo(oamstd.RollingInBatchesState))
By("Pause the rollout")
Eventually(
@@ -428,21 +383,20 @@ var _ = Describe("Cloneset based rollout tests", func() {
}
Expect((&lt).Before(&beforeSleep)).Should(BeTrue())
VerifyRolloutOwnsCloneset()
verifyRolloutOwnsCloneset()
By("Finish the application rollout")
// remove the batch restriction
appRollout.Spec.RolloutPlan.Paused = false
appRollout.Spec.RolloutPlan.BatchPartition = nil
Expect(k8sClient.Update(ctx, &appRollout)).Should(Succeed())
VerifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
verifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
// record the transition time
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appRollout.Name}, &appRollout)
lt = appRollout.Status.GetCondition(oamstd.RolloutSucceed).LastTransitionTime
// nothing should happen, the transition time should be the same
VerifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
verifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appRollout.Name}, &appRollout)
Expect(appRollout.Status.RollingState).Should(BeEquivalentTo(oamstd.RolloutSucceedState))
Expect(appRollout.Status.GetCondition(oamstd.RolloutSucceed).LastTransitionTime).Should(BeEquivalentTo(lt))
@@ -452,17 +406,54 @@ var _ = Describe("Cloneset based rollout tests", func() {
})
It("Test rolling back after a successful rollout", func() {
ApplyTwoAppVersion()
applyTwoAppVersion()
By("Apply the application rollout to deploy the source")
var newAppRollout v1alpha2.AppRollout
var newAppRollout v1beta1.AppRollout
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/app-rollout.yaml", &newAppRollout)).Should(BeNil())
newAppRollout.Namespace = namespace
newAppRollout.Spec.SourceAppRevisionName = ""
newAppRollout.Spec.TargetAppRevisionName = utils.ConstructRevisionName(app.GetName(), 1)
Expect(k8sClient.Create(ctx, &newAppRollout)).Should(Succeed())
createAppRolling(&newAppRollout)
appRollout.Name = newAppRollout.Name
VerifyRolloutSucceeded(newAppRollout.Spec.TargetAppRevisionName)
verifyRolloutSucceeded(newAppRollout.Spec.TargetAppRevisionName)
By("Finish the application rollout")
Eventually(
func() error {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appRollout.Name}, &appRollout)
appRollout.Spec.SourceAppRevisionName = utils.ConstructRevisionName(app.GetName(), 1)
appRollout.Spec.TargetAppRevisionName = utils.ConstructRevisionName(app.GetName(), 2)
appRollout.Spec.RolloutPlan.BatchPartition = nil
return k8sClient.Update(ctx, &appRollout)
}, time.Second*5, time.Millisecond).Should(Succeed())
By("Wait for the rollout phase change to rolling in batches")
Eventually(
func() oamstd.RollingState {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appRollout.Name}, &appRollout)
return appRollout.Status.RollingState
},
time.Second*10, time.Millisecond*10).Should(BeEquivalentTo(oamstd.RollingInBatchesState))
verifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
verifyAppConfigInactive(appRollout.Spec.SourceAppRevisionName)
revertBackToSource()
})
It("Test rolling back in the middle of rollout", func() {
applyTwoAppVersion()
By("Apply the application rollout to deploy the source")
var newAppRollout v1beta1.AppRollout
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/app-rollout.yaml", &newAppRollout)).Should(BeNil())
newAppRollout.Namespace = namespace
newAppRollout.Spec.SourceAppRevisionName = ""
newAppRollout.Spec.TargetAppRevisionName = utils.ConstructRevisionName(app.GetName(), 1)
createAppRolling(&newAppRollout)
appRollout.Name = newAppRollout.Name
verifyRolloutSucceeded(newAppRollout.Spec.TargetAppRevisionName)
By("Finish the application rollout")
Eventually(
@@ -482,61 +473,14 @@ var _ = Describe("Cloneset based rollout tests", func() {
},
time.Second*10, time.Millisecond*10).Should(BeEquivalentTo(oamstd.RollingInBatchesState))
VerifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
VerifyAppConfigInactive(appRollout.Spec.SourceAppRevisionName)
RevertBackToSource()
})
It("Test rolling back in the middle of rollout", func() {
ApplyTwoAppVersion()
By("Apply the application rollout to deploy the source")
var newAppRollout v1alpha2.AppRollout
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/app-rollout.yaml", &newAppRollout)).Should(BeNil())
newAppRollout.Namespace = namespace
newAppRollout.Spec.SourceAppRevisionName = ""
newAppRollout.Spec.TargetAppRevisionName = utils.ConstructRevisionName(app.GetName(), 1)
Expect(k8sClient.Create(ctx, &newAppRollout)).Should(Succeed())
appRollout.Name = newAppRollout.Name
VerifyRolloutSucceeded(newAppRollout.Spec.TargetAppRevisionName)
By("Finish the application rollout")
batchPartition := 1
Eventually(
func() error {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appRollout.Name}, &appRollout)
appRollout.Spec.SourceAppRevisionName = utils.ConstructRevisionName(app.GetName(), 1)
appRollout.Spec.TargetAppRevisionName = utils.ConstructRevisionName(app.GetName(), 2)
appRollout.Spec.RolloutPlan.BatchPartition = pointer.Int32Ptr(int32(batchPartition))
return k8sClient.Update(ctx, &appRollout)
}, time.Second*5, time.Millisecond*500).Should(Succeed())
By("Wait for the rollout phase change to rolling in batches")
Eventually(
func() oamstd.RollingState {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: newAppRollout.Name}, &appRollout)
return appRollout.Status.RollingState
},
time.Second*10, time.Millisecond*500).Should(BeEquivalentTo(oamstd.RollingInBatchesState))
By("Wait for rollout to start the batch")
Eventually(
func() int32 {
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appRollout.Name}, &appRollout)
return appRollout.Status.CurrentBatch
},
time.Second*60, time.Millisecond*500).Should(BeEquivalentTo(batchPartition))
RevertBackToSource()
revertBackToSource()
})
PIt("Test rolling by changing the definition", func() {
CreateClonesetDef()
ApplySourceApp()
MarkAppRolling(1)
applySourceApp()
By("Apply the definition change")
var cd, newCD v1alpha2.ComponentDefinition
var cd, newCD v1beta1.ComponentDefinition
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/clonesetDefinitionModified.yaml.yaml", &newCD)).Should(BeNil())
Eventually(
func() error {
@@ -545,19 +489,16 @@ var _ = Describe("Cloneset based rollout tests", func() {
return k8sClient.Update(ctx, &cd)
},
time.Second*3, time.Millisecond*300).Should(Succeed())
VerifyAppConfigTemplated(2)
By("Apply the application rollout")
var newAppRollout v1alpha2.AppRollout
var newAppRollout v1beta1.AppRollout
Expect(common.ReadYamlToObject("testdata/rollout/cloneset/app-rollout.yaml", &newAppRollout)).Should(BeNil())
newAppRollout.Namespace = namespace
newAppRollout.Spec.RolloutPlan.BatchPartition = pointer.Int32Ptr(int32(len(newAppRollout.Spec.RolloutPlan.
RolloutBatches) - 1))
Expect(k8sClient.Create(ctx, &newAppRollout)).Should(Succeed())
VerifyRolloutSucceeded(appConfig2.Name)
VerifyAppConfigInactive(appConfig1.Name)
createAppRolling(&newAppRollout)
verifyRolloutSucceeded(appRollout.Spec.TargetAppRevisionName)
verifyAppConfigInactive(appRollout.Spec.SourceAppRevisionName)
// Clean up
k8sClient.Delete(ctx, &appRollout)
})