mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-19 04:26:39 +00:00
Chore: remove outdated apis(v1alppha2 ApplicationConfiguration Component, and HealthScope, Rollout) (#6041)
* remove outdated api Signed-off-by: Somefive <yd219913@alibaba-inc.com> * fix rt test: no component rt Signed-off-by: Somefive <yd219913@alibaba-inc.com> * recover context.revision to component hash Signed-off-by: Somefive <yd219913@alibaba-inc.com> --------- Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
@@ -1,26 +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 common
|
||||
|
||||
// error msg for common usage
|
||||
const (
|
||||
ErrLocatingWorkload = "failed to locate the workload"
|
||||
ErrLocatingService = "failed to locate any the services"
|
||||
ErrCreatingService = "failed to create the services"
|
||||
|
||||
ErrUpdateStatus = "failed to update status"
|
||||
)
|
||||
@@ -1,386 +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 rollout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
kruisev1 "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
apps "k8s.io/api/apps/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"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/common"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/common/rollout/workloads"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
// the default time to check back if we still have work to do
|
||||
const rolloutReconcileRequeueTime = 5 * time.Second
|
||||
|
||||
// Controller is the controller that controls the rollout plan resource
|
||||
type Controller struct {
|
||||
client client.Client
|
||||
recorder event.Recorder
|
||||
parentController oam.Object
|
||||
|
||||
rolloutSpec *v1alpha1.RolloutPlan
|
||||
rolloutStatus *v1alpha1.RolloutStatus
|
||||
|
||||
targetWorkload *unstructured.Unstructured
|
||||
sourceWorkload *unstructured.Unstructured
|
||||
}
|
||||
|
||||
// NewRolloutPlanController creates a RolloutPlanController
|
||||
func NewRolloutPlanController(client client.Client, parentController oam.Object, recorder event.Recorder,
|
||||
rolloutSpec *v1alpha1.RolloutPlan, rolloutStatus *v1alpha1.RolloutStatus,
|
||||
targetWorkload, sourceWorkload *unstructured.Unstructured) *Controller {
|
||||
initializedRolloutStatus := rolloutStatus.DeepCopy()
|
||||
if len(initializedRolloutStatus.BatchRollingState) == 0 {
|
||||
initializedRolloutStatus.BatchRollingState = v1alpha1.BatchInitializingState
|
||||
}
|
||||
return &Controller{
|
||||
client: client,
|
||||
parentController: parentController,
|
||||
recorder: recorder,
|
||||
rolloutSpec: rolloutSpec.DeepCopy(),
|
||||
rolloutStatus: initializedRolloutStatus,
|
||||
targetWorkload: targetWorkload,
|
||||
sourceWorkload: sourceWorkload,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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))
|
||||
}
|
||||
klog.InfoS("rollout status", "rollout state", r.rolloutStatus.RollingState, "batch rolling state",
|
||||
r.rolloutStatus.BatchRollingState, "current batch", r.rolloutStatus.CurrentBatch, "upgraded Replicas",
|
||||
r.rolloutStatus.UpgradedReplicas, "ready Replicas", r.rolloutStatus.UpgradedReadyReplicas)
|
||||
|
||||
defer func() {
|
||||
klog.InfoS("Finished one round of reconciling rollout plan", "rollout state", status.RollingState,
|
||||
"batch rolling state", status.BatchRollingState, "current batch", status.CurrentBatch,
|
||||
"upgraded Replicas", status.UpgradedReplicas, "ready Replicas", status.UpgradedReadyReplicas,
|
||||
"reconcile result ", res)
|
||||
}()
|
||||
status = r.rolloutStatus
|
||||
|
||||
defer func() {
|
||||
if status.RollingState == v1alpha1.RolloutFailedState ||
|
||||
status.RollingState == v1alpha1.RolloutSucceedState {
|
||||
// no need to requeue if we reach the terminal states
|
||||
res = reconcile.Result{}
|
||||
} else {
|
||||
res = reconcile.Result{
|
||||
RequeueAfter: rolloutReconcileRequeueTime,
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
workloadController, err := r.GetWorkloadController()
|
||||
if err != nil {
|
||||
r.rolloutStatus.RolloutFailed(err.Error())
|
||||
r.recorder.Event(r.parentController, event.Warning("Unsupported workload", err))
|
||||
return
|
||||
}
|
||||
|
||||
switch r.rolloutStatus.RollingState {
|
||||
case v1alpha1.VerifyingSpecState:
|
||||
verified, err := workloadController.VerifySpec(ctx)
|
||||
if err != nil {
|
||||
// we can fail it right away, everything after initialized need to be finalized
|
||||
r.rolloutStatus.RolloutFailed(err.Error())
|
||||
} else if verified {
|
||||
r.rolloutStatus.StateTransition(v1alpha1.RollingSpecVerifiedEvent)
|
||||
}
|
||||
|
||||
case v1alpha1.InitializingState:
|
||||
if err = r.initializeRollout(ctx); err == nil {
|
||||
initialized, err := workloadController.Initialize(ctx)
|
||||
if err != nil {
|
||||
r.rolloutStatus.RolloutFailing(err.Error())
|
||||
} else if initialized {
|
||||
r.rolloutStatus.StateTransition(v1alpha1.RollingInitializedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
case v1alpha1.RollingInBatchesState:
|
||||
r.reconcileBatchInRolling(ctx, workloadController)
|
||||
|
||||
case v1alpha1.RolloutFailingState, v1alpha1.RolloutAbandoningState, v1alpha1.RolloutDeletingState:
|
||||
if succeed := workloadController.Finalize(ctx, false); succeed {
|
||||
r.finalizeRollout(ctx)
|
||||
}
|
||||
|
||||
case v1alpha1.FinalisingState:
|
||||
if succeed := workloadController.Finalize(ctx, true); succeed {
|
||||
r.finalizeRollout(ctx)
|
||||
}
|
||||
|
||||
case v1alpha1.RolloutSucceedState:
|
||||
// Nothing to do
|
||||
|
||||
case v1alpha1.RolloutFailedState:
|
||||
// Nothing to do
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("illegal rollout status %+v", r.rolloutStatus))
|
||||
}
|
||||
|
||||
return res, r.rolloutStatus
|
||||
}
|
||||
|
||||
// reconcile logic when we are in the middle of rollout, we have to go through finalizing state before succeed or fail
|
||||
func (r *Controller) reconcileBatchInRolling(ctx context.Context, workloadController workloads.WorkloadController) {
|
||||
if r.rolloutSpec.Paused {
|
||||
r.recorder.Event(r.parentController, event.Normal("Rollout paused", "Rollout paused"))
|
||||
r.rolloutStatus.SetConditions(v1alpha1.NewPositiveCondition(v1alpha1.BatchPaused))
|
||||
return
|
||||
}
|
||||
|
||||
switch r.rolloutStatus.BatchRollingState {
|
||||
case v1alpha1.BatchInitializingState:
|
||||
r.initializeOneBatch(ctx)
|
||||
|
||||
case v1alpha1.BatchInRollingState:
|
||||
// still rolling the batch, the batch rolling is not completed yet
|
||||
upgradeDone, err := workloadController.RolloutOneBatchPods(ctx)
|
||||
if err != nil {
|
||||
r.rolloutStatus.RolloutFailing(err.Error())
|
||||
} else if upgradeDone {
|
||||
r.rolloutStatus.StateTransition(v1alpha1.RolloutOneBatchEvent)
|
||||
}
|
||||
|
||||
case v1alpha1.BatchVerifyingState:
|
||||
// verifying if the application is ready to roll
|
||||
// need to check if they meet the availability requirements in the rollout spec.
|
||||
// TODO: evaluate any metrics/analysis
|
||||
// TODO: We may need to go back to rollout again if the size of the resource can change behind our back
|
||||
verified, err := workloadController.CheckOneBatchPods(ctx)
|
||||
if err != nil {
|
||||
r.rolloutStatus.RolloutFailing(err.Error())
|
||||
} else if verified {
|
||||
r.rolloutStatus.StateTransition(v1alpha1.OneBatchAvailableEvent)
|
||||
}
|
||||
|
||||
case v1alpha1.BatchFinalizingState:
|
||||
// finalize one batch
|
||||
finalized, err := workloadController.FinalizeOneBatch(ctx)
|
||||
if err != nil {
|
||||
r.rolloutStatus.RolloutFailing(err.Error())
|
||||
} else if finalized {
|
||||
r.finalizeOneBatch(ctx)
|
||||
}
|
||||
|
||||
case v1alpha1.BatchReadyState:
|
||||
// all the pods in the are upgraded and their state are ready
|
||||
// wait to move to the next batch if there are any
|
||||
r.tryMovingToNextBatch()
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("illegal status %+v", r.rolloutStatus))
|
||||
}
|
||||
}
|
||||
|
||||
// all the common initialize work before we rollout
|
||||
// TODO: fail the rollout if the webhook call is explicitly rejected (through http status code)
|
||||
func (r *Controller) initializeRollout(ctx context.Context) error {
|
||||
// call the pre-rollout webhooks
|
||||
for _, rw := range r.rolloutSpec.RolloutWebhooks {
|
||||
if rw.Type == v1alpha1.InitializeRolloutHook {
|
||||
err := callWebhook(ctx, r.parentController, string(v1alpha1.InitializingState), rw)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to invoke a webhook",
|
||||
"webhook name", rw.Name, "webhook end point", rw.URL)
|
||||
r.rolloutStatus.RolloutRetry("failed to invoke a webhook")
|
||||
return err
|
||||
}
|
||||
klog.InfoS("successfully invoked a pre rollout webhook", "webhook name", rw.Name, "webhook end point",
|
||||
rw.URL)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// all the common initialize work before we rollout one batch of resources
|
||||
func (r *Controller) initializeOneBatch(ctx context.Context) {
|
||||
rolloutHooks := r.gatherAllWebhooks()
|
||||
// call all the pre-batch rollout webhooks
|
||||
for _, rh := range rolloutHooks {
|
||||
if rh.Type == v1alpha1.PreBatchRolloutHook {
|
||||
err := callWebhook(ctx, r.parentController, string(v1alpha1.BatchInitializingState), rh)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to invoke a webhook",
|
||||
"webhook name", rh.Name, "webhook end point", rh.URL)
|
||||
r.rolloutStatus.RolloutRetry("failed to invoke a webhook")
|
||||
return
|
||||
}
|
||||
klog.InfoS("successfully invoked a pre batch webhook", "webhook name", rh.Name, "webhook end point",
|
||||
rh.URL)
|
||||
}
|
||||
}
|
||||
r.rolloutStatus.StateTransition(v1alpha1.InitializedOneBatchEvent)
|
||||
}
|
||||
|
||||
func (r *Controller) gatherAllWebhooks() []v1alpha1.RolloutWebhook {
|
||||
// we go through the rollout level webhooks first
|
||||
rolloutHooks := r.rolloutSpec.RolloutWebhooks
|
||||
// we then append the batch specific rollout webhooks to the overall webhooks
|
||||
// order matters here
|
||||
currentBatch := int(r.rolloutStatus.CurrentBatch)
|
||||
rolloutHooks = append(rolloutHooks, r.rolloutSpec.RolloutBatches[currentBatch].BatchRolloutWebhooks...)
|
||||
return rolloutHooks
|
||||
}
|
||||
|
||||
// check if we can move to the next batch
|
||||
func (r *Controller) tryMovingToNextBatch() {
|
||||
if r.rolloutSpec.BatchPartition == nil || *r.rolloutSpec.BatchPartition > r.rolloutStatus.CurrentBatch {
|
||||
klog.InfoS("ready to rollout the next batch", "current batch", r.rolloutStatus.CurrentBatch)
|
||||
r.rolloutStatus.StateTransition(v1alpha1.BatchRolloutApprovedEvent)
|
||||
} else {
|
||||
klog.V(common.LogDebug).InfoS("the current batch is waiting to move on", "current batch",
|
||||
r.rolloutStatus.CurrentBatch)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Controller) finalizeOneBatch(ctx context.Context) {
|
||||
rolloutHooks := r.gatherAllWebhooks()
|
||||
// call all the post-batch rollout webhooks
|
||||
for _, rh := range rolloutHooks {
|
||||
if rh.Type == v1alpha1.PostBatchRolloutHook {
|
||||
err := callWebhook(ctx, r.parentController, string(v1alpha1.BatchFinalizingState), rh)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to invoke a webhook",
|
||||
"webhook name", rh.Name, "webhook end point", rh.URL)
|
||||
r.rolloutStatus.RolloutRetry("failed to invoke a webhook")
|
||||
return
|
||||
}
|
||||
klog.InfoS("successfully invoked a post batch webhook", "webhook name", rh.Name, "webhook end point",
|
||||
rh.URL)
|
||||
}
|
||||
}
|
||||
// calculate the next phase
|
||||
currentBatch := int(r.rolloutStatus.CurrentBatch)
|
||||
if currentBatch == len(r.rolloutSpec.RolloutBatches)-1 {
|
||||
// this is the last batch, mark the rollout finalized
|
||||
r.rolloutStatus.StateTransition(v1alpha1.AllBatchFinishedEvent)
|
||||
r.recorder.Event(r.parentController, event.Normal("All batches rolled out",
|
||||
fmt.Sprintf("upgrade pod = %d, total ready pod = %d", r.rolloutStatus.UpgradedReplicas,
|
||||
r.rolloutStatus.UpgradedReadyReplicas)))
|
||||
} else {
|
||||
klog.InfoS("finished one batch rollout", "current batch", r.rolloutStatus.CurrentBatch)
|
||||
// th
|
||||
r.recorder.Event(r.parentController, event.Normal("Batch Finalized",
|
||||
fmt.Sprintf("Batch %d is finalized and ready to go", r.rolloutStatus.CurrentBatch)))
|
||||
r.rolloutStatus.StateTransition(v1alpha1.FinishedOneBatchEvent)
|
||||
}
|
||||
}
|
||||
|
||||
// all the common finalize work after we rollout
|
||||
func (r *Controller) finalizeRollout(ctx context.Context) {
|
||||
// call the post-rollout webhooks
|
||||
for _, rw := range r.rolloutSpec.RolloutWebhooks {
|
||||
if rw.Type == v1alpha1.FinalizeRolloutHook {
|
||||
err := callWebhook(ctx, r.parentController, string(r.rolloutStatus.RollingState), rw)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to invoke a webhook",
|
||||
"webhook name", rw.Name, "webhook end point", rw.URL)
|
||||
r.rolloutStatus.RolloutRetry("failed to invoke a post rollout webhook")
|
||||
return
|
||||
}
|
||||
klog.InfoS("successfully invoked a post rollout webhook", "webhook name", rw.Name, "webhook end point",
|
||||
rw.URL)
|
||||
}
|
||||
}
|
||||
r.rolloutStatus.StateTransition(v1alpha1.RollingFinalizedEvent)
|
||||
}
|
||||
|
||||
// GetWorkloadController pick the right workload controller to work on the workload
|
||||
func (r *Controller) GetWorkloadController() (workloads.WorkloadController, error) {
|
||||
kind := r.targetWorkload.GetObjectKind().GroupVersionKind().Kind
|
||||
target := types.NamespacedName{
|
||||
Namespace: r.targetWorkload.GetNamespace(),
|
||||
Name: r.targetWorkload.GetName(),
|
||||
}
|
||||
var source types.NamespacedName
|
||||
if r.sourceWorkload != nil {
|
||||
source.Namespace = r.sourceWorkload.GetNamespace()
|
||||
source.Name = r.sourceWorkload.GetName()
|
||||
}
|
||||
|
||||
if r.targetWorkload.GroupVersionKind().Group == kruisev1.GroupVersion.Group {
|
||||
// check if the target workload is CloneSet
|
||||
if r.targetWorkload.GetKind() == reflect.TypeOf(kruisev1.CloneSet{}).Name() {
|
||||
// check whether current rollout plan is for workload rolling or scaling
|
||||
if r.sourceWorkload != nil {
|
||||
klog.InfoS("using cloneset rollout controller for this rolloutplan", "source workload name", source.Name, "namespace",
|
||||
source.Namespace, "target workload name", target.Name, "namespace",
|
||||
target.Namespace)
|
||||
return workloads.NewCloneSetRolloutController(r.client, r.recorder, r.parentController,
|
||||
r.rolloutSpec, r.rolloutStatus, target), nil
|
||||
}
|
||||
klog.InfoS("using cloneset scale controller for this rolloutplan", "target workload name", target.Name, "namespace",
|
||||
target.Namespace)
|
||||
return workloads.NewCloneSetScaleController(r.client, r.recorder, r.parentController,
|
||||
r.rolloutSpec, r.rolloutStatus, target), nil
|
||||
}
|
||||
}
|
||||
|
||||
if r.targetWorkload.GroupVersionKind().Group == apps.GroupName {
|
||||
// check if the target workload is Deployment
|
||||
if r.targetWorkload.GetKind() == reflect.TypeOf(apps.Deployment{}).Name() {
|
||||
// check whether current rollout plan is for workload rolling or scaling
|
||||
if r.sourceWorkload != nil {
|
||||
klog.InfoS("using deployment rollout controller for this rolloutplan", "source workload name", source.Name, "namespace",
|
||||
source.Namespace, "target workload name", target.Name, "namespace",
|
||||
target.Namespace)
|
||||
return workloads.NewDeploymentRolloutController(r.client, r.recorder, r.parentController,
|
||||
r.rolloutSpec, r.rolloutStatus, source, target), nil
|
||||
}
|
||||
klog.InfoS("using deployment scale controller for this rolloutplan", "target workload name", target.Name, "namespace",
|
||||
target.Namespace)
|
||||
return workloads.NewDeploymentScaleController(r.client, r.recorder, r.parentController,
|
||||
r.rolloutSpec, r.rolloutStatus, target), nil
|
||||
}
|
||||
|
||||
// check if the target workload is StatefulSet
|
||||
if r.targetWorkload.GetKind() == reflect.TypeOf(apps.StatefulSet{}).Name() {
|
||||
// check whether current rollout plan is for workload rolling or scaling
|
||||
if r.sourceWorkload != nil {
|
||||
return workloads.NewStatefulSetRolloutController(r.client, r.recorder, r.parentController,
|
||||
r.rolloutSpec, r.rolloutStatus, target), nil
|
||||
}
|
||||
return workloads.NewStatefulSetScaleController(r.client, r.recorder, r.parentController,
|
||||
r.rolloutSpec, r.rolloutStatus, target), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("the workload kind `%s` is not supported", kind)
|
||||
}
|
||||
@@ -1,65 +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 rollout
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
)
|
||||
|
||||
func Test_TryMovingToNextBatch(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
r Controller
|
||||
rolloutSpec *v1alpha1.RolloutPlan
|
||||
rolloutStatus *v1alpha1.RolloutStatus
|
||||
wantNextBatch int32
|
||||
wantBatchRollingState v1alpha1.BatchRollingState
|
||||
}{
|
||||
"stay at the same batch": {
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{
|
||||
BatchPartition: pointer.Int32(3),
|
||||
},
|
||||
rolloutStatus: &v1alpha1.RolloutStatus{
|
||||
CurrentBatch: 2,
|
||||
RollingState: v1alpha1.RollingInBatchesState,
|
||||
BatchRollingState: v1alpha1.BatchReadyState,
|
||||
},
|
||||
wantNextBatch: 3,
|
||||
wantBatchRollingState: v1alpha1.BatchInitializingState,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r := &Controller{
|
||||
rolloutSpec: tt.rolloutSpec,
|
||||
rolloutStatus: tt.rolloutStatus,
|
||||
}
|
||||
r.tryMovingToNextBatch()
|
||||
if r.rolloutStatus.CurrentBatch != tt.wantNextBatch {
|
||||
t.Errorf("\n%s\n batch miss match: want batch `%d`, got batch:`%d`\n", name,
|
||||
tt.wantNextBatch, r.rolloutStatus.CurrentBatch)
|
||||
}
|
||||
if r.rolloutStatus.BatchRollingState != tt.wantBatchRollingState {
|
||||
t.Errorf("\n%s\nstate miss match: want state `%s`, got state:`%s`\n", name,
|
||||
tt.wantBatchRollingState, r.rolloutStatus.BatchRollingState)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,131 +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 rollout
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
)
|
||||
|
||||
// issue an http call to the an end ponit
|
||||
func makeHTTPRequest(ctx context.Context, webhookEndPoint, method string, payload interface{}) ([]byte, int, error) {
|
||||
payloadBin, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
hook, err := url.Parse(webhookEndPoint)
|
||||
if err != nil {
|
||||
return nil, http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), method, hook.String(), bytes.NewBuffer(payloadBin))
|
||||
if err != nil {
|
||||
return nil, http.StatusInternalServerError, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// issue request with retry
|
||||
var r *http.Response
|
||||
var body []byte
|
||||
err = retry.OnError(retry.DefaultBackoff,
|
||||
func(error) bool {
|
||||
// not sure what not to retry on
|
||||
return true
|
||||
}, func() error {
|
||||
var requestErr error
|
||||
r, requestErr = http.DefaultClient.Do(req.WithContext(ctx))
|
||||
defer func() {
|
||||
if r != nil {
|
||||
_ = r.Body.Close()
|
||||
}
|
||||
}()
|
||||
if requestErr != nil {
|
||||
return requestErr
|
||||
}
|
||||
body, requestErr = io.ReadAll(r.Body)
|
||||
if requestErr != nil {
|
||||
return requestErr
|
||||
}
|
||||
if r.StatusCode >= http.StatusInternalServerError {
|
||||
requestErr = fmt.Errorf("internal server error, status code = %d", r.StatusCode)
|
||||
}
|
||||
return requestErr
|
||||
})
|
||||
|
||||
// failed even with retry
|
||||
if err != nil {
|
||||
if r != nil {
|
||||
return nil, r.StatusCode, err
|
||||
}
|
||||
return nil, -1, err
|
||||
}
|
||||
return body, r.StatusCode, nil
|
||||
}
|
||||
|
||||
// callWebhook does a HTTP POST to an external service and
|
||||
// returns an error if the response status code is non-2xx
|
||||
func callWebhook(ctx context.Context, resource klog.KMetadata, phase string, rw v1alpha1.RolloutWebhook) error {
|
||||
payload := v1alpha1.RolloutWebhookPayload{
|
||||
Name: resource.GetName(),
|
||||
Namespace: resource.GetNamespace(),
|
||||
Phase: phase,
|
||||
}
|
||||
|
||||
if rw.Metadata != nil {
|
||||
payload.Metadata = *rw.Metadata
|
||||
}
|
||||
// make the http request
|
||||
if len(rw.Method) == 0 {
|
||||
rw.Method = http.MethodPost
|
||||
}
|
||||
_, status, err := makeHTTPRequest(ctx, rw.URL, rw.Method, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(rw.ExpectedStatus) == 0 {
|
||||
if status > http.StatusAccepted {
|
||||
err := fmt.Errorf("we fail the webhook request based on status, http status = %d", status)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// check if the returned status is expected
|
||||
accepted := false
|
||||
for _, es := range rw.ExpectedStatus {
|
||||
if es == status {
|
||||
accepted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !accepted {
|
||||
err := fmt.Errorf("http request to the webhook not accepeted, http status = %d", status)
|
||||
klog.ErrorS(err, "The status is not expected", "expected status", rw.ExpectedStatus)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,246 +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 rollout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
const mockUrlBase = "127.0.0.1:"
|
||||
|
||||
func TestMakeHTTPRequest(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
type mockHTTPParameter struct {
|
||||
method string
|
||||
statusCode int
|
||||
body string
|
||||
}
|
||||
type want struct {
|
||||
err error
|
||||
statusCode int
|
||||
body string
|
||||
}
|
||||
tests := map[string]struct {
|
||||
url string
|
||||
method string
|
||||
payload interface{}
|
||||
httpParameter mockHTTPParameter
|
||||
want want
|
||||
}{
|
||||
"Test normal case": {
|
||||
method: http.MethodPost,
|
||||
payload: "doesn't matter",
|
||||
httpParameter: mockHTTPParameter{
|
||||
method: http.MethodPost,
|
||||
statusCode: http.StatusAccepted,
|
||||
body: "all good",
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
statusCode: http.StatusAccepted,
|
||||
body: "all good",
|
||||
},
|
||||
},
|
||||
"Test http failed case with retry": {
|
||||
url: "127.0.0.1:13622",
|
||||
method: http.MethodPost,
|
||||
payload: "doesn't matter",
|
||||
httpParameter: mockHTTPParameter{
|
||||
method: http.MethodGet,
|
||||
statusCode: http.StatusAccepted,
|
||||
body: "doesn't matter",
|
||||
},
|
||||
want: want{
|
||||
err: fmt.Errorf("internal server error, status code = %d", http.StatusNotImplemented),
|
||||
statusCode: -1,
|
||||
body: "",
|
||||
},
|
||||
},
|
||||
"Test failed case with retry": {
|
||||
method: http.MethodPost,
|
||||
payload: "doesn't matter",
|
||||
httpParameter: mockHTTPParameter{
|
||||
method: http.MethodPost,
|
||||
statusCode: http.StatusNotImplemented,
|
||||
body: "please retry",
|
||||
},
|
||||
want: want{
|
||||
err: fmt.Errorf("internal server error, status code = %d", http.StatusNotImplemented),
|
||||
statusCode: http.StatusNotImplemented,
|
||||
body: "",
|
||||
},
|
||||
},
|
||||
"Test client error failed case": {
|
||||
method: http.MethodPost,
|
||||
payload: "doesn't matter",
|
||||
httpParameter: mockHTTPParameter{
|
||||
method: http.MethodPost,
|
||||
statusCode: http.StatusBadRequest,
|
||||
body: "bad request",
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
statusCode: http.StatusBadRequest,
|
||||
body: "bad request",
|
||||
},
|
||||
},
|
||||
}
|
||||
for testName, tt := range tests {
|
||||
func(testName string) {
|
||||
mockUrl := mockUrlBase + strconv.FormatInt(rand.Int63n(128)+2000, 10)
|
||||
// generate a test server so we can capture and inspect the request
|
||||
testServer := NewMock(tt.httpParameter.method, mockUrl, tt.httpParameter.statusCode, tt.httpParameter.body)
|
||||
defer testServer.Close()
|
||||
if len(tt.url) == 0 {
|
||||
tt.url = mockUrl
|
||||
}
|
||||
gotReply, gotCode, gotErr := makeHTTPRequest(ctx, "http://"+tt.url, tt.method, tt.payload)
|
||||
if gotCode != tt.want.statusCode {
|
||||
t.Errorf("\n%s\nr.Reconcile(...): want code `%d`, got code:`%d` got err: %v \n", testName, tt.want.statusCode,
|
||||
gotCode, gotErr)
|
||||
}
|
||||
if gotCode == -1 {
|
||||
// we don't know exactly what error we should get when the network call failed
|
||||
if gotErr == nil {
|
||||
t.Errorf("\n%s\nr.Reconcile(...): want some error, got error:`%s`\n", testName, gotErr)
|
||||
}
|
||||
} else {
|
||||
if (tt.want.err == nil && gotErr != nil) || (tt.want.err != nil && gotErr == nil) {
|
||||
t.Errorf("\n%s\nr.Reconcile(...): want error `%s`, got error:`%s`\n", testName, tt.want.err, gotErr)
|
||||
}
|
||||
if tt.want.err != nil && gotErr != nil && gotErr.Error() != tt.want.err.Error() {
|
||||
t.Errorf("\n%s\nr.Reconcile(...): want error `%s`, got error:`%s`\n", testName, tt.want.err, gotErr)
|
||||
}
|
||||
}
|
||||
if string(gotReply) != tt.want.body {
|
||||
t.Errorf("\n%s\nr.Reconcile(...): want reply `%s`, got reply:`%s`\n", testName, tt.want.body, string(gotReply))
|
||||
}
|
||||
}(testName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallWebhook(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
body := "all good"
|
||||
res := appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "name",
|
||||
Namespace: "namespace",
|
||||
},
|
||||
}
|
||||
type args struct {
|
||||
resource oam.Object
|
||||
phase string
|
||||
rw v1alpha1.RolloutWebhook
|
||||
}
|
||||
tests := map[string]struct {
|
||||
returnedStatusCode int
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
"Test success case": {
|
||||
returnedStatusCode: http.StatusAccepted,
|
||||
args: args{
|
||||
resource: &res,
|
||||
phase: string(v1alpha1.RollingInBatchesState),
|
||||
rw: v1alpha1.RolloutWebhook{},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
"Test failed default case": {
|
||||
returnedStatusCode: http.StatusAlreadyReported,
|
||||
args: args{
|
||||
resource: &res,
|
||||
phase: string(v1alpha1.RollingInBatchesState),
|
||||
rw: v1alpha1.RolloutWebhook{},
|
||||
},
|
||||
wantErr: fmt.Errorf("we fail the webhook request based on status, http status = %d", http.StatusAlreadyReported),
|
||||
},
|
||||
"Test expected treated as success case": {
|
||||
returnedStatusCode: http.StatusAlreadyReported,
|
||||
args: args{
|
||||
resource: &res,
|
||||
phase: string(v1alpha1.RollingInBatchesState),
|
||||
rw: v1alpha1.RolloutWebhook{
|
||||
ExpectedStatus: []int{http.StatusNoContent, http.StatusAlreadyReported},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
"Test not expected treated as failed case": {
|
||||
returnedStatusCode: http.StatusGone,
|
||||
args: args{
|
||||
resource: &res,
|
||||
phase: string(v1alpha1.RolloutFailedState),
|
||||
rw: v1alpha1.RolloutWebhook{
|
||||
ExpectedStatus: []int{http.StatusNoContent, http.StatusAlreadyReported},
|
||||
},
|
||||
},
|
||||
wantErr: fmt.Errorf("http request to the webhook not accepeted, http status = %d", http.StatusGone),
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
func(name string) {
|
||||
url := mockUrlBase + strconv.FormatInt(rand.Int63n(4848)+2000, 10)
|
||||
tt.args.rw.URL = "http://" + url
|
||||
// generate a test server so we can capture and inspect the request
|
||||
testServer := NewMock(http.MethodPost, url, tt.returnedStatusCode, body)
|
||||
defer testServer.Close()
|
||||
|
||||
gotErr := callWebhook(ctx, tt.args.resource, tt.args.phase, tt.args.rw)
|
||||
if (tt.wantErr == nil && gotErr != nil) || (tt.wantErr != nil && gotErr == nil) {
|
||||
t.Errorf("\n%s\nr.Reconcile(...): want error `%s`, got error:`%s`\n", name, tt.wantErr, gotErr)
|
||||
}
|
||||
if tt.wantErr != nil && gotErr != nil && gotErr.Error() != tt.wantErr.Error() {
|
||||
t.Errorf("\n%s\nr.Reconcile(...): want error `%s`, got error:`%s`\n", name, tt.wantErr, gotErr)
|
||||
}
|
||||
}(name)
|
||||
}
|
||||
}
|
||||
|
||||
func NewMock(method, mockUrl string, statusCode int, body string) *httptest.Server {
|
||||
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == method {
|
||||
w.WriteHeader(statusCode)
|
||||
w.Write([]byte(body))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
}))
|
||||
l, err := net.Listen("tcp", mockUrl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ts.Listener.Close()
|
||||
ts.Listener = l
|
||||
ts.Start()
|
||||
return ts
|
||||
}
|
||||
@@ -1,322 +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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
"github.com/pkg/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
// CloneSetRolloutController is responsible for handle rollout Cloneset type of workloads
|
||||
type CloneSetRolloutController struct {
|
||||
cloneSetController
|
||||
}
|
||||
|
||||
// NewCloneSetRolloutController creates a new Cloneset rollout controller
|
||||
func NewCloneSetRolloutController(client client.Client, recorder event.Recorder, parentController oam.Object,
|
||||
rolloutSpec *v1alpha1.RolloutPlan, rolloutStatus *v1alpha1.RolloutStatus, workloadName types.NamespacedName) *CloneSetRolloutController {
|
||||
return &CloneSetRolloutController{
|
||||
cloneSetController: cloneSetController{
|
||||
workloadController: workloadController{
|
||||
client: client,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: workloadName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VerifySpec verifies that the target rollout resource is consistent with the rollout spec
|
||||
func (c *CloneSetRolloutController) VerifySpec(ctx context.Context) (bool, error) {
|
||||
var verifyErr error
|
||||
defer func() {
|
||||
if verifyErr != nil {
|
||||
klog.Error(verifyErr)
|
||||
c.recorder.Event(c.parentController, event.Warning("VerifyFailed", verifyErr))
|
||||
}
|
||||
}()
|
||||
|
||||
// fetch the cloneset and get its current size
|
||||
currentReplicas, verifyErr := c.size(ctx)
|
||||
if verifyErr != nil {
|
||||
// do not fail the rollout because we can't get the resource
|
||||
c.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// the cloneset size has to be the same as the current size
|
||||
if c.cloneSet.Spec.Replicas != nil && *c.cloneSet.Spec.Replicas != c.cloneSet.Status.Replicas {
|
||||
verifyErr = fmt.Errorf("the cloneset is still scaling, target = %d, cloneset size = %d",
|
||||
*c.cloneSet.Spec.Replicas, c.cloneSet.Status.Replicas)
|
||||
// we can wait for the cloneset scale operation to finish
|
||||
c.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// make sure that the updateRevision is different from what we have already done
|
||||
targetHash, verifyErr := utils.ComputeSpecHash(c.cloneSet.Spec)
|
||||
if verifyErr != nil {
|
||||
// do not fail the rollout because we can't compute the hash value for some reason
|
||||
c.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if targetHash == c.rolloutStatus.LastAppliedPodTemplateIdentifier {
|
||||
return false, fmt.Errorf("there is no difference between the source and target, hash = %s", targetHash)
|
||||
}
|
||||
|
||||
// check if the rollout batch replicas added up to the Cloneset replicas
|
||||
if verifyErr = c.verifyRolloutBatchReplicaValue(currentReplicas); verifyErr != nil {
|
||||
return false, verifyErr
|
||||
}
|
||||
|
||||
// record the size
|
||||
klog.InfoS("record the target size", "total replicas", currentReplicas)
|
||||
c.rolloutStatus.RolloutTargetSize = currentReplicas
|
||||
c.rolloutStatus.RolloutOriginalSize = currentReplicas
|
||||
|
||||
// check if the cloneset is disabled
|
||||
if !c.cloneSet.Spec.UpdateStrategy.Paused {
|
||||
return false, fmt.Errorf("the cloneset %s is in the middle of updating, need to be paused first",
|
||||
c.cloneSet.GetName())
|
||||
}
|
||||
|
||||
// check if the cloneset has any controller
|
||||
if controller := metav1.GetControllerOf(c.cloneSet); controller != nil {
|
||||
return false, fmt.Errorf("the cloneset %s has a controller owner %s",
|
||||
c.cloneSet.GetName(), controller.String())
|
||||
}
|
||||
|
||||
// mark the rollout verified
|
||||
c.recorder.Event(c.parentController, event.Normal("Rollout Verified",
|
||||
"Rollout spec and the CloneSet resource are verified"))
|
||||
// record the new pod template hash only if it succeeds
|
||||
c.rolloutStatus.NewPodTemplateIdentifier = targetHash
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Initialize makes sure that the cloneset is under our control
|
||||
func (c *CloneSetRolloutController) Initialize(ctx context.Context) (bool, error) {
|
||||
totalReplicas, err := c.size(ctx)
|
||||
if err != nil {
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if controller := metav1.GetControllerOf(c.cloneSet); controller != nil {
|
||||
if controller.Kind == v1alpha1.RolloutKind && controller.APIVersion == v1alpha1.SchemeGroupVersion.String() {
|
||||
// it's already there
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
// add the parent controller to the owner of the cloneset
|
||||
// before kicking start the update and start from every pod in the old version
|
||||
clonePatch := client.MergeFrom(c.cloneSet.DeepCopy())
|
||||
ref := metav1.NewControllerRef(c.parentController, c.parentController.GetObjectKind().GroupVersionKind())
|
||||
c.cloneSet.SetOwnerReferences(append(c.cloneSet.GetOwnerReferences(), *ref))
|
||||
c.cloneSet.Spec.UpdateStrategy.Paused = false
|
||||
c.cloneSet.Spec.UpdateStrategy.Partition = &intstr.IntOrString{Type: intstr.Int, IntVal: totalReplicas}
|
||||
|
||||
// patch the CloneSet
|
||||
if err := c.client.Patch(ctx, c.cloneSet, clonePatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
|
||||
c.recorder.Event(c.parentController, event.Warning("Failed to the start the cloneset update", err))
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
// mark the rollout initialized
|
||||
c.recorder.Event(c.parentController, event.Normal("Rollout Initialized", "Rollout resource are initialized"))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RolloutOneBatchPods calculates the number of pods we can upgrade once according to the rollout spec
|
||||
// and then set the partition accordingly, return if we are done
|
||||
func (c *CloneSetRolloutController) RolloutOneBatchPods(ctx context.Context) (bool, error) {
|
||||
// calculate what's the total pods that should be upgraded given the currentBatch in the status
|
||||
cloneSetSize, err := c.size(ctx)
|
||||
if err != nil {
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
newPodTarget := calculateNewBatchTarget(c.rolloutSpec, 0, int(cloneSetSize), int(c.rolloutStatus.CurrentBatch))
|
||||
// set the Partition as the desired number of pods in old revisions.
|
||||
clonePatch := client.MergeFrom(c.cloneSet.DeepCopy())
|
||||
c.cloneSet.Spec.UpdateStrategy.Partition = &intstr.IntOrString{Type: intstr.Int,
|
||||
IntVal: cloneSetSize - int32(newPodTarget)}
|
||||
// patch the Cloneset
|
||||
if err = c.client.Patch(ctx, c.cloneSet, clonePatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
|
||||
c.recorder.Event(c.parentController, event.Warning("Failed to update the cloneset to upgrade", err))
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
// record the upgrade
|
||||
klog.InfoS("upgraded one batch", "current batch", c.rolloutStatus.CurrentBatch)
|
||||
c.recorder.Event(c.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("Submitted upgrade quest for batch %d", c.rolloutStatus.CurrentBatch)))
|
||||
c.rolloutStatus.UpgradedReplicas = int32(newPodTarget)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckOneBatchPods checks to see if enough pods are upgraded according to the rollout plan
|
||||
func (c *CloneSetRolloutController) CheckOneBatchPods(ctx context.Context) (bool, error) {
|
||||
cloneSetSize, err := c.size(ctx)
|
||||
if err != nil {
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
newPodTarget := calculateNewBatchTarget(c.rolloutSpec, 0, int(cloneSetSize), int(c.rolloutStatus.CurrentBatch))
|
||||
// get the number of ready pod from cloneset
|
||||
readyPodCount := int(c.cloneSet.Status.UpdatedReadyReplicas)
|
||||
if len(c.rolloutSpec.RolloutBatches) <= int(c.rolloutStatus.CurrentBatch) {
|
||||
err = errors.New("somehow, currentBatch number exceeded the rolloutBatches spec")
|
||||
klog.ErrorS(err, "total batch", len(c.rolloutSpec.RolloutBatches), "current batch",
|
||||
c.rolloutStatus.CurrentBatch)
|
||||
return false, err
|
||||
}
|
||||
currentBatch := c.rolloutSpec.RolloutBatches[c.rolloutStatus.CurrentBatch]
|
||||
unavail := 0
|
||||
if currentBatch.MaxUnavailable != nil {
|
||||
unavail, _ = intstr.GetValueFromIntOrPercent(currentBatch.MaxUnavailable, int(cloneSetSize), true)
|
||||
}
|
||||
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", c.rolloutStatus.CurrentBatch)
|
||||
c.recorder.Event(c.parentController, event.Normal("Batch Available",
|
||||
fmt.Sprintf("Batch %d is available", c.rolloutStatus.CurrentBatch)))
|
||||
return true, nil
|
||||
}
|
||||
// continue to verify
|
||||
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
|
||||
}
|
||||
|
||||
// FinalizeOneBatch makes sure that the upgradedReplicas and current batch in the status are valid according to the spec
|
||||
func (c *CloneSetRolloutController) FinalizeOneBatch(ctx context.Context) (bool, error) {
|
||||
status := c.rolloutStatus
|
||||
spec := c.rolloutSpec
|
||||
if spec.BatchPartition != nil && *spec.BatchPartition < status.CurrentBatch {
|
||||
err := fmt.Errorf("the current batch value in the status is greater than the batch partition")
|
||||
klog.ErrorS(err, "we have moved past the user defined partition", "user specified batch partition",
|
||||
*spec.BatchPartition, "current batch we are working on", status.CurrentBatch)
|
||||
return false, err
|
||||
}
|
||||
upgradedReplicas := int(status.UpgradedReplicas)
|
||||
currentBatch := int(status.CurrentBatch)
|
||||
// calculate the lower bound of the possible pod count just before the current batch
|
||||
podCount := calculateNewBatchTarget(c.rolloutSpec, 0, int(c.rolloutStatus.RolloutTargetSize), currentBatch-1)
|
||||
// the recorded number should be at least as much as the all the pods before the current batch
|
||||
if podCount > upgradedReplicas {
|
||||
err := fmt.Errorf("the upgraded replica in the status is less than all the pods in the previous batch")
|
||||
klog.ErrorS(err, "rollout status inconsistent", "upgraded num status", upgradedReplicas,
|
||||
"pods in all the previous batches", podCount)
|
||||
return false, err
|
||||
}
|
||||
// calculate the upper bound with the current batch
|
||||
podCount = calculateNewBatchTarget(c.rolloutSpec, 0, int(c.rolloutStatus.RolloutTargetSize), currentBatch)
|
||||
// the recorded number should be not as much as the all the pods including the active batch
|
||||
if podCount < upgradedReplicas {
|
||||
err := fmt.Errorf("the upgraded replica in the status is greater than all the pods in the current batch")
|
||||
klog.ErrorS(err, "rollout status inconsistent", "total target size", c.rolloutStatus.RolloutTargetSize,
|
||||
"upgraded num status", upgradedReplicas, "pods in the batches including the current batch", podCount)
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Finalize makes sure the Cloneset is all upgraded
|
||||
func (c *CloneSetRolloutController) Finalize(ctx context.Context, succeed bool) bool {
|
||||
if err := c.fetchCloneSet(ctx); err != nil {
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false
|
||||
}
|
||||
clonePatch := client.MergeFrom(c.cloneSet.DeepCopy())
|
||||
// remove the parent controller from the resources' owner list
|
||||
var newOwnerList []metav1.OwnerReference
|
||||
isOwner := false
|
||||
for _, owner := range c.cloneSet.GetOwnerReferences() {
|
||||
if owner.Kind == c.parentController.GetObjectKind().GroupVersionKind().Kind &&
|
||||
owner.APIVersion == c.parentController.GetObjectKind().GroupVersionKind().GroupVersion().String() &&
|
||||
owner.Controller != nil && *owner.Controller {
|
||||
isOwner = true
|
||||
continue
|
||||
}
|
||||
newOwnerList = append(newOwnerList, owner)
|
||||
}
|
||||
if !isOwner {
|
||||
// nothing to do if we are already not the owner
|
||||
klog.InfoS("the cloneset is already released and not controlled by rollout", "cloneSet", c.cloneSet.Name)
|
||||
return true
|
||||
}
|
||||
c.cloneSet.SetOwnerReferences(newOwnerList)
|
||||
// pause the resource when the rollout failed so we can try again next time
|
||||
if !succeed {
|
||||
c.cloneSet.Spec.UpdateStrategy.Paused = true
|
||||
}
|
||||
// patch the CloneSet
|
||||
if err := c.client.Patch(ctx, c.cloneSet, clonePatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
|
||||
c.recorder.Event(c.parentController, event.Warning("Failed to the finalize the cloneset", err))
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false
|
||||
}
|
||||
// mark the resource finalized
|
||||
c.recorder.Event(c.parentController, event.Normal("Rollout Finalized",
|
||||
fmt.Sprintf("Rollout resource are finalized, succeed := %t", succeed)))
|
||||
c.rolloutStatus.LastAppliedPodTemplateIdentifier = c.rolloutStatus.NewPodTemplateIdentifier
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// The functions below are helper functions
|
||||
// ---------------------------------------------
|
||||
|
||||
// check if the replicas in all the rollout batches add up to the right number
|
||||
func (c *CloneSetRolloutController) verifyRolloutBatchReplicaValue(currentReplicas int32) error {
|
||||
// the target size has to be the same as the cloneset size
|
||||
if c.rolloutSpec.TargetSize != nil && *c.rolloutSpec.TargetSize != currentReplicas {
|
||||
return fmt.Errorf("the rollout plan is attempting to scale the cloneset, target = %d, cloneset size = %d",
|
||||
*c.rolloutSpec.TargetSize, currentReplicas)
|
||||
}
|
||||
// use a common function to check if the sum of all the batches can match the cloneset size
|
||||
err := verifyBatchesWithRollout(c.rolloutSpec, currentReplicas)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,472 +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 workloads
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("cloneset controller", func() {
|
||||
var (
|
||||
c CloneSetRolloutController
|
||||
ns corev1.Namespace
|
||||
name string
|
||||
namespace string
|
||||
cloneSet kruise.CloneSet
|
||||
namespacedName client.ObjectKey
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
namespace = "rollout-ns"
|
||||
name = "rollout1"
|
||||
appRollout := v1alpha1.Rollout{TypeMeta: metav1.TypeMeta{APIVersion: v1alpha1.SchemeGroupVersion.String(), Kind: v1alpha1.RolloutKind}, ObjectMeta: metav1.ObjectMeta{Name: name}}
|
||||
namespacedName = client.ObjectKey{Name: name, Namespace: namespace}
|
||||
c = CloneSetRolloutController{
|
||||
cloneSetController: cloneSetController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
rolloutStatus: &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState},
|
||||
parentController: &appRollout,
|
||||
recorder: event.NewAPIRecorder(mgr.GetEventRecorderFor("Rollout")).
|
||||
WithAnnotations("controller", "Rollout"),
|
||||
},
|
||||
targetNamespacedName: namespacedName,
|
||||
},
|
||||
}
|
||||
|
||||
cloneSet = kruise.CloneSet{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: kruise.GroupVersion.String(), Kind: "CloneSet"},
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
|
||||
Spec: kruise.CloneSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"env": "staging"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"env": "staging"}},
|
||||
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: name, Image: "nginx"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ns = corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
By("Create a namespace")
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
By("clean up")
|
||||
k8sClient.Delete(ctx, &cloneSet)
|
||||
})
|
||||
|
||||
Context("TestNewCloneSetRolloutController", func() {
|
||||
It("init a CloneSet Rollout Controller", func() {
|
||||
recorder := event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout")
|
||||
parentController := &v1alpha1.Rollout{ObjectMeta: metav1.ObjectMeta{Name: name}}
|
||||
rolloutSpec := &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
rolloutStatus := &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState}
|
||||
workloadNamespacedName := client.ObjectKey{Name: name, Namespace: namespace}
|
||||
got := NewCloneSetRolloutController(k8sClient, recorder, parentController, rolloutSpec, rolloutStatus, workloadNamespacedName)
|
||||
c := &CloneSetRolloutController{
|
||||
cloneSetController: cloneSetController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: workloadNamespacedName,
|
||||
},
|
||||
}
|
||||
Expect(got).Should(Equal(c))
|
||||
})
|
||||
})
|
||||
|
||||
Context("VerifySpec", func() {
|
||||
It("could not fetch CloneSet workload", func() {
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("verify rollout spec hash", func() {
|
||||
By("Create a CloneSet")
|
||||
cloneSet.Spec.UpdateStrategy.Paused = true
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("the cloneset is in the middle of updating", func() {
|
||||
By("Create a CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("setting a dummy pod identifier so it's different")
|
||||
c.rolloutStatus.LastAppliedPodTemplateIdentifier = "abc"
|
||||
|
||||
By("verify should fail because it's not paused")
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err).Should(Equal(fmt.Errorf("the cloneset rollout1 is in the middle of updating, need to be paused first")))
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("spec is valid", func() {
|
||||
By("Create a CloneSet and set as paused")
|
||||
cloneSet.Spec.UpdateStrategy.Paused = true
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("setting a dummy pod identifier so it's different")
|
||||
c.rolloutStatus.LastAppliedPodTemplateIdentifier = "abc"
|
||||
|
||||
By("verify should pass and record the size")
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeTrue())
|
||||
Expect(c.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(1))
|
||||
Expect(c.rolloutStatus.RolloutOriginalSize).Should(BeEquivalentTo(1))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestInitialize", func() {
|
||||
BeforeEach(func() {
|
||||
cloneSet.Spec.UpdateStrategy.Paused = true
|
||||
})
|
||||
|
||||
It("could not fetch CloneSet workload", func() {
|
||||
consistent, err := c.Initialize(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("failed to patch the owner of CloneSet", func() {
|
||||
By("Create a CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("initialize will fail because cloneset has wrong owner reference")
|
||||
initialized, err := c.Initialize(ctx)
|
||||
Expect(initialized).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("workload CloneSet is controlled by appRollout already", func() {
|
||||
By("Create a CloneSet")
|
||||
cloneSet.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("initialize succeed without patching")
|
||||
initialized, err := c.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(k8sClient.Get(ctx, c.targetNamespacedName, &cloneSet)).Should(Succeed())
|
||||
Expect(len(cloneSet.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
})
|
||||
|
||||
It("successfully initialized CloneSet", func() {
|
||||
By("create cloneset")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("initialize succeeds")
|
||||
c.parentController.SetUID("1231586900")
|
||||
initialized, err := c.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(k8sClient.Get(ctx, c.targetNamespacedName, &cloneSet)).Should(Succeed())
|
||||
Expect(len(cloneSet.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestRolloutOneBatchPods", func() {
|
||||
It("could not fetch CloneSet workload", func() {
|
||||
consistent, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("successfully rollout, current batch number is not equal to the expected one", func() {
|
||||
By("Create a CloneSet")
|
||||
cloneSet.Spec.Replicas = pointer.Int32(10)
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("rollout the second batch of current cloneset")
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
c.rolloutSpec.RolloutBatches = []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
}
|
||||
done, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(3))
|
||||
Expect(k8sClient.Get(ctx, c.targetNamespacedName, &cloneSet)).Should(Succeed())
|
||||
Expect(cloneSet.Spec.UpdateStrategy.Partition.IntValue()).Should(BeEquivalentTo(7))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestCheckOneBatchPods", func() {
|
||||
BeforeEach(func() {
|
||||
cloneSet.Spec.Replicas = pointer.Int32(10)
|
||||
c.rolloutSpec.RolloutBatches = []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("could not fetch CloneSet workload", func() {
|
||||
consistent, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("current ready Pod is less than expected", func() {
|
||||
By("Create the CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
By("Update the CloneSet status")
|
||||
cloneSet.Status.UpdatedReadyReplicas = 3
|
||||
cloneSet.Status.UpdatedReplicas = 4
|
||||
Expect(k8sClient.Status().Update(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("checking should fail as not enough pod ready")
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
done, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(c.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(cloneSet.Status.UpdatedReadyReplicas))
|
||||
})
|
||||
|
||||
It("failed to check batch Pod when current batch number exceeds the expected ones", func() {
|
||||
By("Create a CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("checking")
|
||||
c.rolloutStatus.CurrentBatch = 3
|
||||
done, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("currentBatch number exceeded the rolloutBatches spec"))
|
||||
})
|
||||
|
||||
It("there are enough pods counting the unavailable", func() {
|
||||
By("Create the CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
By("Update the CloneSet status")
|
||||
cloneSet.Status.UpdatedReadyReplicas = 3
|
||||
cloneSet.Status.UpdatedReplicas = 4
|
||||
Expect(k8sClient.Status().Update(ctx, &cloneSet)).Should(Succeed())
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
// set the rollout batch spec allow unavailable
|
||||
perc := intstr.FromString("20%")
|
||||
c.rolloutSpec.RolloutBatches = []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: perc,
|
||||
MaxUnavailable: &perc,
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
}
|
||||
By("checking one batch")
|
||||
done, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(c.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(cloneSet.Status.UpdatedReadyReplicas))
|
||||
})
|
||||
|
||||
It("there are enough pods ready", func() {
|
||||
By("Create the CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
By("Update the CloneSet status")
|
||||
cloneSet.Status.UpdatedReadyReplicas = 10
|
||||
cloneSet.Status.UpdatedReplicas = 10
|
||||
Expect(k8sClient.Status().Update(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("the second batch should pass when there are more pods upgraded already")
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
done, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(c.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(cloneSet.Status.UpdatedReadyReplicas))
|
||||
|
||||
By("checking the last batch")
|
||||
c.rolloutStatus.CurrentBatch = 2
|
||||
done, err = c.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(c.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(cloneSet.Status.UpdatedReadyReplicas))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalizeOneBatch", func() {
|
||||
BeforeEach(func() {
|
||||
c.rolloutStatus.RolloutTargetSize = 10
|
||||
c.rolloutSpec.RolloutBatches = []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("test illegal batch partition", func() {
|
||||
By("finalizing one batch")
|
||||
c.rolloutSpec.BatchPartition = pointer.Int32(2)
|
||||
c.rolloutStatus.CurrentBatch = 3
|
||||
done, err := c.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("the current batch value in the status is greater than the batch partition"))
|
||||
})
|
||||
|
||||
It("test too few upgraded", func() {
|
||||
By("finalizing one batch")
|
||||
c.rolloutStatus.UpgradedReplicas = 2
|
||||
c.rolloutStatus.CurrentBatch = 2
|
||||
done, err := c.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("is less than all the pods in the previous batch"))
|
||||
})
|
||||
|
||||
It("test too many upgraded", func() {
|
||||
By("finalizing one batch")
|
||||
c.rolloutStatus.UpgradedReplicas = 5
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
done, err := c.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("is greater than all the pods in the current batch"))
|
||||
})
|
||||
|
||||
It("test upgraded in the range", func() {
|
||||
By("finalizing one batch")
|
||||
c.rolloutStatus.UpgradedReplicas = 3
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
done, err := c.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalize", func() {
|
||||
It("failed to fetch CloneSet", func() {
|
||||
By("finalizing")
|
||||
finalized := c.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("Already finalize CloneSet", func() {
|
||||
By("Create a CloneSet")
|
||||
cloneSet.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: "notRollout",
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("finalizing without patch")
|
||||
finalized := c.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("successfully to finalize CloneSet", func() {
|
||||
By("Create a CloneSet")
|
||||
cloneSet.SetOwnerReferences([]metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
},
|
||||
{
|
||||
APIVersion: corev1.SchemeGroupVersion.String(),
|
||||
Kind: "Deployment",
|
||||
Name: "def",
|
||||
UID: "998877745",
|
||||
},
|
||||
})
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("finalizing with patch")
|
||||
finalized := c.Finalize(ctx, false)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
Expect(k8sClient.Get(ctx, c.targetNamespacedName, &cloneSet)).Should(Succeed())
|
||||
Expect(len(cloneSet.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
Expect(cloneSet.GetOwnerReferences()[0].Kind).Should(Equal("Deployment"))
|
||||
Expect(cloneSet.Spec.UpdateStrategy.Paused).Should(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,101 +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 workloads
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
)
|
||||
|
||||
func TestVerifyRolloutBatchReplicaValue4CloneSet(t *testing.T) {
|
||||
// Compared to `deployment_controller_test.go`, there is one case less as common is already 100% covered, so only an
|
||||
// error and nil error for `err := VerifySumOfBatchSizes(c.rolloutSpec, totalReplicas)` is enough.
|
||||
var int2 int32 = 2
|
||||
cases := map[string]struct {
|
||||
c *CloneSetRolloutController
|
||||
totalReplicas int32
|
||||
want error
|
||||
}{
|
||||
"ClonsetTargetSizeIsNotAvaialbe": {
|
||||
c: &CloneSetRolloutController{
|
||||
cloneSetController: cloneSetController{
|
||||
workloadController: workloadController{
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{
|
||||
TargetSize: &int2,
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalReplicas: 3,
|
||||
want: fmt.Errorf("the rollout plan is attempting to scale the cloneset, target = 2, cloneset size = 3"),
|
||||
},
|
||||
"BatchSizeMismatchesClonesetSize": {
|
||||
c: &CloneSetRolloutController{
|
||||
cloneSetController: cloneSetController{
|
||||
workloadController: workloadController{
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalReplicas: 3,
|
||||
want: fmt.Errorf("the rollout plan batch size mismatch, total batch size = 1, totalReplicas size = 3"),
|
||||
},
|
||||
"BatchSizeMatchesCloneSetSize": {
|
||||
c: &CloneSetRolloutController{
|
||||
cloneSetController: cloneSetController{
|
||||
workloadController: workloadController{
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalReplicas: 3,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := tc.c.verifyRolloutBatchReplicaValue(tc.totalReplicas)
|
||||
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nverifyRolloutBatchReplicaValue(...): -want error, +got error:\n%s", name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,304 +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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
// CloneSetScaleController is responsible for handle scale Cloneset type of workloads
|
||||
type CloneSetScaleController struct {
|
||||
cloneSetController
|
||||
}
|
||||
|
||||
// NewCloneSetScaleController creates CloneSet scale controller
|
||||
func NewCloneSetScaleController(client client.Client, recorder event.Recorder, parentController oam.Object, rolloutSpec *v1alpha1.RolloutPlan, rolloutStatus *v1alpha1.RolloutStatus, workloadName types.NamespacedName) *CloneSetScaleController {
|
||||
return &CloneSetScaleController{
|
||||
cloneSetController: cloneSetController{
|
||||
workloadController: workloadController{
|
||||
client: client,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: workloadName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VerifySpec verifies that the cloneset is stable and can be scaled
|
||||
func (s *CloneSetScaleController) VerifySpec(ctx context.Context) (bool, error) {
|
||||
var verifyErr error
|
||||
defer func() {
|
||||
if verifyErr != nil {
|
||||
klog.Error(verifyErr)
|
||||
s.recorder.Event(s.parentController, event.Warning("VerifyFailed", verifyErr))
|
||||
}
|
||||
}()
|
||||
|
||||
// the rollout has to have a target size in the scale case
|
||||
if s.rolloutSpec.TargetSize == nil {
|
||||
return false, fmt.Errorf("the rollout plan is attempting to scale the cloneset %s without a target",
|
||||
s.targetNamespacedName.Name)
|
||||
}
|
||||
// record the target size
|
||||
s.rolloutStatus.RolloutTargetSize = *s.rolloutSpec.TargetSize
|
||||
klog.InfoS("record the target size", "target size", *s.rolloutSpec.TargetSize)
|
||||
|
||||
// fetch the cloneset and get its current size
|
||||
originalSize, verifyErr := s.size(ctx)
|
||||
if verifyErr != nil {
|
||||
// do not fail the rollout because we can't get the resource
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
s.rolloutStatus.RolloutOriginalSize = originalSize
|
||||
klog.InfoS("record the original size", "original size", originalSize)
|
||||
|
||||
// check if the rollout batch replicas scale up/down to the replicas target
|
||||
if verifyErr = verifyBatchesWithScale(s.rolloutSpec, int(originalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize)); verifyErr != nil {
|
||||
return false, verifyErr
|
||||
}
|
||||
|
||||
// check if the cloneset is scaling
|
||||
if originalSize != s.cloneSet.Status.Replicas {
|
||||
verifyErr = fmt.Errorf("the cloneset %s is in the middle of scaling, target size = %d, real size = %d",
|
||||
s.cloneSet.GetName(), originalSize, s.cloneSet.Status.Replicas)
|
||||
// do not fail the rollout, we can wait
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check if the cloneset is upgrading
|
||||
if !s.cloneSet.Spec.UpdateStrategy.Paused && s.cloneSet.Status.UpdatedReplicas != originalSize {
|
||||
verifyErr = fmt.Errorf("the cloneset %s is in the middle of updating, target size = %d, updated pod = %d",
|
||||
s.cloneSet.GetName(), originalSize, s.cloneSet.Status.UpdatedReplicas)
|
||||
// do not fail the rollout, we can wait
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check if the cloneset has any controller
|
||||
if controller := metav1.GetControllerOf(s.cloneSet); controller != nil {
|
||||
return false, fmt.Errorf("the cloneset %s has a controller owner %s",
|
||||
s.cloneSet.GetName(), controller.String())
|
||||
}
|
||||
|
||||
// mark the scale verified
|
||||
s.recorder.Event(s.parentController, event.Normal("Scale Verified",
|
||||
"Rollout spec and the CloneSet resource are verified"))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Initialize makes sure that the cloneset is under our control
|
||||
func (s *CloneSetScaleController) Initialize(ctx context.Context) (bool, error) {
|
||||
err := s.fetchCloneSet(ctx)
|
||||
if err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if controller := metav1.GetControllerOf(s.cloneSet); controller != nil {
|
||||
if controller.Kind == v1alpha1.RolloutKind && controller.APIVersion == v1alpha1.SchemeGroupVersion.String() {
|
||||
|
||||
// it's already there
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
// add the parent controller to the owner of the cloneset
|
||||
clonePatch := client.MergeFrom(s.cloneSet.DeepCopy())
|
||||
ref := metav1.NewControllerRef(s.parentController, s.parentController.GetObjectKind().GroupVersionKind())
|
||||
s.cloneSet.SetOwnerReferences(append(s.cloneSet.GetOwnerReferences(), *ref))
|
||||
s.cloneSet.Spec.UpdateStrategy.Paused = false
|
||||
|
||||
// patch the CloneSet
|
||||
if err := s.client.Patch(ctx, s.cloneSet, clonePatch, client.FieldOwner(s.parentController.GetUID())); err != nil {
|
||||
s.recorder.Event(s.parentController, event.Warning("Failed to the start the cloneset update", err))
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
// mark the rollout initialized
|
||||
s.recorder.Event(s.parentController, event.Normal("Scale Initialized", "Cloneset is initialized"))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RolloutOneBatchPods calculates the number of pods we can scale to according to the rollout spec
|
||||
func (s *CloneSetScaleController) RolloutOneBatchPods(ctx context.Context) (bool, error) {
|
||||
err := s.fetchCloneSet(ctx)
|
||||
if err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
clonePatch := client.MergeFrom(s.cloneSet.DeepCopy())
|
||||
// set the replica according to the batch
|
||||
newPodTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), int(s.rolloutStatus.CurrentBatch))
|
||||
s.cloneSet.Spec.Replicas = pointer.Int32(int32(newPodTarget))
|
||||
// patch the Cloneset
|
||||
if err := s.client.Patch(ctx, s.cloneSet, clonePatch, client.FieldOwner(s.parentController.GetUID())); err != nil {
|
||||
s.recorder.Event(s.parentController, event.Warning("Failed to update the cloneset to upgrade", err))
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
// record the scale
|
||||
klog.InfoS("scale one batch", "current batch", s.rolloutStatus.CurrentBatch)
|
||||
s.recorder.Event(s.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("Submitted scale quest for batch %d", s.rolloutStatus.CurrentBatch)))
|
||||
s.rolloutStatus.UpgradedReplicas = int32(newPodTarget)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckOneBatchPods checks to see if the pods are scaled according to the rollout plan
|
||||
func (s *CloneSetScaleController) CheckOneBatchPods(ctx context.Context) (bool, error) {
|
||||
err := s.fetchCloneSet(ctx)
|
||||
if err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
newPodTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), int(s.rolloutStatus.CurrentBatch))
|
||||
// get the number of ready pod from cloneset
|
||||
// TODO: should we use the replica number when we shrink?
|
||||
readyPodCount := int(s.cloneSet.Status.ReadyReplicas)
|
||||
currentBatch := s.rolloutSpec.RolloutBatches[s.rolloutStatus.CurrentBatch]
|
||||
unavail := 0
|
||||
if currentBatch.MaxUnavailable != nil {
|
||||
unavail, _ = intstr.GetValueFromIntOrPercent(currentBatch.MaxUnavailable,
|
||||
util.Abs(int(s.rolloutStatus.RolloutTargetSize-s.rolloutStatus.RolloutOriginalSize)), true)
|
||||
}
|
||||
klog.InfoS("checking the scaling progress", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"new pod count target", newPodTarget, "new ready pod count", readyPodCount,
|
||||
"max unavailable pod allowed", unavail)
|
||||
s.rolloutStatus.UpgradedReadyReplicas = int32(readyPodCount)
|
||||
targetReached := false
|
||||
// nolint
|
||||
if s.rolloutStatus.RolloutOriginalSize <= s.rolloutStatus.RolloutTargetSize && unavail+readyPodCount >= newPodTarget {
|
||||
targetReached = true
|
||||
} else if s.rolloutStatus.RolloutOriginalSize > s.rolloutStatus.RolloutTargetSize && readyPodCount <= newPodTarget {
|
||||
targetReached = true
|
||||
}
|
||||
if targetReached {
|
||||
// record the successful upgrade
|
||||
klog.InfoS("the current batch is ready", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"target", newPodTarget, "readyPodCount", readyPodCount, "max unavailable allowed", unavail)
|
||||
s.recorder.Event(s.parentController, event.Normal("Batch Available",
|
||||
fmt.Sprintf("Batch %d is available", s.rolloutStatus.CurrentBatch)))
|
||||
return true, nil
|
||||
}
|
||||
// continue to verify
|
||||
klog.InfoS("the batch is not ready yet", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"target", newPodTarget, "readyPodCount", readyPodCount, "max unavailable allowed", unavail)
|
||||
s.rolloutStatus.RolloutRetry("the batch is not ready yet")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// FinalizeOneBatch makes sure that the current batch and replica count in the status are validate
|
||||
func (s *CloneSetScaleController) FinalizeOneBatch(ctx context.Context) (bool, error) {
|
||||
status := s.rolloutStatus
|
||||
spec := s.rolloutSpec
|
||||
if spec.BatchPartition != nil && *spec.BatchPartition < status.CurrentBatch {
|
||||
err := fmt.Errorf("the current batch value in the status is greater than the batch partition")
|
||||
klog.ErrorS(err, "we have moved past the user defined partition", "user specified batch partition",
|
||||
*spec.BatchPartition, "current batch we are working on", status.CurrentBatch)
|
||||
return false, err
|
||||
}
|
||||
// special case the equal case
|
||||
if s.rolloutStatus.RolloutOriginalSize == s.rolloutStatus.RolloutTargetSize {
|
||||
return true, nil
|
||||
}
|
||||
// we just make sure the target is right
|
||||
finishedPodCount := int(status.UpgradedReplicas)
|
||||
currentBatch := int(status.CurrentBatch)
|
||||
// calculate the pod target just before the current batch
|
||||
preBatchTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), currentBatch-1)
|
||||
// calculate the pod target with the current batch
|
||||
curBatchTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), currentBatch)
|
||||
// the recorded number should be at least as much as the all the pods before the current batch
|
||||
if finishedPodCount < util.Min(preBatchTarget, curBatchTarget) {
|
||||
err := fmt.Errorf("the upgraded replica in the status is less than the lower bound")
|
||||
klog.ErrorS(err, "rollout status inconsistent", "existing pod target", finishedPodCount,
|
||||
"the lower bound", util.Min(preBatchTarget, curBatchTarget))
|
||||
return false, err
|
||||
}
|
||||
// the recorded number should be not as much as the all the pods including the active batch
|
||||
if finishedPodCount > util.Max(preBatchTarget, curBatchTarget) {
|
||||
err := fmt.Errorf("the upgraded replica in the status is greater than the upper bound")
|
||||
klog.ErrorS(err, "rollout status inconsistent", "existing pod target", finishedPodCount,
|
||||
"the upper bound", util.Max(preBatchTarget, curBatchTarget))
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Finalize makes sure the Cloneset is scaled and ready to use
|
||||
func (s *CloneSetScaleController) Finalize(ctx context.Context, succeed bool) bool {
|
||||
if err := s.fetchCloneSet(ctx); err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false
|
||||
}
|
||||
clonePatch := client.MergeFrom(s.cloneSet.DeepCopy())
|
||||
// remove the parent controller from the resources' owner list
|
||||
var newOwnerList []metav1.OwnerReference
|
||||
isOwner := false
|
||||
for _, owner := range s.cloneSet.GetOwnerReferences() {
|
||||
if owner.Kind == s.parentController.GetObjectKind().GroupVersionKind().Kind &&
|
||||
owner.APIVersion == s.parentController.GetObjectKind().GroupVersionKind().GroupVersion().String() {
|
||||
isOwner = true
|
||||
continue
|
||||
}
|
||||
newOwnerList = append(newOwnerList, owner)
|
||||
}
|
||||
if !isOwner {
|
||||
// nothing to do if we are already not the owner
|
||||
klog.InfoS("the cloneset is already released and not controlled by rollout", "cloneSet", s.cloneSet.Name)
|
||||
return true
|
||||
}
|
||||
|
||||
s.cloneSet.SetOwnerReferences(newOwnerList)
|
||||
// patch the CloneSet
|
||||
if err := s.client.Patch(ctx, s.cloneSet, clonePatch, client.FieldOwner(s.parentController.GetUID())); err != nil {
|
||||
s.recorder.Event(s.parentController, event.Warning("Failed to the finalize the cloneset", err))
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false
|
||||
}
|
||||
// mark the resource finalized
|
||||
s.recorder.Event(s.parentController, event.Normal("Scale Finalized",
|
||||
fmt.Sprintf("Scale resource are finalized, succeed := %t", succeed)))
|
||||
return true
|
||||
}
|
||||
@@ -1,483 +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 workloads
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("cloneset controller", func() {
|
||||
var (
|
||||
s CloneSetScaleController
|
||||
ns corev1.Namespace
|
||||
name string
|
||||
namespace string
|
||||
cloneSet kruise.CloneSet
|
||||
namespacedName client.ObjectKey
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
namespace = "rollout-ns"
|
||||
name = "rollout1"
|
||||
appRollout := v1alpha1.Rollout{TypeMeta: metav1.TypeMeta{APIVersion: v1alpha1.SchemeGroupVersion.String(), Kind: v1alpha1.RolloutKind}, ObjectMeta: metav1.ObjectMeta{Name: name}}
|
||||
namespacedName = client.ObjectKey{Name: name, Namespace: namespace}
|
||||
s = CloneSetScaleController{
|
||||
cloneSetController: cloneSetController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{
|
||||
TargetSize: pointer.Int32(10),
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
},
|
||||
},
|
||||
rolloutStatus: &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState},
|
||||
parentController: &appRollout,
|
||||
recorder: event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout"),
|
||||
},
|
||||
targetNamespacedName: namespacedName,
|
||||
},
|
||||
}
|
||||
|
||||
cloneSet = kruise.CloneSet{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: kruise.GroupVersion.String(), Kind: "CloneSet"},
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
|
||||
Spec: kruise.CloneSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"env": "staging"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"env": "staging"}},
|
||||
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: name, Image: "nginx"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ns = corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
By("Create a namespace")
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
By("clean up")
|
||||
k8sClient.Delete(ctx, &cloneSet)
|
||||
})
|
||||
|
||||
Context("TestNewCloneSetScaleController", func() {
|
||||
It("init a CloneSet Scale Controller", func() {
|
||||
recorder := event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout")
|
||||
parentController := &v1alpha1.Rollout{ObjectMeta: metav1.ObjectMeta{Name: name}}
|
||||
rolloutSpec := &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
rolloutStatus := &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState}
|
||||
workloadNamespacedName := client.ObjectKey{Name: name, Namespace: namespace}
|
||||
got := NewCloneSetScaleController(k8sClient, recorder, parentController, rolloutSpec, rolloutStatus, workloadNamespacedName)
|
||||
controller := &CloneSetScaleController{
|
||||
cloneSetController: cloneSetController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: workloadNamespacedName,
|
||||
}}
|
||||
Expect(got).Should(Equal(controller))
|
||||
})
|
||||
})
|
||||
|
||||
Context("VerifySpec", func() {
|
||||
It("rollout need a target size", func() {
|
||||
s.rolloutSpec.TargetSize = nil
|
||||
ligit, err := s.VerifySpec(ctx)
|
||||
Expect(ligit).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("without a target"))
|
||||
})
|
||||
|
||||
It("could not fetch CloneSet workload", func() {
|
||||
ligit, err := s.VerifySpec(ctx)
|
||||
Expect(ligit).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("rollout batch doesn't fit scale target", func() {
|
||||
By("Create a CloneSet")
|
||||
cloneSet.Spec.Replicas = pointer.Int32(15)
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("Verify should fail as the scale batches don't match")
|
||||
s.rolloutSpec.RolloutBatches[2].Replicas = intstr.FromInt(10)
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("the cloneset is in the middle of scaling", func() {
|
||||
By("Create a CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
By("verify should fail because replica does not match")
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("the cloneset is in the middle of updating", func() {
|
||||
By("Create a CloneSet and set as paused")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
By("Update the CloneSet status")
|
||||
cloneSet.Status.Replicas = 1
|
||||
Expect(k8sClient.Status().Update(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("verify should fail because replica are not upgraded")
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("spec is valid", func() {
|
||||
By("Create a CloneSet and set as paused")
|
||||
cloneSet.Spec.UpdateStrategy.Paused = true
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
By("Update the CloneSet status")
|
||||
cloneSet.Status.Replicas = 1
|
||||
cloneSet.Status.UpdatedReplicas = 1
|
||||
cloneSet.Status.UpdatedReadyReplicas = 1
|
||||
Expect(k8sClient.Status().Update(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("verify should pass and record the size")
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeTrue())
|
||||
Expect(s.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(10))
|
||||
Expect(s.rolloutStatus.RolloutOriginalSize).Should(BeEquivalentTo(1))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestInitialize", func() {
|
||||
BeforeEach(func() {
|
||||
cloneSet.Spec.UpdateStrategy.Paused = true
|
||||
})
|
||||
|
||||
It("could not fetch CloneSet workload", func() {
|
||||
consistent, err := s.Initialize(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("failed to patch the owner of CloneSet", func() {
|
||||
By("Create a CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("initialize will fail because cloneset has wrong owner reference")
|
||||
initialized, err := s.Initialize(ctx)
|
||||
Expect(initialized).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("workload CloneSet is controlled by appRollout already", func() {
|
||||
By("Create a CloneSet")
|
||||
cloneSet.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("initialize succeed without patching")
|
||||
initialized, err := s.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("successfully initialized CloneSet", func() {
|
||||
By("create cloneset")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("initialize succeeds")
|
||||
s.parentController.SetUID("1231586900")
|
||||
initialized, err := s.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestRolloutOneBatchPods", func() {
|
||||
It("could not fetch CloneSet workload", func() {
|
||||
consistent, err := s.RolloutOneBatchPods(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("successfully rollout, current batch number is not equal to the expected one", func() {
|
||||
By("Create a CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("rollout the second batch of current cloneset")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 0
|
||||
s.rolloutStatus.RolloutTargetSize = 10
|
||||
done, err := s.RolloutOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(3))
|
||||
Expect(k8sClient.Get(ctx, s.targetNamespacedName, &cloneSet)).Should(Succeed())
|
||||
Expect(*cloneSet.Spec.Replicas).Should(BeEquivalentTo(3))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestCheckOneBatchPods", func() {
|
||||
It("could not fetch CloneSet workload", func() {
|
||||
consistent, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("current ready Pod is less than expected during increase", func() {
|
||||
By("Create the CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
By("Update the CloneSet status")
|
||||
cloneSet.Status.ReadyReplicas = 3
|
||||
Expect(k8sClient.Status().Update(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("checking should fail as not enough pod ready")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 2
|
||||
s.rolloutStatus.RolloutTargetSize = 10
|
||||
done, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(cloneSet.Status.ReadyReplicas))
|
||||
|
||||
// set the rollout batch spec allow unavailable
|
||||
perc := intstr.FromString("20%")
|
||||
s.rolloutSpec.RolloutBatches[1] = v1alpha1.RolloutBatch{
|
||||
Replicas: perc,
|
||||
MaxUnavailable: &perc,
|
||||
}
|
||||
By("checking one batch should succeed with unavailble allowed")
|
||||
done, err = s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(cloneSet.Status.ReadyReplicas))
|
||||
})
|
||||
|
||||
It("current ready Pod is more than expected during decrease", func() {
|
||||
By("Create the CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
By("Update the CloneSet status")
|
||||
cloneSet.Status.ReadyReplicas = 10
|
||||
Expect(k8sClient.Status().Update(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("checking should fail as not enough pod ready")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 12
|
||||
s.rolloutStatus.RolloutTargetSize = 5
|
||||
done, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(cloneSet.Status.ReadyReplicas))
|
||||
|
||||
// set the rollout batch spec allow unavailable
|
||||
perc := intstr.FromString("20%")
|
||||
s.rolloutSpec.RolloutBatches[1] = v1alpha1.RolloutBatch{
|
||||
Replicas: perc,
|
||||
MaxUnavailable: &perc,
|
||||
}
|
||||
By("checking one batch should still fail even with unavailble allowed")
|
||||
done, err = s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(cloneSet.Status.ReadyReplicas))
|
||||
})
|
||||
|
||||
It("there are more pods shrunk during decrease", func() {
|
||||
By("Create the CloneSet")
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
By("Update the CloneSet status")
|
||||
cloneSet.Status.ReadyReplicas = 8
|
||||
Expect(k8sClient.Status().Update(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("checking should pass even with not enough pod ready")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 12
|
||||
s.rolloutStatus.RolloutTargetSize = 5
|
||||
done, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(cloneSet.Status.ReadyReplicas))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalizeOneBatch", func() {
|
||||
BeforeEach(func() {
|
||||
s.rolloutSpec.RolloutBatches[0] = v1alpha1.RolloutBatch{
|
||||
Replicas: intstr.FromInt(2),
|
||||
}
|
||||
})
|
||||
|
||||
It("test illegal batch partition", func() {
|
||||
By("finalizing one batch")
|
||||
s.rolloutSpec.BatchPartition = pointer.Int32(2)
|
||||
s.rolloutStatus.CurrentBatch = 3
|
||||
done, err := s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("the current batch value in the status is greater than the batch partition"))
|
||||
})
|
||||
|
||||
It("test finalize during increase", func() {
|
||||
By("finalizing one batch with not enough")
|
||||
s.rolloutStatus.UpgradedReplicas = 6
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 5
|
||||
s.rolloutStatus.RolloutTargetSize = 12
|
||||
done, err := s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring(" upgraded replica in the status is less than the lower bound"))
|
||||
|
||||
By("finalizing one batch with just enough")
|
||||
s.rolloutStatus.UpgradedReplicas = 7
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch with all")
|
||||
s.rolloutStatus.UpgradedReplicas = 9
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch with more than")
|
||||
s.rolloutStatus.UpgradedReplicas = 12
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("upgraded replica in the status is greater than the upper bound"))
|
||||
})
|
||||
|
||||
It("test finalize during decrease", func() {
|
||||
By("finalizing one batch with too many")
|
||||
s.rolloutStatus.UpgradedReplicas = 13
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 14
|
||||
s.rolloutStatus.RolloutTargetSize = 2
|
||||
done, err := s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("upgraded replica in the status is greater than the upper bound"))
|
||||
|
||||
By("finalizing one batch with just enough")
|
||||
s.rolloutStatus.UpgradedReplicas = 12
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch with all")
|
||||
s.rolloutStatus.UpgradedReplicas = 9
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch with not enough")
|
||||
s.rolloutStatus.UpgradedReplicas = 8
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring(" upgraded replica in the status is less than the lower bound"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalize", func() {
|
||||
It("failed to fetch CloneSet", func() {
|
||||
By("finalizing")
|
||||
finalized := s.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("Already finalize CloneSet", func() {
|
||||
By("Create a CloneSet")
|
||||
cloneSet.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: "notRollout",
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("finalizing without patch")
|
||||
finalized := s.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("successfully to finalize CloneSet", func() {
|
||||
By("Create a CloneSet")
|
||||
cloneSet.SetOwnerReferences([]metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
},
|
||||
{
|
||||
APIVersion: corev1.SchemeGroupVersion.String(),
|
||||
Kind: "Deployment",
|
||||
Name: "def",
|
||||
UID: "998877745",
|
||||
},
|
||||
})
|
||||
Expect(k8sClient.Create(ctx, &cloneSet)).Should(Succeed())
|
||||
|
||||
By("finalizing with patch")
|
||||
finalized := s.Finalize(ctx, false)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
Expect(k8sClient.Get(ctx, s.targetNamespacedName, &cloneSet)).Should(Succeed())
|
||||
Expect(len(cloneSet.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
Expect(cloneSet.GetOwnerReferences()[0].Kind).Should(Equal("Deployment"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,152 +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 workloads
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
apps "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
)
|
||||
|
||||
// verifyBatchesWithRollout verifies that the the sum of all the batch replicas is valid given the total replica
|
||||
// each batch replica can be absolute or a percentage
|
||||
func verifyBatchesWithRollout(rolloutSpec *v1alpha1.RolloutPlan, totalReplicas int32) error {
|
||||
// If rolloutBatches length equal to zero will cause index out of bounds panic, guarantee don't crash whole vela controller
|
||||
if len(rolloutSpec.RolloutBatches) == 0 {
|
||||
return fmt.Errorf("the rolloutPlan must have batches")
|
||||
}
|
||||
// if not set, the sum of all the batch sizes minus the last batch cannot be more than the totalReplicas
|
||||
totalRollout := 0
|
||||
for i := 0; i < len(rolloutSpec.RolloutBatches)-1; i++ {
|
||||
rb := rolloutSpec.RolloutBatches[i]
|
||||
batchSize, _ := intstr.GetValueFromIntOrPercent(&rb.Replicas, int(totalReplicas), true)
|
||||
totalRollout += batchSize
|
||||
}
|
||||
if totalRollout >= int(totalReplicas) {
|
||||
return fmt.Errorf("the rollout plan batch size mismatch, total batch size = %d, totalReplicas size = %d",
|
||||
totalRollout, totalReplicas)
|
||||
}
|
||||
|
||||
// include the last batch if it has an int value
|
||||
// we ignore the last batch percentage since it is very likely to cause rounding errors
|
||||
lastBatch := rolloutSpec.RolloutBatches[len(rolloutSpec.RolloutBatches)-1]
|
||||
if lastBatch.Replicas.Type == intstr.Int {
|
||||
totalRollout += int(lastBatch.Replicas.IntVal)
|
||||
// now that they should be the same
|
||||
if totalRollout != int(totalReplicas) {
|
||||
return fmt.Errorf("the rollout plan batch size mismatch, total batch size = %d, totalReplicas size = %d",
|
||||
totalRollout, totalReplicas)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyBatchesWithScale verifies that executing batches finally reach the target size starting from original size
|
||||
func verifyBatchesWithScale(rolloutSpec *v1alpha1.RolloutPlan, originalSize, targetSize int) error {
|
||||
// If rolloutBatches length equal to zero will cause index out of bounds panic, guarantee don't crash whole vela controller
|
||||
if len(rolloutSpec.RolloutBatches) == 0 {
|
||||
return fmt.Errorf("the rolloutPlan must have batches")
|
||||
}
|
||||
totalRollout := originalSize
|
||||
for i := 0; i < len(rolloutSpec.RolloutBatches)-1; i++ {
|
||||
rb := rolloutSpec.RolloutBatches[i]
|
||||
if targetSize > originalSize {
|
||||
batchSize, _ := intstr.GetValueFromIntOrPercent(&rb.Replicas, targetSize-originalSize, true)
|
||||
totalRollout += batchSize
|
||||
} else {
|
||||
batchSize, _ := intstr.GetValueFromIntOrPercent(&rb.Replicas, originalSize-targetSize, true)
|
||||
totalRollout -= batchSize
|
||||
}
|
||||
}
|
||||
//nolint ifElseChain
|
||||
if targetSize > originalSize {
|
||||
if totalRollout >= targetSize {
|
||||
return fmt.Errorf("the rollout plan increased too much, total batch size = %d, targetSize size = %d",
|
||||
totalRollout, targetSize)
|
||||
}
|
||||
} else if targetSize < originalSize {
|
||||
if totalRollout <= targetSize {
|
||||
return fmt.Errorf("the rollout plan reduced too much, total batch size = %d, targetSize size = %d",
|
||||
totalRollout, targetSize)
|
||||
}
|
||||
} else if totalRollout != targetSize {
|
||||
return fmt.Errorf("the rollout plan changed on no-op scale, total batch size = %d, targetSize size = %d",
|
||||
totalRollout, targetSize)
|
||||
}
|
||||
// include the last batch if it has an int value
|
||||
// we ignore the last batch percentage since it is very likely to cause rounding errors
|
||||
lastBatch := rolloutSpec.RolloutBatches[len(rolloutSpec.RolloutBatches)-1]
|
||||
if lastBatch.Replicas.Type == intstr.Int {
|
||||
if targetSize > originalSize {
|
||||
totalRollout += int(lastBatch.Replicas.IntVal)
|
||||
} else {
|
||||
totalRollout -= int(lastBatch.Replicas.IntVal)
|
||||
}
|
||||
// now that they should be the same
|
||||
if totalRollout != targetSize {
|
||||
return fmt.Errorf("the rollout plan batch size mismatch, total batch size = %d, targetSize size = %d",
|
||||
totalRollout, targetSize)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateNewBatchTarget(rolloutSpec *v1alpha1.RolloutPlan, originalSize, targetSize, currentBatch int) int {
|
||||
if currentBatch == len(rolloutSpec.RolloutBatches)-1 {
|
||||
// special handle the last batch, we ignore the rest of the batch in case there are rounding errors
|
||||
klog.InfoS("use the target size as the total pod target for the last rolling batch",
|
||||
"current batch", currentBatch, "new pod target", targetSize)
|
||||
return targetSize
|
||||
}
|
||||
|
||||
newPodTarget := originalSize
|
||||
for i := 0; i <= currentBatch && i < len(rolloutSpec.RolloutBatches); i++ {
|
||||
if targetSize > originalSize {
|
||||
batchSize, _ := intstr.GetValueFromIntOrPercent(&rolloutSpec.RolloutBatches[i].Replicas, targetSize-originalSize,
|
||||
true)
|
||||
newPodTarget += batchSize
|
||||
} else {
|
||||
batchSize, _ := intstr.GetValueFromIntOrPercent(&rolloutSpec.RolloutBatches[i].Replicas, originalSize-targetSize,
|
||||
true)
|
||||
newPodTarget -= batchSize
|
||||
}
|
||||
}
|
||||
|
||||
klog.InfoS("calculated the number of new pod size", "current batch", currentBatch,
|
||||
"new pod target", newPodTarget)
|
||||
return newPodTarget
|
||||
}
|
||||
|
||||
func getDeploymentReplicas(deploy *apps.Deployment) int32 {
|
||||
// replicas default is 1
|
||||
if deploy.Spec.Replicas != nil {
|
||||
return *deploy.Spec.Replicas
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func getStatefulSetReplicas(statefulSet *apps.StatefulSet) int32 {
|
||||
// replicas default is 1
|
||||
if statefulSet.Spec.Replicas != nil {
|
||||
return *statefulSet.Spec.Replicas
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@@ -1,399 +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 workloads
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
rolloutPercentSpec = &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("40%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("30%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("10%"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rolloutNumericSpec = &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromInt(4),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromInt(3),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rolloutMixedSpec = &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("50%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rolloutRelaxSpec = &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("50%"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rolloutOverFlowSpec = &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromString("140%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("40%"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestCalculateNewBatchTarget(t *testing.T) {
|
||||
// test common rollout
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 0, 10, 0); got != 1 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 1)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 0, 10, 1); got != 3 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 3)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 0, 10, 2); got != 8 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 8)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 0, 10, 3); got != 10 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 10)
|
||||
}
|
||||
|
||||
// test scale up
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 2, 12, 0); got != 3 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 3)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 3, 13, 1); got != 6 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 6)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 4, 14, 2); got != 12 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 12)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 5, 15, 3); got != 15 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 15)
|
||||
}
|
||||
|
||||
// test scale down
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 10, 0, 0); got != 9 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 9)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 20, 5, 1); got != 16 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 16)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 30, 15, 2); got != 18 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 18)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 40, 10, 3); got != 10 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 10)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateNewBatchTargetCornerCases(t *testing.T) {
|
||||
// test current batch overflow
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 2, 12, 4); got != 12 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 12)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutMixedSpec, 13, 3, 5); got != 3 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 3)
|
||||
}
|
||||
// numeric value doesn't match the range
|
||||
if got := calculateNewBatchTarget(rolloutNumericSpec, 16, 10, 0); got != 15 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 15)
|
||||
}
|
||||
if got := calculateNewBatchTarget(rolloutPercentSpec, 10, 10, 2); got != 10 {
|
||||
t.Errorf("calculateNewBatchTarget() = %v, want %v", got, 10)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyBatchesWithRolloutNormal(t *testing.T) {
|
||||
if err := verifyBatchesWithRollout(rolloutMixedSpec, 10); err != nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want nil", err)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutMixedSpec, 12); err != nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want nil", err)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutMixedSpec, 13); err != nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want nil", err)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutMixedSpec, 20); err == nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want error", nil)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutMixedSpec, 6); err == nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want error", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyBatchesWithRolloutRelaxed(t *testing.T) {
|
||||
// last batch as a percentage always succeeds
|
||||
if err := verifyBatchesWithRollout(rolloutRelaxSpec, 10); err != nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want nil", err)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutRelaxSpec, 100); err != nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want nil", err)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutRelaxSpec, 31); err != nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want nil", err)
|
||||
}
|
||||
// last can't be zero
|
||||
if err := verifyBatchesWithRollout(rolloutRelaxSpec, 6); err == nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want error", nil)
|
||||
}
|
||||
// overflow always fail
|
||||
if err := verifyBatchesWithRollout(rolloutOverFlowSpec, 10); err == nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want error", nil)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutOverFlowSpec, 100); err == nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want error", nil)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutOverFlowSpec, 1); err == nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want error", nil)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutOverFlowSpec, 0); err == nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want error", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmptyRolloutBatches(t *testing.T) {
|
||||
plan := &v1alpha1.RolloutPlan{
|
||||
TargetSize: pointer.Int32(2),
|
||||
}
|
||||
if err := verifyBatchesWithRollout(plan, 3); err == nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want error", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyBatchesWithRolloutNumeric(t *testing.T) {
|
||||
// test hard number
|
||||
if err := verifyBatchesWithRollout(rolloutNumericSpec, 6); err == nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want error", nil)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutNumericSpec, 11); err == nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want error", nil)
|
||||
}
|
||||
if err := verifyBatchesWithRollout(rolloutNumericSpec, 10); err != nil {
|
||||
t.Errorf("verifyBatchesWithRollout() = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_VerifyBatchesWithScalePassCases(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
rolloutSpec *v1alpha1.RolloutPlan
|
||||
originalSize int
|
||||
targetSize int
|
||||
}{
|
||||
"percent equal case 1": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
originalSize: 12,
|
||||
targetSize: 12,
|
||||
},
|
||||
"percent equal case 2": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
originalSize: 0,
|
||||
targetSize: 0,
|
||||
},
|
||||
"percent increase case": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
originalSize: 12,
|
||||
targetSize: 22,
|
||||
},
|
||||
"percent decrease case 1": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
originalSize: 27,
|
||||
targetSize: 7,
|
||||
},
|
||||
"percent decrease case 2": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
originalSize: 27,
|
||||
targetSize: 0,
|
||||
},
|
||||
"relax increase 1": {
|
||||
rolloutSpec: rolloutRelaxSpec,
|
||||
originalSize: 12,
|
||||
targetSize: 32,
|
||||
},
|
||||
"relax increase 2": {
|
||||
rolloutSpec: rolloutRelaxSpec,
|
||||
originalSize: 12,
|
||||
targetSize: 22,
|
||||
},
|
||||
"mix increase 1": {
|
||||
rolloutSpec: rolloutMixedSpec,
|
||||
originalSize: 13,
|
||||
targetSize: 26,
|
||||
},
|
||||
"mix increase 2": {
|
||||
rolloutSpec: rolloutMixedSpec,
|
||||
originalSize: 30,
|
||||
targetSize: 42,
|
||||
},
|
||||
"mix decrease 1": {
|
||||
rolloutSpec: rolloutMixedSpec,
|
||||
originalSize: 32,
|
||||
targetSize: 20,
|
||||
},
|
||||
"mix decrease 2": {
|
||||
rolloutSpec: rolloutMixedSpec,
|
||||
originalSize: 12,
|
||||
targetSize: 0,
|
||||
},
|
||||
"numeric increase": {
|
||||
rolloutSpec: rolloutNumericSpec,
|
||||
originalSize: 16,
|
||||
targetSize: 26,
|
||||
},
|
||||
"numeric decrease": {
|
||||
rolloutSpec: rolloutNumericSpec,
|
||||
originalSize: 13,
|
||||
targetSize: 3,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := verifyBatchesWithScale(tt.rolloutSpec, tt.originalSize, tt.targetSize); err != nil {
|
||||
t.Errorf("verifyBatchesWithScale() error = %v, want pass", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_VerifyBatchesWithScaleFailCases(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
rolloutSpec *v1alpha1.RolloutPlan
|
||||
originalSize int
|
||||
targetSize int
|
||||
}{
|
||||
"total percent more than 100 increase": {
|
||||
rolloutSpec: rolloutOverFlowSpec,
|
||||
originalSize: 12,
|
||||
targetSize: 115,
|
||||
},
|
||||
"total percent more than 100 decrease": {
|
||||
rolloutSpec: rolloutOverFlowSpec,
|
||||
originalSize: 312,
|
||||
targetSize: 15,
|
||||
},
|
||||
"total percent more than 100 equal": {
|
||||
rolloutSpec: rolloutOverFlowSpec,
|
||||
originalSize: 12,
|
||||
targetSize: 12,
|
||||
},
|
||||
"percent increase case less than batch number": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
originalSize: 12,
|
||||
targetSize: 15,
|
||||
},
|
||||
"percent decrease case": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
originalSize: 10,
|
||||
targetSize: 7,
|
||||
},
|
||||
"relax increase too little": {
|
||||
rolloutSpec: rolloutRelaxSpec,
|
||||
originalSize: 12,
|
||||
targetSize: 17,
|
||||
},
|
||||
"mix increase": {
|
||||
rolloutSpec: rolloutMixedSpec,
|
||||
originalSize: 13,
|
||||
targetSize: 20,
|
||||
},
|
||||
"mix decrease": {
|
||||
rolloutSpec: rolloutMixedSpec,
|
||||
originalSize: 42,
|
||||
targetSize: 33,
|
||||
},
|
||||
"numeric increase": {
|
||||
rolloutSpec: rolloutNumericSpec,
|
||||
originalSize: 16,
|
||||
targetSize: 32,
|
||||
},
|
||||
"numeric decrease 1": {
|
||||
rolloutSpec: rolloutNumericSpec,
|
||||
originalSize: 13,
|
||||
targetSize: 10,
|
||||
},
|
||||
"numeric decrease 2": {
|
||||
rolloutSpec: rolloutNumericSpec,
|
||||
originalSize: 16,
|
||||
targetSize: 10,
|
||||
},
|
||||
"empty rollingBatches": {
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{},
|
||||
targetSize: 3,
|
||||
originalSize: 2,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if err := verifyBatchesWithScale(tt.rolloutSpec, tt.originalSize, tt.targetSize); err == nil {
|
||||
t.Errorf("verifyBatchesWithScale passed, want fail")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,106 +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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
// WorkloadController is the interface that all type of cloneSet controller implements
|
||||
type WorkloadController interface {
|
||||
// VerifySpec makes sure that the resources can be upgraded according to the rollout plan
|
||||
// it returns if the verification succeeded/failed or should retry
|
||||
VerifySpec(ctx context.Context) (bool, error)
|
||||
|
||||
// Initialize make sure that the resource is ready to be upgraded
|
||||
// this function is tasked to do any initialization work on the resources
|
||||
// it returns if the initialization succeeded/failed or should retry
|
||||
Initialize(ctx context.Context) (bool, error)
|
||||
|
||||
// RolloutOneBatchPods tries to upgrade pods in the resources following the rollout plan
|
||||
// it will upgrade pods as the rollout plan allows at once
|
||||
// it returns if the upgrade actionable items succeeded/failed or should continue
|
||||
RolloutOneBatchPods(ctx context.Context) (bool, error)
|
||||
|
||||
// CheckOneBatchPods checks how many pods are ready to serve requests in the current batch
|
||||
// it returns whether the number of pods upgraded in this round satisfies the rollout plan
|
||||
CheckOneBatchPods(ctx context.Context) (bool, error)
|
||||
|
||||
// FinalizeOneBatch makes sure that the rollout can start the next batch
|
||||
// it returns if the finalization of this batch succeeded/failed or should retry
|
||||
FinalizeOneBatch(ctx context.Context) (bool, error)
|
||||
|
||||
// Finalize makes sure the resources are in a good final state.
|
||||
// It might depend on if the rollout succeeded or not.
|
||||
// For example, we may remove the source object to prevent scalar traits to ever work
|
||||
// and the finalize rollout web hooks will be called after this call succeeds
|
||||
Finalize(ctx context.Context, succeed bool) bool
|
||||
}
|
||||
|
||||
type workloadController struct {
|
||||
client client.Client
|
||||
recorder event.Recorder
|
||||
parentController oam.Object
|
||||
|
||||
rolloutSpec *v1alpha1.RolloutPlan
|
||||
rolloutStatus *v1alpha1.RolloutStatus
|
||||
}
|
||||
|
||||
// cloneSetController is the place to hold fields needed for handle Cloneset type of workloads
|
||||
type cloneSetController struct {
|
||||
workloadController
|
||||
targetNamespacedName types.NamespacedName
|
||||
cloneSet *kruise.CloneSet
|
||||
}
|
||||
|
||||
// size fetches the Cloneset and returns the replicas (not the actual number of pods)
|
||||
func (c *cloneSetController) size(ctx context.Context) (int32, error) {
|
||||
if c.cloneSet == nil {
|
||||
err := c.fetchCloneSet(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
// default is 1
|
||||
if c.cloneSet.Spec.Replicas == nil {
|
||||
return 1, nil
|
||||
}
|
||||
return *c.cloneSet.Spec.Replicas, nil
|
||||
}
|
||||
|
||||
func (c *cloneSetController) fetchCloneSet(ctx context.Context) error {
|
||||
// get the cloneSet
|
||||
workload := kruise.CloneSet{}
|
||||
err := c.client.Get(ctx, c.targetNamespacedName, &workload)
|
||||
if err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
c.recorder.Event(c.parentController, event.Warning("Failed to get the Cloneset", err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
c.cloneSet = &workload
|
||||
return nil
|
||||
}
|
||||
@@ -1,115 +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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// deploymentController is the place to hold fields needed for handle Deployment type of workloads
|
||||
type deploymentController struct {
|
||||
workloadController
|
||||
targetNamespacedName types.NamespacedName
|
||||
}
|
||||
|
||||
// add the parent controller to the owner of the deployment, unpause it and initialize the size
|
||||
// before kicking start the update and start from every pod in the old version
|
||||
func (c *deploymentController) claimDeployment(ctx context.Context, deploy *apps.Deployment, initSize *int32) (bool, error) {
|
||||
if controller := metav1.GetControllerOf(deploy); controller != nil && controller.APIVersion == v1alpha1.SchemeGroupVersion.String() &&
|
||||
controller.Kind == v1alpha1.RolloutKind {
|
||||
// it's already there
|
||||
return true, nil
|
||||
}
|
||||
|
||||
deployPatch := client.MergeFrom(deploy.DeepCopy())
|
||||
|
||||
// add the parent controller to the owner of the deployment
|
||||
ref := metav1.NewControllerRef(c.parentController, c.parentController.GetObjectKind().GroupVersionKind())
|
||||
deploy.SetOwnerReferences(append(deploy.GetOwnerReferences(), *ref))
|
||||
|
||||
deploy.Spec.Paused = false
|
||||
if initSize != nil {
|
||||
deploy.Spec.Replicas = initSize
|
||||
}
|
||||
|
||||
// patch the Deployment
|
||||
if err := c.client.Patch(ctx, deploy, deployPatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
|
||||
c.recorder.Event(c.parentController, event.Warning("Failed to the start the Deployment update", err))
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// scale the deployment
|
||||
func (c *deploymentController) scaleDeployment(ctx context.Context, deploy *apps.Deployment, size int32) error {
|
||||
deployPatch := client.MergeFrom(deploy.DeepCopy())
|
||||
deploy.Spec.Replicas = pointer.Int32(size)
|
||||
|
||||
// patch the Deployment
|
||||
if err := c.client.Patch(ctx, deploy, deployPatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
|
||||
c.recorder.Event(c.parentController, event.Warning(event.Reason(fmt.Sprintf(
|
||||
"Failed to update the deployment %s to the correct target %d", deploy.GetName(), size)), err))
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
klog.InfoS("Submitted upgrade quest for deployment", "deployment",
|
||||
deploy.GetName(), "target replica size", size, "batch", c.rolloutStatus.CurrentBatch)
|
||||
return nil
|
||||
}
|
||||
|
||||
// remove the parent controller from the deployment's owner list
|
||||
func (c *deploymentController) releaseDeployment(ctx context.Context, deploy *apps.Deployment) (bool, error) {
|
||||
deployPatch := client.MergeFrom(deploy.DeepCopy())
|
||||
|
||||
var newOwnerList []metav1.OwnerReference
|
||||
found := false
|
||||
for _, owner := range deploy.GetOwnerReferences() {
|
||||
if owner.Kind == c.parentController.GetObjectKind().GroupVersionKind().Kind &&
|
||||
owner.APIVersion == c.parentController.GetObjectKind().GroupVersionKind().GroupVersion().String() &&
|
||||
owner.Controller != nil && *owner.Controller {
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
newOwnerList = append(newOwnerList, owner)
|
||||
}
|
||||
if !found {
|
||||
klog.InfoS("the deployment is already released", "deploy", deploy.Name)
|
||||
return true, nil
|
||||
}
|
||||
deploy.SetOwnerReferences(newOwnerList)
|
||||
|
||||
// patch the Deployment
|
||||
if err := c.client.Patch(ctx, deploy, deployPatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
|
||||
c.recorder.Event(c.parentController, event.Warning("Failed to the release the Deployment", err))
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -1,458 +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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
// DeploymentRolloutController is responsible for handling rollout deployment type of workloads
|
||||
type DeploymentRolloutController struct {
|
||||
deploymentController
|
||||
sourceNamespacedName types.NamespacedName
|
||||
sourceDeploy apps.Deployment
|
||||
targetDeploy apps.Deployment
|
||||
}
|
||||
|
||||
// NewDeploymentRolloutController creates a new deployment rollout controller
|
||||
func NewDeploymentRolloutController(client client.Client, recorder event.Recorder, parentController oam.Object,
|
||||
rolloutSpec *v1alpha1.RolloutPlan, rolloutStatus *v1alpha1.RolloutStatus, sourceNamespacedName,
|
||||
targetNamespacedName types.NamespacedName) *DeploymentRolloutController {
|
||||
return &DeploymentRolloutController{
|
||||
deploymentController: deploymentController{
|
||||
workloadController: workloadController{
|
||||
client: client,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: targetNamespacedName,
|
||||
},
|
||||
sourceNamespacedName: sourceNamespacedName,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifySpec verifies that the rollout resource is consistent with the rollout spec
|
||||
func (c *DeploymentRolloutController) VerifySpec(ctx context.Context) (bool, error) {
|
||||
var verifyErr error
|
||||
|
||||
defer func() {
|
||||
if verifyErr != nil {
|
||||
klog.Error(verifyErr)
|
||||
c.recorder.Event(c.parentController, event.Warning("VerifyFailed", verifyErr))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := c.fetchDeployments(ctx); err != nil {
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
// do not fail the rollout just because we can't get the resource
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check if the rollout spec is compatible with the current state
|
||||
targetTotalReplicas, verifyErr := c.calculateRolloutTotalSize()
|
||||
if verifyErr != nil {
|
||||
return false, verifyErr
|
||||
}
|
||||
// record the size and we will use this value to drive the rest of the batches
|
||||
// we do not handle scale case in this controller
|
||||
c.rolloutStatus.RolloutTargetSize = targetTotalReplicas
|
||||
|
||||
// make sure that the updateRevision is different from what we have already done
|
||||
targetHash, verifyErr := utils.ComputeSpecHash(c.targetDeploy.Spec)
|
||||
if verifyErr != nil {
|
||||
// do not fail the rollout because we can't compute the hash value for some reason
|
||||
c.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
if targetHash == c.rolloutStatus.LastAppliedPodTemplateIdentifier {
|
||||
return false, fmt.Errorf("there is no difference between the source and target, hash = %s", targetHash)
|
||||
}
|
||||
|
||||
// check if the rollout batch replicas added up to the Deployment replicas
|
||||
// we don't handle scale case in this controller
|
||||
if verifyErr = c.verifyRolloutBatchReplicaValue(targetTotalReplicas); verifyErr != nil {
|
||||
return false, verifyErr
|
||||
}
|
||||
if !c.sourceDeploy.Spec.Paused && getDeploymentReplicas(&c.sourceDeploy) != c.sourceDeploy.Status.Replicas {
|
||||
return false, fmt.Errorf("the source deployment %s is still being reconciled, need to be paused or stable",
|
||||
c.sourceDeploy.GetName())
|
||||
}
|
||||
|
||||
if !c.targetDeploy.Spec.Paused && getDeploymentReplicas(&c.targetDeploy) != c.targetDeploy.Status.Replicas {
|
||||
return false, fmt.Errorf("the target deployment %s is still being reconciled, need to be paused or stable",
|
||||
c.targetDeploy.GetName())
|
||||
}
|
||||
|
||||
// check if the targetDeploy has any controller
|
||||
if controller := metav1.GetControllerOf(&c.targetDeploy); controller != nil {
|
||||
return false, fmt.Errorf("the target deployment %s has a controller owner %s",
|
||||
c.targetDeploy.GetName(), controller.String())
|
||||
}
|
||||
|
||||
// check if the sourceDeploy has any controller
|
||||
if controller := metav1.GetControllerOf(&c.sourceDeploy); controller != nil {
|
||||
return false, fmt.Errorf("the source deployment %s has a controller owner %s",
|
||||
c.sourceDeploy.GetName(), controller.String())
|
||||
}
|
||||
|
||||
// mark the rollout verified
|
||||
c.recorder.Event(c.parentController, event.Normal("Rollout Verified",
|
||||
"Rollout spec and the Deployment resource are verified"))
|
||||
// record the new pod template hash on success
|
||||
c.rolloutStatus.NewPodTemplateIdentifier = targetHash
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Initialize makes sure that the source and target deployment is under our control
|
||||
func (c *DeploymentRolloutController) Initialize(ctx context.Context) (bool, error) {
|
||||
if err := c.fetchDeployments(ctx); err != nil {
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// claim source deployment
|
||||
if _, err := c.claimDeployment(ctx, &c.sourceDeploy, nil); err != nil {
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// claim target deployment
|
||||
// make sure we start with the matching replicas and target
|
||||
targetInitSize := pointer.Int32(c.rolloutStatus.RolloutTargetSize - getDeploymentReplicas(&c.sourceDeploy))
|
||||
if _, err := c.claimDeployment(ctx, &c.targetDeploy, targetInitSize); err != nil {
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// mark the rollout initialized
|
||||
c.recorder.Event(c.parentController, event.Normal("Rollout Initialized", "Rollout resource are initialized"))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RolloutOneBatchPods calculates the number of pods we can upgrade once according to the rollout spec
|
||||
// and then set the partition accordingly
|
||||
func (c *DeploymentRolloutController) RolloutOneBatchPods(ctx context.Context) (bool, error) {
|
||||
if err := c.fetchDeployments(ctx); err != nil {
|
||||
// don't fail the rollout just because of we can't get the resource
|
||||
// nolint:nilerr
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
currentSizeSetting := *c.sourceDeploy.Spec.Replicas + *c.targetDeploy.Spec.Replicas
|
||||
// get the rollout strategy
|
||||
rolloutStrategy := v1alpha1.IncreaseFirstRolloutStrategyType
|
||||
if len(c.rolloutSpec.RolloutStrategy) != 0 {
|
||||
rolloutStrategy = c.rolloutSpec.RolloutStrategy
|
||||
}
|
||||
|
||||
// Determine if we are the first or the second part of the current batch rollout
|
||||
if currentSizeSetting == c.rolloutStatus.RolloutTargetSize {
|
||||
// we need to finish the first part of the rollout,
|
||||
// this may conclude that we've already reached the size (in a rollback case)
|
||||
return c.rolloutBatchFirstHalf(ctx, rolloutStrategy)
|
||||
}
|
||||
|
||||
// we are at the second half
|
||||
targetSize := c.calculateCurrentTarget(c.rolloutStatus.RolloutTargetSize)
|
||||
if !c.rolloutBatchSecondHalf(ctx, rolloutStrategy, targetSize) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// record the finished upgrade action
|
||||
klog.InfoS("upgraded one batch", "current batch", c.rolloutStatus.CurrentBatch,
|
||||
"target deployment size", targetSize)
|
||||
c.recorder.Event(c.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("Finished submiting all upgrade quests for batch %d", c.rolloutStatus.CurrentBatch)))
|
||||
c.rolloutStatus.UpgradedReplicas = targetSize
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckOneBatchPods checks to see if the pods are all available according to the rollout plan
|
||||
func (c *DeploymentRolloutController) CheckOneBatchPods(ctx context.Context) (bool, error) {
|
||||
if err := c.fetchDeployments(ctx); err != nil {
|
||||
// don't fail the rollout just because of we can't get the resource
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// get the number of ready pod from target
|
||||
readyTargetPodCount := c.targetDeploy.Status.ReadyReplicas
|
||||
sourcePodCount := c.sourceDeploy.Status.Replicas
|
||||
currentBatch := c.rolloutSpec.RolloutBatches[c.rolloutStatus.CurrentBatch]
|
||||
targetGoal := c.calculateCurrentTarget(c.rolloutStatus.RolloutTargetSize)
|
||||
sourceGoal := c.calculateCurrentSource(c.rolloutStatus.RolloutTargetSize)
|
||||
// get the rollout strategy
|
||||
rolloutStrategy := v1alpha1.IncreaseFirstRolloutStrategyType
|
||||
if len(c.rolloutSpec.RolloutStrategy) != 0 {
|
||||
rolloutStrategy = c.rolloutSpec.RolloutStrategy
|
||||
}
|
||||
maxUnavail := 0
|
||||
if currentBatch.MaxUnavailable != nil {
|
||||
maxUnavail, _ = intstr.GetValueFromIntOrPercent(currentBatch.MaxUnavailable, int(c.rolloutStatus.RolloutTargetSize), true)
|
||||
}
|
||||
klog.InfoS("checking the rolling out progress", "current batch", c.rolloutStatus.CurrentBatch,
|
||||
"target pod ready count", readyTargetPodCount, "source pod count", sourcePodCount,
|
||||
"max unavailable pod allowed", maxUnavail, "target goal", targetGoal, "source goal", sourceGoal,
|
||||
"rolloutStrategy", rolloutStrategy)
|
||||
|
||||
if (rolloutStrategy == v1alpha1.IncreaseFirstRolloutStrategyType && sourcePodCount > sourceGoal) ||
|
||||
(rolloutStrategy == v1alpha1.DecreaseFirstRolloutStrategyType &&
|
||||
int32(maxUnavail)+readyTargetPodCount < targetGoal) {
|
||||
// we haven't met the end goal of this batch, continue to verify
|
||||
klog.InfoS("the batch is not ready yet", "current batch", c.rolloutStatus.CurrentBatch)
|
||||
c.rolloutStatus.RolloutRetry(fmt.Sprintf(
|
||||
"the batch %d is not ready yet with %d target pods ready and %d source pods with %d unavailable allowed",
|
||||
c.rolloutStatus.CurrentBatch, readyTargetPodCount, sourcePodCount, maxUnavail))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// record the successful upgrade
|
||||
c.rolloutStatus.UpgradedReadyReplicas = readyTargetPodCount
|
||||
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)))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FinalizeOneBatch makes sure that the rollout status are updated correctly
|
||||
func (c *DeploymentRolloutController) FinalizeOneBatch(ctx context.Context) (bool, error) {
|
||||
if err := c.fetchDeployments(ctx); err != nil {
|
||||
// don't fail the rollout just because of we can't get the resource
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sourceTarget := getDeploymentReplicas(&c.sourceDeploy)
|
||||
targetTarget := getDeploymentReplicas(&c.targetDeploy)
|
||||
if sourceTarget+targetTarget != c.rolloutStatus.RolloutTargetSize {
|
||||
err := fmt.Errorf("deployment targets don't match total rollout, sourceTarget = %d, targetTarget = %d, "+
|
||||
"rolloutTargetSize = %d", sourceTarget, targetTarget, c.rolloutStatus.RolloutTargetSize)
|
||||
klog.ErrorS(err, "the batch is not valid", "current batch", c.rolloutStatus.CurrentBatch)
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Finalize makes sure the Deployment is all upgraded
|
||||
func (c *DeploymentRolloutController) Finalize(ctx context.Context, succeed bool) bool {
|
||||
if err := c.fetchDeployments(ctx); err != nil {
|
||||
// don't fail the rollout just because of we can't get the resource
|
||||
return false
|
||||
}
|
||||
|
||||
// release source deployment
|
||||
if _, err := c.releaseDeployment(ctx, &c.sourceDeploy); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// release target deployment
|
||||
if _, err := c.releaseDeployment(ctx, &c.targetDeploy); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// mark the resource finalized
|
||||
c.rolloutStatus.LastAppliedPodTemplateIdentifier = c.rolloutStatus.NewPodTemplateIdentifier
|
||||
c.recorder.Event(c.parentController, event.Normal("Rollout Finalized",
|
||||
fmt.Sprintf("Rollout resource are finalized, succeed := %t", succeed)))
|
||||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
----------------------------------
|
||||
|
||||
The functions below are helper functions
|
||||
-------------------------------------
|
||||
*/
|
||||
func (c *DeploymentRolloutController) fetchDeployments(ctx context.Context) error {
|
||||
if err := c.client.Get(ctx, c.sourceNamespacedName, &c.sourceDeploy); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
c.recorder.Event(c.parentController, event.Warning("Failed to get the source Deployment", err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := c.client.Get(ctx, c.targetNamespacedName, &c.targetDeploy); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
c.recorder.Event(c.parentController, event.Warning("Failed to get the target Deployment", err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateRolloutTotalSize fetches the Deployment and returns the replicas (not the actual number of pods)
|
||||
func (c *DeploymentRolloutController) calculateRolloutTotalSize() (int32, error) {
|
||||
sourceSize := getDeploymentReplicas(&c.sourceDeploy)
|
||||
// the spec target size is the truth if it's set
|
||||
if c.rolloutSpec.TargetSize != nil {
|
||||
targetSize := *c.rolloutSpec.TargetSize
|
||||
if targetSize < sourceSize {
|
||||
return -1, fmt.Errorf("target size `%d` less than source size `%d`", targetSize, sourceSize)
|
||||
}
|
||||
return targetSize, nil
|
||||
}
|
||||
// otherwise, we assume that the source is the total
|
||||
return sourceSize, nil
|
||||
}
|
||||
|
||||
// check if the replicas in all the rollout batches add up to the right number
|
||||
func (c *DeploymentRolloutController) verifyRolloutBatchReplicaValue(totalReplicas int32) error {
|
||||
// use a common function to check if the sum of all the batches can match the Deployment size
|
||||
return verifyBatchesWithRollout(c.rolloutSpec, totalReplicas)
|
||||
}
|
||||
|
||||
// the target deploy size for the current batch
|
||||
func (c *DeploymentRolloutController) calculateCurrentTarget(totalSize int32) int32 {
|
||||
targetSize := int32(calculateNewBatchTarget(c.rolloutSpec, 0, int(totalSize), int(c.rolloutStatus.CurrentBatch)))
|
||||
klog.InfoS("Calculated the number of pods in the target deployment after current batch",
|
||||
"current batch", c.rolloutStatus.CurrentBatch, "target deploy size", targetSize)
|
||||
return targetSize
|
||||
}
|
||||
|
||||
// the source deploy size for the current batch
|
||||
func (c *DeploymentRolloutController) calculateCurrentSource(totalSize int32) int32 {
|
||||
sourceSize := totalSize - c.calculateCurrentTarget(totalSize)
|
||||
klog.InfoS("Calculated the number of pods in the source deployment after current batch",
|
||||
"current batch", c.rolloutStatus.CurrentBatch, "source deploy size", sourceSize)
|
||||
return sourceSize
|
||||
}
|
||||
|
||||
func (c *DeploymentRolloutController) rolloutBatchFirstHalf(ctx context.Context,
|
||||
rolloutStrategy v1alpha1.RolloutStrategyType) (finished bool, rolloutError error) {
|
||||
targetSize := c.calculateCurrentTarget(c.rolloutStatus.RolloutTargetSize)
|
||||
defer func() {
|
||||
if finished {
|
||||
// record the finished upgrade action
|
||||
klog.InfoS("one batch is done already, no need to upgrade", "current batch", c.rolloutStatus.CurrentBatch)
|
||||
c.recorder.Event(c.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("upgrade quests for batch %d is already reached, no need to upgrade",
|
||||
c.rolloutStatus.CurrentBatch)))
|
||||
c.rolloutStatus.UpgradedReplicas = targetSize
|
||||
}
|
||||
}()
|
||||
|
||||
if rolloutStrategy == v1alpha1.IncreaseFirstRolloutStrategyType {
|
||||
// set the target replica first which should increase its size
|
||||
if getDeploymentReplicas(&c.targetDeploy) < targetSize {
|
||||
klog.InfoS("set target deployment replicas", "deploy", c.targetDeploy.Name, "targetSize", targetSize)
|
||||
_ = c.scaleDeployment(ctx, &c.targetDeploy, targetSize)
|
||||
c.recorder.Event(c.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("Submitted the increase part of upgrade quests for batch %d, target size = %d",
|
||||
c.rolloutStatus.CurrentBatch, targetSize)))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// do nothing if the target is already reached
|
||||
klog.InfoS("target deployment replicas overshoot the size already", "deploy", c.targetDeploy.Name,
|
||||
"deployment size", getDeploymentReplicas(&c.targetDeploy), "targetSize", targetSize)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if rolloutStrategy == v1alpha1.DecreaseFirstRolloutStrategyType {
|
||||
// set the source replicas first which should shrink its size
|
||||
sourceSize := c.calculateCurrentSource(c.rolloutStatus.RolloutTargetSize)
|
||||
if getDeploymentReplicas(&c.sourceDeploy) > sourceSize {
|
||||
klog.InfoS("set source deployment replicas", "source deploy", c.sourceDeploy.Name, "sourceSize", sourceSize)
|
||||
_ = c.scaleDeployment(ctx, &c.sourceDeploy, sourceSize)
|
||||
c.recorder.Event(c.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("Submitted the decrease part of upgrade quests for batch %d, source size = %d",
|
||||
c.rolloutStatus.CurrentBatch, sourceSize)))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// do nothing if the reduce target is already reached
|
||||
klog.InfoS("source deployment replicas overshoot the size already", "source deploy", c.sourceDeploy.Name,
|
||||
"deployment size", getDeploymentReplicas(&c.sourceDeploy), "sourceSize", sourceSize)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("encountered an unknown rolloutStrategy `%s`", rolloutStrategy)
|
||||
}
|
||||
|
||||
func (c *DeploymentRolloutController) rolloutBatchSecondHalf(ctx context.Context,
|
||||
rolloutStrategy v1alpha1.RolloutStrategyType, targetSize int32) bool {
|
||||
sourceSize := c.calculateCurrentSource(c.rolloutStatus.RolloutTargetSize)
|
||||
|
||||
if rolloutStrategy == v1alpha1.IncreaseFirstRolloutStrategyType {
|
||||
// calculate the max unavailable given the target size
|
||||
maxUnavail := 0
|
||||
currentBatch := c.rolloutSpec.RolloutBatches[c.rolloutStatus.CurrentBatch]
|
||||
if currentBatch.MaxUnavailable != nil {
|
||||
maxUnavail, _ = intstr.GetValueFromIntOrPercent(currentBatch.MaxUnavailable, int(c.rolloutStatus.RolloutTargetSize), true)
|
||||
}
|
||||
// make sure that the target deployment has enough ready pods before reducing the source
|
||||
if c.targetDeploy.Status.ReadyReplicas+int32(maxUnavail) >= targetSize {
|
||||
// set the source replicas now which should shrink its size
|
||||
klog.InfoS("set source deployment replicas", "deploy", c.sourceDeploy.Name, "sourceSize", sourceSize)
|
||||
if err := c.scaleDeployment(ctx, &c.sourceDeploy, sourceSize); err != nil {
|
||||
return false
|
||||
}
|
||||
c.recorder.Event(c.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("Submitted the decrease part of upgrade quests for batch %d, source size = %d",
|
||||
c.rolloutStatus.CurrentBatch, sourceSize)))
|
||||
} else {
|
||||
// continue to verify
|
||||
klog.InfoS("the batch is not ready yet", "current batch", c.rolloutStatus.CurrentBatch,
|
||||
"target ready pod", c.targetDeploy.Status.ReadyReplicas)
|
||||
c.rolloutStatus.RolloutRetry(fmt.Sprintf("the batch %d is not ready yet with %d target pods ready",
|
||||
c.rolloutStatus.CurrentBatch, c.targetDeploy.Status.ReadyReplicas))
|
||||
return false
|
||||
}
|
||||
} else if rolloutStrategy == v1alpha1.DecreaseFirstRolloutStrategyType {
|
||||
// make sure that the source deployment has the correct pods before moving the target
|
||||
if c.sourceDeploy.Status.Replicas == sourceSize {
|
||||
// we can increase the target deployment as soon as the source deployment's replica is correct
|
||||
// no need to wait for them to be ready
|
||||
klog.InfoS("set target deployment replicas", "deploy", c.targetDeploy.Name, "targetSize", targetSize)
|
||||
if err := c.scaleDeployment(ctx, &c.targetDeploy, targetSize); err != nil {
|
||||
return false
|
||||
}
|
||||
c.recorder.Event(c.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("Submitted the increase part of upgrade quests for batch %d, target size = %d",
|
||||
c.rolloutStatus.CurrentBatch, targetSize)))
|
||||
} else {
|
||||
// continue to verify
|
||||
klog.InfoS("the batch is not ready yet", "current batch", c.rolloutStatus.CurrentBatch,
|
||||
"source deploy pod", c.sourceDeploy.Status.Replicas)
|
||||
c.rolloutStatus.RolloutRetry(fmt.Sprintf("the batch %d is not ready yet with %d source pods",
|
||||
c.rolloutStatus.CurrentBatch, c.sourceDeploy.Status.Replicas))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -1,818 +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 workloads
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("deployment controller", func() {
|
||||
var (
|
||||
c DeploymentRolloutController
|
||||
ns corev1.Namespace
|
||||
namespaceName string
|
||||
sourceName string
|
||||
targetName string
|
||||
sourceDeploy appsv1.Deployment
|
||||
targetDeploy appsv1.Deployment
|
||||
sourceNamespacedName client.ObjectKey
|
||||
targetNamespacedName client.ObjectKey
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
By("setup before each test")
|
||||
namespaceName = "rollout-ns"
|
||||
sourceName = "source-dep"
|
||||
targetName = "target-dep"
|
||||
appRollout := v1alpha1.Rollout{TypeMeta: metav1.TypeMeta{APIVersion: v1alpha1.SchemeGroupVersion.String(), Kind: v1alpha1.RolloutKind}, ObjectMeta: metav1.ObjectMeta{Name: "test-rollout"}}
|
||||
sourceNamespacedName = client.ObjectKey{Name: sourceName, Namespace: namespaceName}
|
||||
targetNamespacedName = client.ObjectKey{Name: targetName, Namespace: namespaceName}
|
||||
c = DeploymentRolloutController{
|
||||
deploymentController: deploymentController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromInt(3),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("50%"),
|
||||
},
|
||||
},
|
||||
},
|
||||
rolloutStatus: &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState},
|
||||
parentController: &appRollout,
|
||||
recorder: event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout"),
|
||||
},
|
||||
targetNamespacedName: targetNamespacedName,
|
||||
},
|
||||
sourceNamespacedName: sourceNamespacedName,
|
||||
}
|
||||
|
||||
targetDeploy = appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String(), Kind: "Deployment"},
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespaceName, Name: targetName},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"env": "staging"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"env": "staging"}},
|
||||
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: targetName,
|
||||
Image: "stefanprodan/podinfo:5.0.3"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sourceDeploy = appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String(), Kind: "Deployment"},
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespaceName, Name: sourceName},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: pointer.Int32(10),
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"env": "staging"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"env": "staging"}},
|
||||
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: sourceName,
|
||||
Image: "stefanprodan/podinfo:4.0.6"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
ns = corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName,
|
||||
},
|
||||
}
|
||||
By("Create a namespace")
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
By("clean up after each test")
|
||||
k8sClient.Delete(ctx, &sourceDeploy)
|
||||
// Delete the target
|
||||
k8sClient.Delete(ctx, &targetDeploy)
|
||||
})
|
||||
|
||||
Context("TestNewDeploymentRolloutController", func() {
|
||||
It("init a Deployment Rollout Controller", func() {
|
||||
recorder := event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout")
|
||||
parentController := &v1alpha1.Rollout{ObjectMeta: metav1.ObjectMeta{Name: sourceName}}
|
||||
rolloutSpec := &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
rolloutStatus := &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState}
|
||||
workloadNamespacedName := client.ObjectKey{Name: sourceName, Namespace: namespaceName}
|
||||
got := NewDeploymentRolloutController(k8sClient, recorder, parentController, rolloutSpec, rolloutStatus,
|
||||
workloadNamespacedName, workloadNamespacedName)
|
||||
c := &DeploymentRolloutController{
|
||||
deploymentController: deploymentController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: workloadNamespacedName,
|
||||
},
|
||||
sourceNamespacedName: workloadNamespacedName,
|
||||
}
|
||||
Expect(got).Should(Equal(c))
|
||||
})
|
||||
})
|
||||
|
||||
Context("VerifySpec", func() {
|
||||
It("Could not fetch both deployment workload", func() {
|
||||
By("Create only the source deployment")
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("verify target size value", func() {
|
||||
By("Create the deployments, source size is 10")
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
c.rolloutSpec.TargetSize = pointer.Int32(8)
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("less than source size"))
|
||||
Expect(c.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(0))
|
||||
Expect(c.rolloutStatus.NewPodTemplateIdentifier).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("verify rollout spec hash", func() {
|
||||
By("Create the deployments")
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
targetDeploy.Spec.Replicas = pointer.Int32(1)
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
targetHash, _ := utils.ComputeSpecHash(targetDeploy.Spec)
|
||||
c.rolloutStatus.LastAppliedPodTemplateIdentifier = targetHash
|
||||
By("Verify should fail because the the target hash didn't change")
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err.Error()).Should(ContainSubstring("there is no difference between the source and target"))
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(c.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(10))
|
||||
Expect(c.rolloutStatus.NewPodTemplateIdentifier).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("verify rolloutBatch replica value", func() {
|
||||
By("Create the source deployment")
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
By("modify rollout batches")
|
||||
c.rolloutSpec.RolloutBatches = []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromInt(13),
|
||||
},
|
||||
}
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err.Error()).Should(ContainSubstring("the rollout plan batch size mismatch"))
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(c.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(10))
|
||||
Expect(c.rolloutStatus.NewPodTemplateIdentifier).Should(BeEmpty())
|
||||
|
||||
By("set the correct rollout target size")
|
||||
c.rolloutSpec.TargetSize = pointer.Int32(15)
|
||||
consistent, err = c.VerifySpec(ctx)
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(err.Error()).ShouldNot(ContainSubstring("the rollout plan batch size mismatch"))
|
||||
Expect(c.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(15))
|
||||
Expect(c.rolloutStatus.NewPodTemplateIdentifier).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("the deployment need to be stable if not paused", func() {
|
||||
By("create the source deployment with many pods")
|
||||
sourceDeploy.Spec.Replicas = pointer.Int32(50)
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
By("verify should fail b/c source is not stable")
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err.Error()).Should(ContainSubstring("is still being reconciled, need to be paused or stable"))
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(c.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(50))
|
||||
Expect(c.rolloutStatus.NewPodTemplateIdentifier).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("the deployment don't need to be paused if stable", func() {
|
||||
By("Create the source deployment with none")
|
||||
var sourceReplica int32 = 6
|
||||
sourceDeploy.Spec.Replicas = &sourceReplica
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
// test environment doesn't have deployment controller, has to fake it
|
||||
sourceDeploy.Status.Replicas = sourceReplica // this has to pass batch check
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
|
||||
targetDeploy.Spec.Replicas = pointer.Int32(0)
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
By("verify should not fail b/c of deployment not stable")
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeTrue())
|
||||
Expect(c.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(sourceReplica))
|
||||
Expect(c.rolloutStatus.NewPodTemplateIdentifier).ShouldNot(BeEmpty())
|
||||
})
|
||||
|
||||
It("deployment should not have controller", func() {
|
||||
By("Create deployments")
|
||||
sourceDeploy.Spec.Paused = true
|
||||
sourceDeploy.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
targetDeploy.Spec.Paused = true
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("verify should fail because deployment still has a controller")
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err.Error()).Should(ContainSubstring("has a controller owner"))
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(c.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(10))
|
||||
Expect(c.rolloutStatus.NewPodTemplateIdentifier).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("spec is valid", func() {
|
||||
By("Create a deployment")
|
||||
sourceDeploy.Spec.Paused = true
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
targetDeploy.Spec.Paused = true
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
By("verify should succeed")
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestInitialize", func() {
|
||||
It("failed to fetch Deployment", func() {
|
||||
initialized, err := c.Initialize(ctx)
|
||||
Expect(initialized).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("failed to claim Deployment as the owner reference is ill-formated", func() {
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(Succeed())
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(Succeed())
|
||||
initialized, err := c.Initialize(ctx)
|
||||
Expect(initialized).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("successfully initialized Deployment", func() {
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(Succeed())
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(Succeed())
|
||||
c.parentController.SetUID("abcdedg")
|
||||
c.rolloutStatus.RolloutTargetSize = 12
|
||||
initialized, err := c.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is claimed")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(len(sourceDeploy.GetOwnerReferences())).Should(Equal(1))
|
||||
Expect(sourceDeploy.GetOwnerReferences()[0].Kind).Should(Equal(v1alpha1.RolloutKindVersionKind.Kind))
|
||||
Expect(sourceDeploy.GetOwnerReferences()[0].UID).Should(BeEquivalentTo(c.parentController.GetUID()))
|
||||
Expect(sourceDeploy.Spec.Paused).Should(BeFalse())
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(10))
|
||||
By("Verify the target deployment is claimed and init to zero")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(len(targetDeploy.GetOwnerReferences())).Should(Equal(1))
|
||||
Expect(targetDeploy.GetOwnerReferences()[0].Kind).Should(Equal(v1alpha1.RolloutKindVersionKind.Kind))
|
||||
Expect(targetDeploy.GetOwnerReferences()[0].UID).Should(BeEquivalentTo(c.parentController.GetUID()))
|
||||
Expect(targetDeploy.Spec.Paused).Should(BeFalse())
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(2))
|
||||
})
|
||||
|
||||
It("successfully initialized deployment on resume/revert case", func() {
|
||||
sourceDeploy.Spec.Replicas = pointer.Int32(7)
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(Succeed())
|
||||
targetDeploy.Spec.Replicas = pointer.Int32(5)
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(Succeed())
|
||||
c.parentController.SetUID("abcdedg")
|
||||
c.rolloutStatus.RolloutTargetSize = 10
|
||||
initialized, err := c.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is claimed")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(len(sourceDeploy.GetOwnerReferences())).Should(Equal(1))
|
||||
Expect(sourceDeploy.GetOwnerReferences()[0].Kind).Should(Equal(v1alpha1.RolloutKindVersionKind.Kind))
|
||||
Expect(sourceDeploy.GetOwnerReferences()[0].UID).Should(BeEquivalentTo(c.parentController.GetUID()))
|
||||
Expect(sourceDeploy.Spec.Paused).Should(BeFalse())
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(7))
|
||||
By("Verify the target deployment is claimed with the right amount of replicas")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(len(targetDeploy.GetOwnerReferences())).Should(Equal(1))
|
||||
Expect(targetDeploy.GetOwnerReferences()[0].Kind).Should(Equal(v1alpha1.RolloutKindVersionKind.Kind))
|
||||
Expect(targetDeploy.GetOwnerReferences()[0].UID).Should(BeEquivalentTo(c.parentController.GetUID()))
|
||||
Expect(targetDeploy.Spec.Paused).Should(BeFalse())
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(
|
||||
c.rolloutStatus.RolloutTargetSize - *sourceDeploy.Spec.Replicas))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestRolloutOneBatchPods", func() {
|
||||
It("failed to fetch Deployment", func() {
|
||||
initialized, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(initialized).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("rollout increase first, first batch", func() {
|
||||
By("Create the source deployment")
|
||||
sourceDeploy.Spec.Replicas = pointer.Int32(10)
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("Create the target deployment")
|
||||
targetDeploy.Spec.Replicas = pointer.Int32(0)
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("rollout the first half")
|
||||
// rolloutRelaxSpec doesn't set the RolloutStrategy
|
||||
c.rolloutSpec = rolloutRelaxSpec
|
||||
c.rolloutStatus.CurrentBatch = 0
|
||||
c.rolloutStatus.RolloutTargetSize = *sourceDeploy.Spec.Replicas
|
||||
rolloutDone, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(10))
|
||||
By("Verify the target deployment is scaled up first")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(2))
|
||||
By("try to rollout the second half, fail because target status didn't change")
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("rollout the second half after fake target status update")
|
||||
// Replicas has to be more than ReadyReplicas
|
||||
targetDeploy.Status.Replicas = *targetDeploy.Spec.Replicas
|
||||
targetDeploy.Status.ReadyReplicas = *targetDeploy.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &targetDeploy)).Should(Succeed())
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is scaled down")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(8))
|
||||
By("Verify the target deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(2))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(2))
|
||||
})
|
||||
|
||||
It("rollout decrease first, first batch", func() {
|
||||
By("Create the source deployment")
|
||||
sourceDeploy.Spec.Replicas = pointer.Int32(10)
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("Create the target deployment")
|
||||
targetDeploy.Spec.Replicas = pointer.Int32(0)
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("rollout the first half to decrease first")
|
||||
c.rolloutSpec = rolloutRelaxSpec
|
||||
c.rolloutSpec.RolloutStrategy = v1alpha1.DecreaseFirstRolloutStrategyType
|
||||
c.rolloutStatus.CurrentBatch = 0
|
||||
c.rolloutStatus.RolloutTargetSize = *sourceDeploy.Spec.Replicas
|
||||
rolloutDone, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is scaled down first")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(8))
|
||||
By("Verify the target deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(0))
|
||||
By("try to rollout the second half, fail because target status didn't change")
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("rollout the second half after fake target status update")
|
||||
sourceDeploy.Status.Replicas = *sourceDeploy.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(8))
|
||||
By("Verify the target deployment is scaled up")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(2))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(2))
|
||||
})
|
||||
|
||||
It("rollout increase first, last batch", func() {
|
||||
By("Create the source deployment")
|
||||
sourceDeploy.Spec.Replicas = pointer.Int32(4)
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("Create the target deployment")
|
||||
targetDeploy.Spec.Replicas = pointer.Int32(6)
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("rollout the first half, omit strategy")
|
||||
c.rolloutSpec = rolloutRelaxSpec
|
||||
c.rolloutSpec.RolloutStrategy = v1alpha1.IncreaseFirstRolloutStrategyType
|
||||
c.rolloutStatus.CurrentBatch = 3
|
||||
c.rolloutStatus.RolloutTargetSize = *sourceDeploy.Spec.Replicas + *targetDeploy.Spec.Replicas
|
||||
rolloutDone, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(4))
|
||||
By("Verify the target deployment is scaled up first")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(10))
|
||||
By("try to rollout the second half, fail because target status didn't change")
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("try to rollout the second half, fail because target status didn't meet")
|
||||
targetDeploy.Status.Replicas = *targetDeploy.Spec.Replicas
|
||||
targetDeploy.Status.ReadyReplicas = *targetDeploy.Spec.Replicas - 1
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("rollout the second half after fake target status update")
|
||||
targetDeploy.Status.Replicas = *targetDeploy.Spec.Replicas
|
||||
targetDeploy.Status.ReadyReplicas = *targetDeploy.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &targetDeploy)).Should(Succeed())
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
By("Verify the source deployment is scaled down")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(0))
|
||||
By("Verify the target deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(10))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(10))
|
||||
})
|
||||
|
||||
It("rollout decrease first, last batch", func() {
|
||||
By("Create the source deployment")
|
||||
sourceDeploy.Spec.Replicas = pointer.Int32(4)
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
// set status as default is 0
|
||||
sourceDeploy.Status.Replicas = *sourceDeploy.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
By("Create the target deployment")
|
||||
targetDeploy.Spec.Replicas = pointer.Int32(6)
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("rollout the first half to decrease first")
|
||||
c.rolloutSpec = rolloutRelaxSpec
|
||||
c.rolloutSpec.RolloutStrategy = v1alpha1.DecreaseFirstRolloutStrategyType
|
||||
c.rolloutStatus.CurrentBatch = 3
|
||||
c.rolloutStatus.RolloutTargetSize = *sourceDeploy.Spec.Replicas + *targetDeploy.Spec.Replicas
|
||||
rolloutDone, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is scaled down first")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(0))
|
||||
By("Verify the target deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(6))
|
||||
By("try to rollout the second half, fail because target status didn't change")
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("try to rollout the second half, fail because target status didn't meet")
|
||||
sourceDeploy.Status.Replicas = *sourceDeploy.Spec.Replicas + 1
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("rollout the second half after fake source status update")
|
||||
sourceDeploy.Status.Replicas = *sourceDeploy.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
c.sourceDeploy = appsv1.Deployment{}
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(0))
|
||||
By("Verify the target deployment is scaled up")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(10))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(10))
|
||||
})
|
||||
|
||||
It("rollout increase first, revert case", func() {
|
||||
By("Create the deployments in the middle of rolling out")
|
||||
sourceDeploy.Spec.Replicas = pointer.Int32(6)
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
sourceDeploy.Status.Replicas = *sourceDeploy.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
By("Create the target deployment")
|
||||
targetDeploy.Spec.Replicas = pointer.Int32(14)
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
targetDeploy.Status.Replicas = *targetDeploy.Spec.Replicas
|
||||
targetDeploy.Status.ReadyReplicas = *targetDeploy.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &targetDeploy)).Should(Succeed())
|
||||
By("rollout the first batch to start the revert")
|
||||
c.rolloutSpec = rolloutRelaxSpec
|
||||
c.rolloutSpec.RolloutStrategy = v1alpha1.IncreaseFirstRolloutStrategyType
|
||||
c.rolloutStatus.CurrentBatch = 0
|
||||
c.rolloutStatus.RolloutTargetSize = 20
|
||||
rolloutDone, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(6))
|
||||
By("Verify the target deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(14))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(4))
|
||||
By("rollout the second batch")
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(6))
|
||||
By("Verify the target deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(14))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(6))
|
||||
By("rollout the third batch")
|
||||
c.rolloutStatus.CurrentBatch = 2
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(6))
|
||||
By("Verify the target deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(14))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(8))
|
||||
By("rollout the fourth batch")
|
||||
c.rolloutStatus.CurrentBatch = 3
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(6))
|
||||
By("Verify the target deployment is scaled up")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(20))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(8))
|
||||
})
|
||||
|
||||
It("rollout decrease first, revert case", func() {
|
||||
By("Create the deployments in the middle of rolling out")
|
||||
sourceDeploy.Spec.Replicas = pointer.Int32(14)
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
sourceDeploy.Status.Replicas = *sourceDeploy.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
By("Create the target deployment")
|
||||
targetDeploy.Spec.Replicas = pointer.Int32(6)
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
targetDeploy.Status.Replicas = *targetDeploy.Spec.Replicas
|
||||
targetDeploy.Status.ReadyReplicas = *targetDeploy.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &targetDeploy)).Should(Succeed())
|
||||
By("rollout the first batch to start the revert")
|
||||
c.rolloutSpec = rolloutRelaxSpec
|
||||
c.rolloutSpec.RolloutStrategy = v1alpha1.DecreaseFirstRolloutStrategyType
|
||||
c.rolloutStatus.CurrentBatch = 0
|
||||
c.rolloutStatus.RolloutTargetSize = 20
|
||||
rolloutDone, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(14))
|
||||
By("Verify the target deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(6))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(4))
|
||||
By("rollout the second batch")
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(14))
|
||||
By("Verify the target deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(6))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(6))
|
||||
By("rollout the third batch")
|
||||
c.rolloutStatus.CurrentBatch = 2
|
||||
rolloutDone, err = c.RolloutOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("Verify the source deployment is not touched")
|
||||
Expect(k8sClient.Get(ctx, sourceNamespacedName, &sourceDeploy))
|
||||
Expect(*sourceDeploy.Spec.Replicas).Should(BeEquivalentTo(12))
|
||||
By("Verify the target deployment is scaled up")
|
||||
Expect(k8sClient.Get(ctx, targetNamespacedName, &targetDeploy))
|
||||
Expect(*targetDeploy.Spec.Replicas).Should(BeEquivalentTo(6))
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(6))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestCheckOneBatchPods", func() {
|
||||
It("failed to fetch Deployment", func() {
|
||||
initialized, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(initialized).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("check batches with rollout increase first", func() {
|
||||
By("Create the source deployment")
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("Create the target deployment")
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("check first batch")
|
||||
c.rolloutSpec = rolloutRelaxSpec
|
||||
c.rolloutSpec.RolloutStrategy = v1alpha1.IncreaseFirstRolloutStrategyType
|
||||
c.rolloutStatus.CurrentBatch = 0
|
||||
c.rolloutStatus.RolloutTargetSize = 20
|
||||
By("source more than goal")
|
||||
sourceDeploy.Status.Replicas = 17
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
rolloutDone, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("source meet goal")
|
||||
sourceDeploy.Status.Replicas = 16
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
rolloutDone, err = c.CheckOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("source less than goal")
|
||||
sourceDeploy.Status.Replicas = 15
|
||||
Expect(k8sClient.Status().Update(ctx, &sourceDeploy)).Should(Succeed())
|
||||
rolloutDone, err = c.CheckOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("check batches with rollout decrease first", func() {
|
||||
By("Create the source deployment")
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("Create the target deployment")
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("check first batch")
|
||||
c.rolloutSpec = rolloutRelaxSpec
|
||||
c.rolloutSpec.RolloutStrategy = v1alpha1.DecreaseFirstRolloutStrategyType
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
c.rolloutStatus.RolloutTargetSize = 20
|
||||
By("target more than goal")
|
||||
targetDeploy.Status.Replicas = 9
|
||||
targetDeploy.Status.ReadyReplicas = 7
|
||||
Expect(k8sClient.Status().Update(ctx, &targetDeploy)).Should(Succeed())
|
||||
rolloutDone, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("target meet goal")
|
||||
targetDeploy.Status.Replicas = 7
|
||||
targetDeploy.Status.ReadyReplicas = 6
|
||||
Expect(k8sClient.Status().Update(ctx, &targetDeploy)).Should(Succeed())
|
||||
rolloutDone, err = c.CheckOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
By("target less than goal")
|
||||
targetDeploy.Status.Replicas = 6
|
||||
targetDeploy.Status.ReadyReplicas = 5
|
||||
Expect(k8sClient.Status().Update(ctx, &targetDeploy)).Should(Succeed())
|
||||
rolloutDone, err = c.CheckOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
By("target less than goal but unavailable allowed")
|
||||
unavil := intstr.FromString("10%")
|
||||
c.rolloutSpec.RolloutBatches[1].MaxUnavailable = &unavil
|
||||
rolloutDone, err = c.CheckOneBatchPods(ctx)
|
||||
Expect(rolloutDone).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalizeOneBatch", func() {
|
||||
It("failed to fetch Deployment", func() {
|
||||
finalized, err := c.FinalizeOneBatch(ctx)
|
||||
Expect(finalized).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("test rollout batch configured correctly", func() {
|
||||
By("Create the deployments")
|
||||
sourceDeploy.Spec.Replicas = pointer.Int32(8)
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
targetDeploy.Spec.Replicas = pointer.Int32(5)
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("Fail if the targets don't add up")
|
||||
c.rolloutSpec = rolloutRelaxSpec
|
||||
c.rolloutSpec.RolloutStrategy = v1alpha1.DecreaseFirstRolloutStrategyType
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
c.rolloutStatus.RolloutTargetSize = 10
|
||||
finalized, err := c.FinalizeOneBatch(ctx)
|
||||
Expect(finalized).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("deployment targets don't match total rollout"))
|
||||
By("Success if they do")
|
||||
// sum of target and source
|
||||
c.rolloutStatus.RolloutTargetSize = 13
|
||||
finalized, err = c.FinalizeOneBatch(ctx)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalize", func() {
|
||||
It("failed to fetch deployment", func() {
|
||||
finalized := c.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("release success without ownership", func() {
|
||||
By("Create the deployments")
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("no op success if we are not the owner")
|
||||
finalized := c.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("release success as the owner", func() {
|
||||
By("Create the deployments")
|
||||
sourceDeploy.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &sourceDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
targetDeploy.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &targetDeploy)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
By("success if we are the owner")
|
||||
finalized := c.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,99 +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 workloads
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
)
|
||||
|
||||
func TestCalculateCurrentSource(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
rolloutSpec *v1alpha1.RolloutPlan
|
||||
currentBatch int32
|
||||
totalSize int32
|
||||
want int32
|
||||
}{
|
||||
"PercentBatch0": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
currentBatch: 0,
|
||||
totalSize: 10,
|
||||
want: 8,
|
||||
},
|
||||
"PercentBatch1": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
currentBatch: 1,
|
||||
totalSize: 10,
|
||||
want: 4,
|
||||
},
|
||||
"PercentBatch2": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
currentBatch: 2,
|
||||
totalSize: 100,
|
||||
want: 10,
|
||||
},
|
||||
"PercentBatch3": {
|
||||
rolloutSpec: rolloutPercentSpec,
|
||||
currentBatch: 3,
|
||||
totalSize: 1000,
|
||||
want: 0,
|
||||
},
|
||||
"MixedBatch0": {
|
||||
rolloutSpec: rolloutMixedSpec,
|
||||
currentBatch: 0,
|
||||
totalSize: 100,
|
||||
want: 99,
|
||||
},
|
||||
"MixedBatch1": {
|
||||
rolloutSpec: rolloutMixedSpec,
|
||||
currentBatch: 1,
|
||||
totalSize: 100,
|
||||
want: 79,
|
||||
},
|
||||
"MixedBatch2": {
|
||||
rolloutSpec: rolloutMixedSpec,
|
||||
currentBatch: 2,
|
||||
totalSize: 15,
|
||||
want: 3,
|
||||
},
|
||||
"RelaxedBatch3": {
|
||||
rolloutSpec: rolloutRelaxSpec,
|
||||
currentBatch: 3,
|
||||
totalSize: 15,
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
controller := DeploymentRolloutController{
|
||||
deploymentController: deploymentController{
|
||||
workloadController: workloadController{
|
||||
rolloutSpec: tc.rolloutSpec,
|
||||
rolloutStatus: &v1alpha1.RolloutStatus{
|
||||
CurrentBatch: tc.currentBatch,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ct := controller.calculateCurrentSource(tc.totalSize)
|
||||
if tc.want-ct != 0 {
|
||||
t.Errorf("\n%s\ncalculateCurrentTarget(...): -want count, +got count:\n%d, %d", name, tc.want, ct)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,296 +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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
// DeploymentScaleController is responsible for handle scale Deployment type of workloads
|
||||
type DeploymentScaleController struct {
|
||||
deploymentController
|
||||
deploy *appsv1.Deployment
|
||||
}
|
||||
|
||||
// NewDeploymentScaleController creates Deployment scale controller
|
||||
func NewDeploymentScaleController(client client.Client, recorder event.Recorder, parentController oam.Object, rolloutSpec *v1alpha1.RolloutPlan, rolloutStatus *v1alpha1.RolloutStatus, workloadName types.NamespacedName) *DeploymentScaleController {
|
||||
return &DeploymentScaleController{
|
||||
deploymentController: deploymentController{
|
||||
workloadController: workloadController{
|
||||
client: client,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: workloadName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VerifySpec verifies that the deployment is stable and can be scaled
|
||||
func (s *DeploymentScaleController) VerifySpec(ctx context.Context) (bool, error) {
|
||||
var verifyErr error
|
||||
defer func() {
|
||||
if verifyErr != nil {
|
||||
klog.Error(verifyErr)
|
||||
s.recorder.Event(s.parentController, event.Warning("VerifyFailed", verifyErr))
|
||||
}
|
||||
}()
|
||||
|
||||
// the rollout has to have a target size in the scale case
|
||||
if s.rolloutSpec.TargetSize == nil {
|
||||
return false, fmt.Errorf("the rollout plan is attempting to scale the deployment %s without a target",
|
||||
s.targetNamespacedName.Name)
|
||||
}
|
||||
// record the target size
|
||||
s.rolloutStatus.RolloutTargetSize = *s.rolloutSpec.TargetSize
|
||||
klog.InfoS("record the target size", "target size", *s.rolloutSpec.TargetSize)
|
||||
|
||||
// fetch the deployment and get its current size
|
||||
originalSize, verifyErr := s.size(ctx)
|
||||
if verifyErr != nil {
|
||||
// do not fail the rollout because we can't get the resource
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
s.rolloutStatus.RolloutOriginalSize = originalSize
|
||||
klog.InfoS("record the original size", "original size", originalSize)
|
||||
|
||||
// check if the rollout batch replicas scale up/down to the replicas target
|
||||
if verifyErr = verifyBatchesWithScale(s.rolloutSpec, int(originalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize)); verifyErr != nil {
|
||||
return false, verifyErr
|
||||
}
|
||||
|
||||
// check if the deployment is scaling
|
||||
if !s.deploy.Spec.Paused && originalSize != s.deploy.Status.Replicas {
|
||||
verifyErr = fmt.Errorf("the deployment %s is in the middle of scaling, target size = %d, real size = %d",
|
||||
s.deploy.GetName(), originalSize, s.deploy.Status.Replicas)
|
||||
// do not fail the rollout, we can wait
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check if the deployment is upgrading
|
||||
if !s.deploy.Spec.Paused && s.deploy.Status.UpdatedReplicas != originalSize {
|
||||
verifyErr = fmt.Errorf("the deployment %s is in the middle of updating, target size = %d, updated pod = %d",
|
||||
s.deploy.GetName(), originalSize, s.deploy.Status.UpdatedReplicas)
|
||||
// do not fail the rollout, we can wait
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check if the deployment has any controller
|
||||
if controller := metav1.GetControllerOf(s.deploy); controller != nil {
|
||||
return false, fmt.Errorf("the deployment %s has a controller owner %s",
|
||||
s.deploy.GetName(), controller.String())
|
||||
}
|
||||
|
||||
// mark the scale verified
|
||||
s.recorder.Event(s.parentController, event.Normal("Scale Verified",
|
||||
"Rollout spec and the deployment resource are verified"))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Initialize makes sure that the deployment is under our control
|
||||
func (s *DeploymentScaleController) Initialize(ctx context.Context) (bool, error) {
|
||||
if err := s.fetchDeployment(ctx); err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
claimedBefore, err := s.claimDeployment(ctx, s.deploy, nil)
|
||||
if err != nil {
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
if !claimedBefore {
|
||||
// mark the rollout initialized
|
||||
s.recorder.Event(s.parentController, event.Normal("Scale Initialized", "deployment is initialized"))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RolloutOneBatchPods calculates the number of pods we can scale to according to the rollout spec
|
||||
func (s *DeploymentScaleController) RolloutOneBatchPods(ctx context.Context) (bool, error) {
|
||||
if err := s.fetchDeployment(ctx); err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// set the replica according to the batch
|
||||
newPodTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), int(s.rolloutStatus.CurrentBatch))
|
||||
|
||||
if err := s.scaleDeployment(ctx, s.deploy, int32(newPodTarget)); err != nil {
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// record the scale
|
||||
klog.InfoS("scale one batch", "current batch", s.rolloutStatus.CurrentBatch)
|
||||
s.recorder.Event(s.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("Submitted scale quest for batch %d", s.rolloutStatus.CurrentBatch)))
|
||||
s.rolloutStatus.UpgradedReplicas = int32(newPodTarget)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckOneBatchPods checks to see if the pods are scaled according to the rollout plan
|
||||
func (s *DeploymentScaleController) CheckOneBatchPods(ctx context.Context) (bool, error) {
|
||||
if err := s.fetchDeployment(ctx); err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
newPodTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), int(s.rolloutStatus.CurrentBatch))
|
||||
// get the number of ready pod from deployment
|
||||
// TODO: should we use the replica number when we shrink?
|
||||
readyPodCount := int(s.deploy.Status.ReadyReplicas)
|
||||
currentBatch := s.rolloutSpec.RolloutBatches[s.rolloutStatus.CurrentBatch]
|
||||
unavail := 0
|
||||
if currentBatch.MaxUnavailable != nil {
|
||||
unavail, _ = intstr.GetValueFromIntOrPercent(currentBatch.MaxUnavailable,
|
||||
util.Abs(int(s.rolloutStatus.RolloutTargetSize-s.rolloutStatus.RolloutOriginalSize)), true)
|
||||
}
|
||||
klog.InfoS("checking the scaling progress", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"new pod count target", newPodTarget, "new ready pod count", readyPodCount,
|
||||
"max unavailable pod allowed", unavail)
|
||||
s.rolloutStatus.UpgradedReadyReplicas = int32(readyPodCount)
|
||||
targetReached := false
|
||||
// nolint
|
||||
if s.rolloutStatus.RolloutOriginalSize <= s.rolloutStatus.RolloutTargetSize && unavail+readyPodCount >= newPodTarget {
|
||||
targetReached = true
|
||||
} else if s.rolloutStatus.RolloutOriginalSize > s.rolloutStatus.RolloutTargetSize && readyPodCount <= newPodTarget {
|
||||
targetReached = true
|
||||
}
|
||||
if targetReached {
|
||||
// record the successful upgrade
|
||||
klog.InfoS("the current batch is ready", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"target", newPodTarget, "readyPodCount", readyPodCount, "max unavailable allowed", unavail)
|
||||
s.recorder.Event(s.parentController, event.Normal("Batch Available",
|
||||
fmt.Sprintf("Batch %d is available", s.rolloutStatus.CurrentBatch)))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// continue to verify
|
||||
klog.InfoS("the batch is not ready yet", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"target", newPodTarget, "readyPodCount", readyPodCount, "max unavailable allowed", unavail)
|
||||
s.rolloutStatus.RolloutRetry("the batch is not ready yet")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// FinalizeOneBatch makes sure that the current batch and replica count in the status are validate
|
||||
func (s *DeploymentScaleController) FinalizeOneBatch(ctx context.Context) (bool, error) {
|
||||
status := s.rolloutStatus
|
||||
spec := s.rolloutSpec
|
||||
if spec.BatchPartition != nil && *spec.BatchPartition < status.CurrentBatch {
|
||||
err := fmt.Errorf("the current batch value in the status is greater than the batch partition")
|
||||
klog.ErrorS(err, "we have moved past the user defined partition", "user specified batch partition",
|
||||
*spec.BatchPartition, "current batch we are working on", status.CurrentBatch)
|
||||
return false, err
|
||||
}
|
||||
// special case the equal case
|
||||
if s.rolloutStatus.RolloutOriginalSize == s.rolloutStatus.RolloutTargetSize {
|
||||
return true, nil
|
||||
}
|
||||
// we just make sure the target is right
|
||||
finishedPodCount := int(status.UpgradedReplicas)
|
||||
currentBatch := int(status.CurrentBatch)
|
||||
// calculate the pod target just before the current batch
|
||||
preBatchTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), currentBatch-1)
|
||||
// calculate the pod target with the current batch
|
||||
curBatchTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), currentBatch)
|
||||
// the recorded number should be at least as much as the all the pods before the current batch
|
||||
if finishedPodCount < util.Min(preBatchTarget, curBatchTarget) {
|
||||
err := fmt.Errorf("the upgraded replica in the status is less than the lower bound")
|
||||
klog.ErrorS(err, "rollout status inconsistent", "existing pod target", finishedPodCount,
|
||||
"the lower bound", util.Min(preBatchTarget, curBatchTarget))
|
||||
return false, err
|
||||
}
|
||||
// the recorded number should be not as much as the all the pods including the active batch
|
||||
if finishedPodCount > util.Max(preBatchTarget, curBatchTarget) {
|
||||
err := fmt.Errorf("the upgraded replica in the status is greater than the upper bound")
|
||||
klog.ErrorS(err, "rollout status inconsistent", "existing pod target", finishedPodCount,
|
||||
"the upper bound", util.Max(preBatchTarget, curBatchTarget))
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Finalize makes sure the deployment is scaled and ready to use
|
||||
func (s *DeploymentScaleController) Finalize(ctx context.Context, succeed bool) bool {
|
||||
if err := s.fetchDeployment(ctx); err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
releasedBefore, err := s.releaseDeployment(ctx, s.deploy)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !releasedBefore {
|
||||
// mark the resource finalized
|
||||
s.recorder.Event(s.parentController, event.Normal("Scale Finalized",
|
||||
fmt.Sprintf("Scale resource are finalized, succeed := %t", succeed)))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// size fetches the Deloyment and returns the replicas (not the actual number of pods)
|
||||
func (s *DeploymentScaleController) size(ctx context.Context) (int32, error) {
|
||||
if s.deploy == nil {
|
||||
if err := s.fetchDeployment(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return getDeploymentReplicas(s.deploy), nil
|
||||
}
|
||||
|
||||
func (s *DeploymentScaleController) fetchDeployment(ctx context.Context) error {
|
||||
// get the deployment
|
||||
workload := appsv1.Deployment{}
|
||||
if err := s.client.Get(ctx, s.targetNamespacedName, &workload); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
s.recorder.Event(s.parentController, event.Warning("Failed to get the Deployment", err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
s.deploy = &workload
|
||||
return nil
|
||||
}
|
||||
@@ -1,504 +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 workloads
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("deployment controller", func() {
|
||||
var (
|
||||
s DeploymentScaleController
|
||||
ns corev1.Namespace
|
||||
name string
|
||||
namespace string
|
||||
deployment appsv1.Deployment
|
||||
namespacedName client.ObjectKey
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
namespace = "rollout-ns"
|
||||
name = "rollout1"
|
||||
appRollout := v1alpha1.Rollout{TypeMeta: metav1.TypeMeta{APIVersion: v1alpha1.SchemeGroupVersion.String(), Kind: v1alpha1.RolloutKind}, ObjectMeta: metav1.ObjectMeta{Name: name}}
|
||||
namespacedName = client.ObjectKey{Name: name, Namespace: namespace}
|
||||
s = DeploymentScaleController{
|
||||
deploymentController: deploymentController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{
|
||||
TargetSize: pointer.Int32(10),
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
},
|
||||
},
|
||||
rolloutStatus: &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState},
|
||||
parentController: &appRollout,
|
||||
recorder: event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout"),
|
||||
},
|
||||
targetNamespacedName: namespacedName,
|
||||
},
|
||||
}
|
||||
|
||||
deployment = appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String(), Kind: "Deployment"},
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: pointer.Int32(1),
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"env": "staging"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"env": "staging"}},
|
||||
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: name, Image: "nginx"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ns = corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
By("Create a namespace")
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
By("clean up")
|
||||
k8sClient.Delete(ctx, &deployment)
|
||||
})
|
||||
|
||||
Context("TestNewDeploymentScaleController", func() {
|
||||
It("init a Deployment Scale Controller", func() {
|
||||
recorder := event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout")
|
||||
parentController := &v1alpha1.Rollout{ObjectMeta: metav1.ObjectMeta{Name: name}}
|
||||
rolloutSpec := &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
rolloutStatus := &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState}
|
||||
workloadNamespacedName := client.ObjectKey{Name: name, Namespace: namespace}
|
||||
got := NewDeploymentScaleController(k8sClient, recorder, parentController, rolloutSpec, rolloutStatus, workloadNamespacedName)
|
||||
controller := &DeploymentScaleController{
|
||||
deploymentController: deploymentController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: workloadNamespacedName,
|
||||
},
|
||||
}
|
||||
Expect(got).Should(Equal(controller))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestVerifySpec", func() {
|
||||
It("rollout need a target size", func() {
|
||||
s.rolloutSpec.TargetSize = nil
|
||||
ligit, err := s.VerifySpec(ctx)
|
||||
Expect(ligit).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("without a target"))
|
||||
})
|
||||
|
||||
It("could not fetch Deployment workload", func() {
|
||||
ligit, err := s.VerifySpec(ctx)
|
||||
Expect(ligit).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("rollout batch doesn't fit scale target", func() {
|
||||
By("Create a Deployment")
|
||||
deployment.Spec.Replicas = pointer.Int32(15)
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("Verify should fail as the scale batches don't match")
|
||||
s.rolloutSpec.RolloutBatches[2].Replicas = intstr.FromInt(10)
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("the deployment is in the middle of scaling", func() {
|
||||
By("Create a Deployment")
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
By("verify should fail because replica does not match")
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("the deployment is in the middle of updating", func() {
|
||||
By("Create a Deployment and set as paused")
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
By("Update the Deployment status")
|
||||
deployment.Status.Replicas = 1
|
||||
Expect(k8sClient.Status().Update(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("verify should fail because replica are not upgraded")
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("spec is valid", func() {
|
||||
By("Create a Deployment and set as paused")
|
||||
deployment.Spec.Paused = true
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
By("Update the Deployment status")
|
||||
deployment.Status.Replicas = 1
|
||||
deployment.Status.UpdatedReplicas = 1
|
||||
deployment.Status.ReadyReplicas = 1
|
||||
Expect(k8sClient.Status().Update(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("verify should pass and record the size")
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeTrue())
|
||||
Expect(s.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(10))
|
||||
Expect(s.rolloutStatus.RolloutOriginalSize).Should(BeEquivalentTo(1))
|
||||
})
|
||||
|
||||
It("spec is valid, if it's paused but replicas not consistent", func() {
|
||||
By("Create a Deployment and set as paused")
|
||||
deployment.Spec.Paused = true
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
// do not update status
|
||||
|
||||
By("verify should pass and record the size")
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeTrue())
|
||||
Expect(s.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(10))
|
||||
Expect(s.rolloutStatus.RolloutOriginalSize).Should(BeEquivalentTo(1))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestInitialize", func() {
|
||||
BeforeEach(func() {
|
||||
deployment.Spec.Paused = true
|
||||
})
|
||||
|
||||
It("could not fetch Deployment workload", func() {
|
||||
consistent, err := s.Initialize(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("failed to patch the owner of Deployment", func() {
|
||||
By("Create a Deployment")
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("initialize will fail because deployment has wrong owner reference")
|
||||
initialized, err := s.Initialize(ctx)
|
||||
Expect(initialized).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("workload Deployment is controlled by appRollout already", func() {
|
||||
By("Create a Deployment")
|
||||
deployment.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("initialize succeed without patching")
|
||||
initialized, err := s.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("successfully initialized Deployment", func() {
|
||||
By("create deployment")
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("initialize succeeds")
|
||||
s.parentController.SetUID("1231586900")
|
||||
initialized, err := s.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestRolloutOneBatchPods", func() {
|
||||
It("could not fetch Deployment workload", func() {
|
||||
consistent, err := s.RolloutOneBatchPods(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("successfully rollout, current batch number is not equal to the expected one", func() {
|
||||
By("Create a Deployment")
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("rollout the second batch of current deployment")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 0
|
||||
s.rolloutStatus.RolloutTargetSize = 10
|
||||
done, err := s.RolloutOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(3))
|
||||
Expect(k8sClient.Get(ctx, s.targetNamespacedName, &deployment)).Should(Succeed())
|
||||
Expect(*deployment.Spec.Replicas).Should(BeEquivalentTo(3))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestCheckOneBatchPods", func() {
|
||||
It("could not fetch Deployment workload", func() {
|
||||
consistent, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("current ready Pod is less than expected during increase", func() {
|
||||
By("Create the Deployment")
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
By("Update the Deployment status")
|
||||
deployment.Status.Replicas = 3
|
||||
deployment.Status.ReadyReplicas = 3
|
||||
Expect(k8sClient.Status().Update(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("checking should fail as not enough pod ready")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 2
|
||||
s.rolloutStatus.RolloutTargetSize = 10
|
||||
done, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(deployment.Status.ReadyReplicas))
|
||||
|
||||
// set the rollout batch spec allow unavailable
|
||||
perc := intstr.FromString("20%")
|
||||
s.rolloutSpec.RolloutBatches[1] = v1alpha1.RolloutBatch{
|
||||
Replicas: perc,
|
||||
MaxUnavailable: &perc,
|
||||
}
|
||||
By("checking one batch should succeed with unavailble allowed")
|
||||
done, err = s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(deployment.Status.ReadyReplicas))
|
||||
})
|
||||
|
||||
It("current ready Pod is more than expected during decrease", func() {
|
||||
By("Create the Deployment")
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
By("Update the Deployment status")
|
||||
deployment.Status.Replicas = 10
|
||||
deployment.Status.ReadyReplicas = 10
|
||||
Expect(k8sClient.Status().Update(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("checking should fail as not enough pod ready")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 12
|
||||
s.rolloutStatus.RolloutTargetSize = 5
|
||||
done, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(deployment.Status.ReadyReplicas))
|
||||
|
||||
// set the rollout batch spec allow unavailable
|
||||
perc := intstr.FromString("20%")
|
||||
s.rolloutSpec.RolloutBatches[1] = v1alpha1.RolloutBatch{
|
||||
Replicas: perc,
|
||||
MaxUnavailable: &perc,
|
||||
}
|
||||
By("checking one batch should still fail even with unavailble allowed")
|
||||
done, err = s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(deployment.Status.ReadyReplicas))
|
||||
})
|
||||
|
||||
It("there are more pods shrunk during decrease", func() {
|
||||
By("Create the Deployment")
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
By("Update the Deployment status")
|
||||
deployment.Status.Replicas = 8
|
||||
deployment.Status.ReadyReplicas = 8
|
||||
Expect(k8sClient.Status().Update(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("checking should pass even with not enough pod ready")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 12
|
||||
s.rolloutStatus.RolloutTargetSize = 5
|
||||
done, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(deployment.Status.ReadyReplicas))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalizeOneBatch", func() {
|
||||
BeforeEach(func() {
|
||||
s.rolloutSpec.RolloutBatches[0] = v1alpha1.RolloutBatch{
|
||||
Replicas: intstr.FromInt(2),
|
||||
}
|
||||
})
|
||||
|
||||
It("test illegal batch partition", func() {
|
||||
By("finalizing one batch")
|
||||
s.rolloutSpec.BatchPartition = pointer.Int32(2)
|
||||
s.rolloutStatus.CurrentBatch = 3
|
||||
done, err := s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("the current batch value in the status is greater than the batch partition"))
|
||||
})
|
||||
|
||||
It("test finalize during increase", func() {
|
||||
By("finalizing one batch with not enough")
|
||||
s.rolloutStatus.UpgradedReplicas = 6
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 5
|
||||
s.rolloutStatus.RolloutTargetSize = 12
|
||||
done, err := s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring(" upgraded replica in the status is less than the lower bound"))
|
||||
|
||||
By("finalizing one batch with just enough")
|
||||
s.rolloutStatus.UpgradedReplicas = 7
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch with all")
|
||||
s.rolloutStatus.UpgradedReplicas = 9
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch with more than")
|
||||
s.rolloutStatus.UpgradedReplicas = 12
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("upgraded replica in the status is greater than the upper bound"))
|
||||
})
|
||||
|
||||
It("test finalize during decrease", func() {
|
||||
By("finalizing one batch with too many")
|
||||
s.rolloutStatus.UpgradedReplicas = 13
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 14
|
||||
s.rolloutStatus.RolloutTargetSize = 2
|
||||
done, err := s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("upgraded replica in the status is greater than the upper bound"))
|
||||
|
||||
By("finalizing one batch with just enough")
|
||||
s.rolloutStatus.UpgradedReplicas = 12
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch with all")
|
||||
s.rolloutStatus.UpgradedReplicas = 9
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch with not enough")
|
||||
s.rolloutStatus.UpgradedReplicas = 8
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring(" upgraded replica in the status is less than the lower bound"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalize", func() {
|
||||
It("failed to fetch Deployment", func() {
|
||||
By("finalizing")
|
||||
finalized := s.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("Already finalize Deployment", func() {
|
||||
By("Create a Deployment")
|
||||
deployment.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: "notRollout",
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("finalizing without patch")
|
||||
finalized := s.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("successfully to finalize Deployment", func() {
|
||||
By("Create a Deployment")
|
||||
deployment.SetOwnerReferences([]metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
},
|
||||
{
|
||||
APIVersion: corev1.SchemeGroupVersion.String(),
|
||||
Kind: "Deployment",
|
||||
Name: "def",
|
||||
UID: "998877745",
|
||||
},
|
||||
})
|
||||
Expect(k8sClient.Create(ctx, &deployment)).Should(Succeed())
|
||||
|
||||
By("finalizing with patch")
|
||||
finalized := s.Finalize(ctx, false)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
Expect(k8sClient.Get(ctx, s.targetNamespacedName, &deployment)).Should(Succeed())
|
||||
Expect(len(deployment.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
Expect(deployment.GetOwnerReferences()[0].Kind).Should(Equal("Deployment"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,125 +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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
)
|
||||
|
||||
type statefulSetController struct {
|
||||
workloadController
|
||||
targetNamespacedName types.NamespacedName
|
||||
}
|
||||
|
||||
// add the parent controller to the owner of the StatefulSet, and initialize the size
|
||||
// before kicking start the update and start from every pod in the old version
|
||||
func (c *statefulSetController) claimStatefulSet(ctx context.Context, statefulSet *apps.StatefulSet) (bool, error) {
|
||||
if controller := metav1.GetControllerOf(statefulSet); controller != nil &&
|
||||
(controller.Kind == v1alpha1.RolloutKind && controller.APIVersion == v1alpha1.SchemeGroupVersion.String()) {
|
||||
// it's already there
|
||||
return true, nil
|
||||
}
|
||||
|
||||
statefulSetPatch := client.MergeFrom(statefulSet.DeepCopy())
|
||||
|
||||
// add the parent controller to the owner of the StatefulSet
|
||||
ref := metav1.NewControllerRef(c.parentController, c.parentController.GetObjectKind().GroupVersionKind())
|
||||
statefulSet.SetOwnerReferences(append(statefulSet.GetOwnerReferences(), *ref))
|
||||
|
||||
// patch the StatefulSet
|
||||
if err := c.client.Patch(ctx, statefulSet, statefulSetPatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
|
||||
c.recorder.Event(c.parentController, event.Warning("Failed to the start the StatefulSet update", err))
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// scale the StatefulSet
|
||||
func (c *statefulSetController) scaleStatefulSet(ctx context.Context, statefulSet *apps.StatefulSet, size int32) error {
|
||||
statefulSetPatch := client.MergeFrom(statefulSet.DeepCopy())
|
||||
statefulSet.Spec.Replicas = pointer.Int32(size)
|
||||
|
||||
// patch the StatefulSet
|
||||
if err := c.client.Patch(ctx, statefulSet, statefulSetPatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
|
||||
c.recorder.Event(c.parentController, event.Warning(event.Reason(fmt.Sprintf(
|
||||
"Failed to update the StatefulSet %s to the correct target %d", statefulSet.GetName(), size)), err))
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
klog.InfoS("Submitted upgrade quest for StatefulSet", "StatefulSet",
|
||||
statefulSet.GetName(), "target replica size", size, "batch", c.rolloutStatus.CurrentBatch)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *statefulSetController) setPartition(ctx context.Context, statefulSet *apps.StatefulSet, partition int32) error {
|
||||
statefulSetPatch := client.MergeFrom(statefulSet.DeepCopy())
|
||||
statefulSet.Spec.UpdateStrategy.RollingUpdate.Partition = pointer.Int32(partition)
|
||||
|
||||
// patch the StatefulSet
|
||||
if err := c.client.Patch(ctx, statefulSet, statefulSetPatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
|
||||
c.recorder.Event(c.parentController, event.Warning(event.Reason(fmt.Sprintf(
|
||||
"Failed to update the partition of StatefulSet %s to the correct target %d", statefulSet.GetName(), partition)), err))
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
klog.InfoS("Submitted upgrade quest for StatefulSet", "StatefulSet",
|
||||
statefulSet.GetName(), "target partition", partition, "batch", c.rolloutStatus.CurrentBatch)
|
||||
return nil
|
||||
}
|
||||
|
||||
// remove the parent controller from the StatefulSet's owner list
|
||||
func (c *statefulSetController) releaseStatefulSet(ctx context.Context, statefulSet *apps.StatefulSet) (bool, error) {
|
||||
statefulSetPatch := client.MergeFrom(statefulSet.DeepCopy())
|
||||
|
||||
var newOwnerList []metav1.OwnerReference
|
||||
found := false
|
||||
for _, owner := range statefulSet.GetOwnerReferences() {
|
||||
if owner.Kind == v1alpha1.RolloutKind && owner.APIVersion == v1alpha1.SchemeGroupVersion.String() &&
|
||||
owner.Controller != nil && *owner.Controller {
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
newOwnerList = append(newOwnerList, owner)
|
||||
}
|
||||
if !found {
|
||||
klog.InfoS("the StatefulSet is already released", "StatefulSet", statefulSet.Name)
|
||||
return true, nil
|
||||
}
|
||||
statefulSet.SetOwnerReferences(newOwnerList)
|
||||
|
||||
// patch the StatefulSet
|
||||
if err := c.client.Patch(ctx, statefulSet, statefulSetPatch, client.FieldOwner(c.parentController.GetUID())); err != nil {
|
||||
c.recorder.Event(c.parentController, event.Warning("Failed to the release the StatefulSet", err))
|
||||
c.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -1,300 +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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
"github.com/pkg/errors"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
// StatefulSetRolloutController is responsible for handle rollout StatefulSet type of workloads
|
||||
type StatefulSetRolloutController struct {
|
||||
statefulSetController
|
||||
statefulSet *appsv1.StatefulSet
|
||||
}
|
||||
|
||||
// NewStatefulSetRolloutController creates StatefulSet rollout controller
|
||||
func NewStatefulSetRolloutController(client client.Client, recorder event.Recorder, parentController oam.Object, rolloutSpec *v1alpha1.RolloutPlan, rolloutStatus *v1alpha1.RolloutStatus,
|
||||
targetNamespacedName types.NamespacedName) *StatefulSetRolloutController {
|
||||
return &StatefulSetRolloutController{
|
||||
statefulSetController: statefulSetController{
|
||||
workloadController: workloadController{
|
||||
client: client,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: targetNamespacedName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VerifySpec verifies that the rollout resource is consistent with the rollout spec
|
||||
func (s *StatefulSetRolloutController) VerifySpec(ctx context.Context) (bool, error) {
|
||||
var verifyErr error
|
||||
|
||||
defer func() {
|
||||
if verifyErr != nil {
|
||||
klog.Error(verifyErr)
|
||||
s.recorder.Event(s.parentController, event.Warning("VerifyFailed", verifyErr))
|
||||
}
|
||||
}()
|
||||
|
||||
currentReplicas, verifyErr := s.size(ctx)
|
||||
if verifyErr != nil {
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
// record the size and we will use this value to drive the rest of the batches
|
||||
klog.InfoS("record the target size", "total replicas", currentReplicas)
|
||||
s.rolloutStatus.RolloutTargetSize = currentReplicas
|
||||
s.rolloutStatus.RolloutOriginalSize = currentReplicas
|
||||
|
||||
// make sure that the updateRevision is different from what we have already done
|
||||
targetHash, verifyErr := utils.ComputeSpecHash(s.statefulSet.Spec)
|
||||
if verifyErr != nil {
|
||||
// do not fail the rollout because we can't compute the hash value for some reason
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if targetHash == s.rolloutStatus.LastAppliedPodTemplateIdentifier {
|
||||
return false, fmt.Errorf("there is no difference between the source and target, hash = %s", targetHash)
|
||||
}
|
||||
|
||||
if s.statefulSet.Spec.Replicas != nil && currentReplicas != s.statefulSet.Status.Replicas {
|
||||
verifyErr = fmt.Errorf("the StatefulSet is still scaling, target = %d, statefulSet size = %d",
|
||||
currentReplicas, s.statefulSet.Status.Replicas)
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
return false, verifyErr
|
||||
}
|
||||
|
||||
// check if the rollout batch replicas added up to the StatefulSet replicas
|
||||
if verifyErr = s.verifyRolloutBatchReplicaValue(currentReplicas); verifyErr != nil {
|
||||
return false, verifyErr
|
||||
}
|
||||
|
||||
// check if the StatefulSet has any controller
|
||||
if controller := metav1.GetControllerOf(s.statefulSet); controller != nil {
|
||||
return false, fmt.Errorf("the StatefulSet %s has a controller owner %s",
|
||||
s.statefulSet.GetName(), controller.String())
|
||||
}
|
||||
|
||||
// mark the rollout verified
|
||||
s.recorder.Event(s.parentController, event.Normal("Rollout Verified",
|
||||
"Rollout spec and the StatefulSet resource are verified"))
|
||||
// record the new pod template StatefulSet on success
|
||||
s.rolloutStatus.NewPodTemplateIdentifier = targetHash
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Initialize makes sure that the source and target StatefulSet is under our control
|
||||
func (s *StatefulSetRolloutController) Initialize(ctx context.Context) (bool, error) {
|
||||
currentReplicas, err := s.size(ctx)
|
||||
if err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if _, err := s.claimStatefulSet(ctx, s.statefulSet); err != nil {
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := s.setPartition(ctx, s.statefulSet, currentReplicas); err != nil {
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// mark the rollout initialized
|
||||
s.recorder.Event(s.parentController, event.Normal("Rollout Initialized", "Rollout resource are initialized"))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RolloutOneBatchPods calculates the number of pods we can upgrade once according to the rollout spec
|
||||
// and then set the partition accordingly
|
||||
func (s *StatefulSetRolloutController) RolloutOneBatchPods(ctx context.Context) (bool, error) {
|
||||
currentReplicas, err := s.size(ctx)
|
||||
if err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
newPodTarget := s.calculateCurrentTarget(currentReplicas)
|
||||
if err := s.setPartition(ctx, s.statefulSet, currentReplicas-newPodTarget); err != nil {
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// record the finished upgrade action
|
||||
klog.InfoS("upgraded one batch", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"target size", newPodTarget)
|
||||
s.recorder.Event(s.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("Finished submiting all upgrade quests for batch %d", s.rolloutStatus.CurrentBatch)))
|
||||
s.rolloutStatus.UpgradedReplicas = newPodTarget
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckOneBatchPods checks to see if the pods are all available according to the rollout plan
|
||||
func (s *StatefulSetRolloutController) CheckOneBatchPods(ctx context.Context) (bool, error) {
|
||||
currentReplicas, err := s.size(ctx)
|
||||
if err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
newPodTarget := s.calculateCurrentTarget(currentReplicas)
|
||||
readyPodCount := int(s.statefulSet.Status.ReadyReplicas)
|
||||
|
||||
if len(s.rolloutSpec.RolloutBatches) <= int(s.rolloutStatus.CurrentBatch) {
|
||||
err := errors.New("somehow, currentBatch number exceeded the rolloutBatches spec")
|
||||
klog.ErrorS(err, "total batch", len(s.rolloutSpec.RolloutBatches), "current batch",
|
||||
s.rolloutStatus.CurrentBatch)
|
||||
return false, err
|
||||
}
|
||||
|
||||
currentBatch := s.rolloutSpec.RolloutBatches[s.rolloutStatus.CurrentBatch]
|
||||
maxUnavail := 0
|
||||
if currentBatch.MaxUnavailable != nil {
|
||||
maxUnavail, _ = intstr.GetValueFromIntOrPercent(currentBatch.MaxUnavailable, int(currentReplicas), true)
|
||||
}
|
||||
klog.InfoS("checking the rolling out progress", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"new pod count target", newPodTarget, "new ready pod count", readyPodCount,
|
||||
"max unavailable pod allowed", maxUnavail)
|
||||
s.rolloutStatus.UpgradedReadyReplicas = int32(readyPodCount)
|
||||
|
||||
if maxUnavail+readyPodCount >= int(newPodTarget) {
|
||||
// record the successful upgrade
|
||||
klog.InfoS("all pods in current batch are ready", "current batch", s.rolloutStatus.CurrentBatch)
|
||||
s.recorder.Event(s.parentController, event.Normal("Batch Available",
|
||||
fmt.Sprintf("Batch %d is available", s.rolloutStatus.CurrentBatch)))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// continue to verify
|
||||
klog.InfoS("the batch is not ready yet", "current batch", s.rolloutStatus.CurrentBatch)
|
||||
s.rolloutStatus.RolloutRetry("the batch is not ready yet")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// FinalizeOneBatch makes sure that the rollout status are updated correctly
|
||||
func (s *StatefulSetRolloutController) FinalizeOneBatch(ctx context.Context) (bool, error) {
|
||||
status := s.rolloutStatus
|
||||
spec := s.rolloutSpec
|
||||
|
||||
if spec.BatchPartition != nil && *spec.BatchPartition < status.CurrentBatch {
|
||||
err := fmt.Errorf("the current batch value in the status is greater than the batch partition")
|
||||
klog.ErrorS(err, "we have moved past the user defined partition", "user specified batch partition",
|
||||
*spec.BatchPartition, "current batch we are working on", status.CurrentBatch)
|
||||
return false, err
|
||||
}
|
||||
|
||||
upgradedReplicas := int(status.UpgradedReplicas)
|
||||
currentBatch := int(status.CurrentBatch)
|
||||
// calculate the lower bound of the possible pod count just before the current batch
|
||||
podCount := calculateNewBatchTarget(s.rolloutSpec, 0, int(s.rolloutStatus.RolloutTargetSize), currentBatch-1)
|
||||
// the recorded number should be at least as much as the all the pods before the current batch
|
||||
if podCount > upgradedReplicas {
|
||||
err := fmt.Errorf("the upgraded replica in the status is less than all the pods in the previous batch")
|
||||
klog.ErrorS(err, "rollout status inconsistent", "upgraded num status", upgradedReplicas,
|
||||
"pods in all the previous batches", podCount)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// calculate the upper bound with the current batch
|
||||
podCount = calculateNewBatchTarget(s.rolloutSpec, 0, int(s.rolloutStatus.RolloutTargetSize), currentBatch)
|
||||
// the recorded number should be not as much as the all the pods including the active batch
|
||||
if podCount < upgradedReplicas {
|
||||
err := fmt.Errorf("the upgraded replica in the status is greater than all the pods in the current batch")
|
||||
klog.ErrorS(err, "rollout status inconsistent", "total target size", s.rolloutStatus.RolloutTargetSize,
|
||||
"upgraded num status", upgradedReplicas, "pods in the batches including the current batch", podCount)
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Finalize makes sure the StatefulSet is all upgraded
|
||||
func (s *StatefulSetRolloutController) Finalize(ctx context.Context, succeed bool) bool {
|
||||
if err := s.fetchStatefulSet(ctx); err != nil {
|
||||
// don't fail the rollout just because of we can't get the resource
|
||||
return false
|
||||
}
|
||||
|
||||
// release StatefulSet
|
||||
if _, err := s.releaseStatefulSet(ctx, s.statefulSet); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// mark the resource finalized
|
||||
s.rolloutStatus.LastAppliedPodTemplateIdentifier = s.rolloutStatus.NewPodTemplateIdentifier
|
||||
s.recorder.Event(s.parentController, event.Normal("Rollout Finalized",
|
||||
fmt.Sprintf("Rollout resource are finalized, succeed := %t", succeed)))
|
||||
return true
|
||||
}
|
||||
|
||||
// check if the replicas in all the rollout batches add up to the right number
|
||||
func (s *StatefulSetRolloutController) verifyRolloutBatchReplicaValue(totalReplicas int32) error {
|
||||
return verifyBatchesWithRollout(s.rolloutSpec, totalReplicas)
|
||||
}
|
||||
|
||||
// the target StatefulSet size for the current batch
|
||||
func (s *StatefulSetRolloutController) calculateCurrentTarget(totalSize int32) int32 {
|
||||
targetSize := int32(calculateNewBatchTarget(s.rolloutSpec, 0, int(totalSize), int(s.rolloutStatus.CurrentBatch)))
|
||||
klog.InfoS("Calculated the number of pods in the target StatefulSet after current batch",
|
||||
"current batch", s.rolloutStatus.CurrentBatch, "target StatefulSet size", targetSize)
|
||||
return targetSize
|
||||
}
|
||||
|
||||
func (s *StatefulSetRolloutController) fetchStatefulSet(ctx context.Context) error {
|
||||
workload := appsv1.StatefulSet{}
|
||||
if err := s.client.Get(ctx, s.targetNamespacedName, &workload); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
s.recorder.Event(s.parentController, event.Warning("Failed to get the StatefulSet", err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
s.statefulSet = &workload
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StatefulSetRolloutController) size(ctx context.Context) (int32, error) {
|
||||
if s.statefulSet == nil {
|
||||
if err := s.fetchStatefulSet(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
// default is 1
|
||||
return getStatefulSetReplicas(s.statefulSet), nil
|
||||
}
|
||||
@@ -1,501 +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 workloads
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("StatefulSet controller", func() {
|
||||
var (
|
||||
c StatefulSetRolloutController
|
||||
ns corev1.Namespace
|
||||
name string
|
||||
namespace string
|
||||
statefulSet apps.StatefulSet
|
||||
namespacedName client.ObjectKey
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
namespace = "rollout-ns"
|
||||
name = "rollout"
|
||||
appRollout := v1alpha1.Rollout{TypeMeta: metav1.TypeMeta{APIVersion: v1alpha1.SchemeGroupVersion.String(), Kind: v1alpha1.RolloutKind}, ObjectMeta: metav1.ObjectMeta{Name: name}}
|
||||
namespacedName = client.ObjectKey{Name: name, Namespace: namespace}
|
||||
c = StatefulSetRolloutController{
|
||||
statefulSetController: statefulSetController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
rolloutStatus: &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState},
|
||||
parentController: &appRollout,
|
||||
recorder: event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout"),
|
||||
},
|
||||
targetNamespacedName: namespacedName,
|
||||
},
|
||||
}
|
||||
|
||||
statefulSet = apps.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: apps.SchemeGroupVersion.String(), Kind: "StatefulSet"},
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
|
||||
Spec: apps.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"env": "staging"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"env": "staging"}},
|
||||
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: name, Image: "nginx"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ns = corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
By("Create a namespace")
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
By("clean up")
|
||||
k8sClient.Delete(ctx, &statefulSet)
|
||||
})
|
||||
|
||||
Context("TestNewStatefulSetRolloutController", func() {
|
||||
It("init a StatefulSet Rollout Controller", func() {
|
||||
recorder := event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout")
|
||||
parentController := &v1alpha1.Rollout{ObjectMeta: metav1.ObjectMeta{Name: name}}
|
||||
rolloutSpec := &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
rolloutStatus := &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState}
|
||||
workloadNamespacedName := client.ObjectKey{Name: name, Namespace: namespace}
|
||||
got := NewStatefulSetRolloutController(k8sClient, recorder, parentController, rolloutSpec, rolloutStatus, workloadNamespacedName)
|
||||
c := &StatefulSetRolloutController{
|
||||
statefulSetController: statefulSetController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: workloadNamespacedName,
|
||||
},
|
||||
}
|
||||
Expect(got).Should(Equal(c))
|
||||
})
|
||||
})
|
||||
|
||||
Context("VerifySpec", func() {
|
||||
It("could not fetch StatefulSet workload", func() {
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("verify rollout spec hash", func() {
|
||||
By("Create a StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("Verify should fail because the the target hash didn't change")
|
||||
targetHash := statefulSet.Status.UpdateRevision
|
||||
c.rolloutStatus.LastAppliedPodTemplateIdentifier = targetHash
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err).ShouldNot(Equal(fmt.Errorf("there is no difference between the source and target, hash = ")))
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("the StatefulSet need to be stable", func() {
|
||||
By("create the StatefulSet with many pods")
|
||||
statefulSet.Spec.Replicas = pointer.Int32(50)
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("setting a dummy pod identifier so it's different")
|
||||
c.rolloutStatus.LastAppliedPodTemplateIdentifier = "abc"
|
||||
|
||||
By("verify should fail because the StatefulSet is not stable")
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("is still scaling"))
|
||||
Expect(c.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(50))
|
||||
Expect(c.rolloutStatus.NewPodTemplateIdentifier).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("the StatefulSet should not have controller", func() {
|
||||
By("Create a StatefulSet")
|
||||
statefulSet.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("setting a dummy pod identifier so it's different")
|
||||
c.rolloutStatus.LastAppliedPodTemplateIdentifier = "abc"
|
||||
|
||||
statefulSet.Status.Replicas = *statefulSet.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("verify should fail because the StatefulSet still has a controller")
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("has a controller owner"))
|
||||
})
|
||||
|
||||
It("spec is valid", func() {
|
||||
By("Create a StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("setting a dummy pod identifier so it's different")
|
||||
c.rolloutStatus.LastAppliedPodTemplateIdentifier = "abc"
|
||||
|
||||
statefulSet.Status.Replicas = *statefulSet.Spec.Replicas
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("verify should succeed")
|
||||
consistent, err := c.VerifySpec(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeTrue())
|
||||
Expect(c.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(*statefulSet.Spec.Replicas))
|
||||
// NewPodTemplateIdenifier should be fill with computed hash
|
||||
Expect(c.rolloutStatus.NewPodTemplateIdentifier).ShouldNot(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestInitialize", func() {
|
||||
It("could not fetch StatefulSet workload", func() {
|
||||
consistent, err := c.Initialize(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("failed to patch the owner of StatefulSet", func() {
|
||||
By("Create a StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("initialize will fail because StatefulSet has wrong owner reference")
|
||||
initialized, err := c.Initialize(ctx)
|
||||
Expect(initialized).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("workload StatefulSet is controlled by appRollout already", func() {
|
||||
By("Create a StatefulSet")
|
||||
statefulSet.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("initialize succeed without patching")
|
||||
initialized, err := c.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(k8sClient.Get(ctx, c.targetNamespacedName, &statefulSet)).Should(Succeed())
|
||||
Expect(len(statefulSet.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
})
|
||||
|
||||
It("successfully initialized StatefulSet", func() {
|
||||
By("create StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("initialize succeeds")
|
||||
c.parentController.SetUID("1231586900")
|
||||
initialized, err := c.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(k8sClient.Get(ctx, c.targetNamespacedName, &statefulSet)).Should(Succeed())
|
||||
Expect(len(statefulSet.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestRolloutOneBatchPods", func() {
|
||||
It("could not fetch StatefulSet workload", func() {
|
||||
consistent, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("successfully rollout, current batch number is not equal to the expected one", func() {
|
||||
By("Create a StatefulSet")
|
||||
statefulSet.Spec.Replicas = pointer.Int32(10)
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("rollout the second batch of current StatefulSet")
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
c.rolloutSpec.RolloutBatches = []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
}
|
||||
done, err := c.RolloutOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(c.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(3))
|
||||
Expect(k8sClient.Get(ctx, c.targetNamespacedName, &statefulSet)).Should(Succeed())
|
||||
Expect(*statefulSet.Spec.UpdateStrategy.RollingUpdate.Partition).Should(BeEquivalentTo(7))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestCheckOneBatchPods", func() {
|
||||
BeforeEach(func() {
|
||||
statefulSet.Spec.Replicas = pointer.Int32(10)
|
||||
c.rolloutSpec.RolloutBatches = []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("could not fetch StatefulSet workload", func() {
|
||||
consistent, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(consistent).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("current ready Pod is less than expected", func() {
|
||||
By("Create the StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
By("Update the StatefulSet status")
|
||||
statefulSet.Status.Replicas = 4
|
||||
statefulSet.Status.ReadyReplicas = 3
|
||||
statefulSet.Status.UpdatedReplicas = 4
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("checking should fail as not enough pod ready")
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
done, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(c.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(statefulSet.Status.ReadyReplicas))
|
||||
})
|
||||
|
||||
It("failed to check batch Pod when current batch number exceeds the expected ones", func() {
|
||||
By("Create a StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("checking")
|
||||
c.rolloutStatus.CurrentBatch = 3
|
||||
done, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("currentBatch number exceeded the rolloutBatches spec"))
|
||||
})
|
||||
|
||||
It("there are enough pods counting the unavailable", func() {
|
||||
By("Create the StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
By("Update the StatefulSet status")
|
||||
statefulSet.Status.Replicas = 4
|
||||
statefulSet.Status.ReadyReplicas = 3
|
||||
statefulSet.Status.UpdatedReplicas = 4
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
// set the rollout batch spec allow unavailable
|
||||
perc := intstr.FromString("20%")
|
||||
c.rolloutSpec.RolloutBatches = []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: perc,
|
||||
MaxUnavailable: &perc,
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
}
|
||||
By("checking one batch")
|
||||
done, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(c.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(statefulSet.Status.ReadyReplicas))
|
||||
})
|
||||
|
||||
It("there are enough pods ready", func() {
|
||||
By("Create the StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
By("Update the StatefulSet status")
|
||||
statefulSet.Status.Replicas = 10
|
||||
statefulSet.Status.ReadyReplicas = 10
|
||||
statefulSet.Status.UpdatedReplicas = 10
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("the second batch should pass when there are more pods upgraded already")
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
done, err := c.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(c.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(statefulSet.Status.ReadyReplicas))
|
||||
|
||||
By("checking the last batch")
|
||||
c.rolloutStatus.CurrentBatch = 2
|
||||
done, err = c.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(c.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(statefulSet.Status.ReadyReplicas))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalizeOneBatch", func() {
|
||||
BeforeEach(func() {
|
||||
c.rolloutStatus.RolloutTargetSize = 10
|
||||
c.rolloutSpec.RolloutBatches = []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(2),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
It("test illegal batch partition", func() {
|
||||
By("finalizing one batch")
|
||||
c.rolloutSpec.BatchPartition = pointer.Int32(2)
|
||||
c.rolloutStatus.CurrentBatch = 3
|
||||
done, err := c.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("the current batch value in the status is greater than the batch partition"))
|
||||
})
|
||||
|
||||
It("test too few upgraded", func() {
|
||||
By("finalizing one batch")
|
||||
c.rolloutStatus.UpgradedReplicas = 2
|
||||
c.rolloutStatus.CurrentBatch = 2
|
||||
done, err := c.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("is less than all the pods in the previous batch"))
|
||||
})
|
||||
|
||||
It("test too many upgraded", func() {
|
||||
By("finalizing one batch")
|
||||
c.rolloutStatus.UpgradedReplicas = 5
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
done, err := c.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("is greater than all the pods in the current batch"))
|
||||
})
|
||||
|
||||
It("test upgraded in the range", func() {
|
||||
By("finalizing one batch")
|
||||
c.rolloutStatus.UpgradedReplicas = 3
|
||||
c.rolloutStatus.CurrentBatch = 1
|
||||
done, err := c.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalize", func() {
|
||||
It("failed to fetch StatefulSet", func() {
|
||||
By("finalizing")
|
||||
finalized := c.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("Already finalize StatefulSet", func() {
|
||||
By("Create a StatefulSet")
|
||||
statefulSet.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: "notRollout",
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("finalizing without patch")
|
||||
finalized := c.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("successfully to finalize StatefulSet", func() {
|
||||
By("Create a StatefulSet")
|
||||
statefulSet.SetOwnerReferences([]metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
},
|
||||
{
|
||||
APIVersion: corev1.SchemeGroupVersion.String(),
|
||||
Kind: "Deployment",
|
||||
Name: "def",
|
||||
UID: "998877745",
|
||||
},
|
||||
})
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("finalizing with patch")
|
||||
finalized := c.Finalize(ctx, false)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
Expect(k8sClient.Get(ctx, c.targetNamespacedName, &statefulSet)).Should(Succeed())
|
||||
Expect(len(statefulSet.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
Expect(statefulSet.GetOwnerReferences()[0].Kind).Should(Equal("Deployment"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,284 +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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
// StatefulSetScaleController is responsible for handle scale StatefulSet type of workloads
|
||||
type StatefulSetScaleController struct {
|
||||
statefulSetController
|
||||
statefulSet *appsv1.StatefulSet
|
||||
}
|
||||
|
||||
// NewStatefulSetScaleController creates StatefulSet scale controller
|
||||
func NewStatefulSetScaleController(client client.Client, recorder event.Recorder, parentController oam.Object, rolloutSpec *v1alpha1.RolloutPlan, rolloutStatus *v1alpha1.RolloutStatus, workloadName types.NamespacedName) *StatefulSetScaleController {
|
||||
return &StatefulSetScaleController{
|
||||
statefulSetController: statefulSetController{
|
||||
workloadController: workloadController{
|
||||
client: client,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: workloadName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// VerifySpec verifies that the StatefulSet is stable and can be scaled
|
||||
func (s *StatefulSetScaleController) VerifySpec(ctx context.Context) (bool, error) {
|
||||
var verifyErr error
|
||||
defer func() {
|
||||
if verifyErr != nil {
|
||||
klog.Error(verifyErr)
|
||||
s.recorder.Event(s.parentController, event.Warning("VerifyFailed", verifyErr))
|
||||
}
|
||||
}()
|
||||
|
||||
// the rollout has to have a target size in the scale case
|
||||
if s.rolloutSpec.TargetSize == nil {
|
||||
return false, fmt.Errorf("the rollout plan is attempting to scale the StatefulSet %s without a target",
|
||||
s.targetNamespacedName.Name)
|
||||
}
|
||||
s.rolloutStatus.RolloutTargetSize = *s.rolloutSpec.TargetSize
|
||||
klog.InfoS("record the target size", "target size", *s.rolloutSpec.TargetSize)
|
||||
|
||||
// fetch the StatefulSet and get its current size
|
||||
originalSize, verifyErr := s.size(ctx)
|
||||
if verifyErr != nil {
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
s.rolloutStatus.RolloutOriginalSize = originalSize
|
||||
klog.InfoS("record the original size", "original size", originalSize)
|
||||
|
||||
// check if the rollout batch replicas scale up/down to the replicas target
|
||||
if verifyErr = verifyBatchesWithScale(s.rolloutSpec, int(originalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize)); verifyErr != nil {
|
||||
return false, verifyErr
|
||||
}
|
||||
|
||||
// check if the StatefulSet is scaling
|
||||
if s.statefulSet.Status.Replicas != originalSize {
|
||||
verifyErr = fmt.Errorf("the StatefulSet %s is in the middle of scaling, target size = %d, real size = %d",
|
||||
s.statefulSet.GetName(), originalSize, s.statefulSet.Status.Replicas)
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check if the StatefulSet is upgrading
|
||||
if s.statefulSet.Status.UpdatedReplicas != originalSize {
|
||||
verifyErr = fmt.Errorf("the StatefulSet %s is in the middle of updating, target size = %d, updated pod = %d",
|
||||
s.statefulSet.GetName(), originalSize, s.statefulSet.Status.UpdatedReplicas)
|
||||
s.rolloutStatus.RolloutRetry(verifyErr.Error())
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check if the StatefulSet has any controller
|
||||
if controller := metav1.GetControllerOf(s.statefulSet); controller != nil {
|
||||
return false, fmt.Errorf("the statefulSet %s has a controller owner %s",
|
||||
s.statefulSet.GetName(), controller.String())
|
||||
}
|
||||
|
||||
// mark the scale verified
|
||||
s.recorder.Event(s.parentController, event.Normal("Scale Verified",
|
||||
"Rollout spec and the StatefulSet resource are verified"))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Initialize makes sure that the StatefulSet is under our control
|
||||
func (s *StatefulSetScaleController) Initialize(ctx context.Context) (bool, error) {
|
||||
if err := s.fetchStatefulSet(ctx); err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
claimedBefore, err := s.claimStatefulSet(ctx, s.statefulSet)
|
||||
if err != nil {
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
if !claimedBefore {
|
||||
// mark the rollout initialized
|
||||
s.recorder.Event(s.parentController, event.Normal("Scale Initialized", "StatefulSet is initialized"))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RolloutOneBatchPods calculates the number of pods we can scale to according to the rollout spec
|
||||
func (s *StatefulSetScaleController) RolloutOneBatchPods(ctx context.Context) (bool, error) {
|
||||
if err := s.fetchStatefulSet(ctx); err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
// nolint: nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// set the replica according to the batch
|
||||
newPodTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), int(s.rolloutStatus.CurrentBatch))
|
||||
|
||||
if err := s.scaleStatefulSet(ctx, s.statefulSet, int32(newPodTarget)); err != nil {
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// record the scale
|
||||
klog.InfoS("scale one batch", "current batch", s.rolloutStatus.CurrentBatch)
|
||||
s.recorder.Event(s.parentController, event.Normal("Batch Rollout",
|
||||
fmt.Sprintf("Submitted scale quest for batch %d", s.rolloutStatus.CurrentBatch)))
|
||||
s.rolloutStatus.UpgradedReplicas = int32(newPodTarget)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckOneBatchPods checks to see if the pods are scaled according to the rollout plan
|
||||
func (s *StatefulSetScaleController) CheckOneBatchPods(ctx context.Context) (bool, error) {
|
||||
if err := s.fetchStatefulSet(ctx); err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
// nolint:nilerr
|
||||
return false, nil
|
||||
}
|
||||
|
||||
newPodTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), int(s.rolloutStatus.CurrentBatch))
|
||||
readyPodCount := int(s.statefulSet.Status.ReadyReplicas)
|
||||
currentBatch := s.rolloutSpec.RolloutBatches[s.rolloutStatus.CurrentBatch]
|
||||
unavail := 0
|
||||
if currentBatch.MaxUnavailable != nil {
|
||||
unavail, _ = intstr.GetValueFromIntOrPercent(currentBatch.MaxUnavailable,
|
||||
util.Abs(int(s.rolloutStatus.RolloutTargetSize-s.rolloutStatus.RolloutOriginalSize)), true)
|
||||
}
|
||||
klog.InfoS("checking the scaling progress", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"new pod count target", newPodTarget, "new ready pod count", readyPodCount,
|
||||
"max unavailable pod allowed", unavail)
|
||||
s.rolloutStatus.UpgradedReadyReplicas = int32(readyPodCount)
|
||||
isScaleDown := s.rolloutStatus.RolloutTargetSize < s.rolloutStatus.RolloutOriginalSize
|
||||
targetReached := (isScaleDown && readyPodCount <= newPodTarget) || (!isScaleDown && unavail+readyPodCount >= newPodTarget)
|
||||
|
||||
if targetReached {
|
||||
// record the successful upgrade
|
||||
klog.InfoS("the current batch is ready", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"target", newPodTarget, "readyPodCount", readyPodCount, "max unavailable allowed", unavail)
|
||||
s.recorder.Event(s.parentController, event.Normal("Batch Available",
|
||||
fmt.Sprintf("Batch %d is available", s.rolloutStatus.CurrentBatch)))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// continue to verify
|
||||
klog.InfoS("the batch is not ready yet", "current batch", s.rolloutStatus.CurrentBatch,
|
||||
"target", newPodTarget, "readyPodCount", readyPodCount, "max unavailable allowed", unavail)
|
||||
s.rolloutStatus.RolloutRetry("the batch is not ready yet")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// FinalizeOneBatch makes sure that the current batch and replica count in the status are validate
|
||||
func (s *StatefulSetScaleController) FinalizeOneBatch(ctx context.Context) (bool, error) {
|
||||
if s.rolloutSpec.BatchPartition != nil && s.rolloutStatus.CurrentBatch > *s.rolloutSpec.BatchPartition {
|
||||
err := fmt.Errorf("the current batch value in the status is greater than the batch partition")
|
||||
klog.ErrorS(err, "we have moved past the user defined partition", "user specified batch partition",
|
||||
*s.rolloutSpec.BatchPartition, "current batch we are working on", s.rolloutStatus.CurrentBatch)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if s.rolloutStatus.RolloutOriginalSize == s.rolloutStatus.RolloutTargetSize {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
finishedPodCount := int(s.rolloutStatus.UpgradedReplicas)
|
||||
currentBatch := int(s.rolloutStatus.CurrentBatch)
|
||||
|
||||
// calculate the pod target just before the current batch
|
||||
preBatchTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), currentBatch-1)
|
||||
// calculate the pod target with the current batch
|
||||
curBatchTarget := calculateNewBatchTarget(s.rolloutSpec, int(s.rolloutStatus.RolloutOriginalSize),
|
||||
int(s.rolloutStatus.RolloutTargetSize), currentBatch)
|
||||
|
||||
if finishedPodCount < util.Min(preBatchTarget, curBatchTarget) {
|
||||
err := fmt.Errorf("the upgraded replica in the status is less than the lower bound")
|
||||
klog.ErrorS(err, "rollout status inconsistent", "existing pod target", finishedPodCount,
|
||||
"the lower bound", util.Min(preBatchTarget, curBatchTarget))
|
||||
return false, err
|
||||
}
|
||||
|
||||
if finishedPodCount > util.Max(preBatchTarget, curBatchTarget) {
|
||||
err := fmt.Errorf("the upgraded replica in the status is greater than the upper bound")
|
||||
klog.ErrorS(err, "rollout status inconsistent", "existing pod target", finishedPodCount,
|
||||
"the upper bound", util.Max(preBatchTarget, curBatchTarget))
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Finalize makes sure the StatefulSet is scaled and ready to use
|
||||
func (s *StatefulSetScaleController) Finalize(ctx context.Context, succeed bool) bool {
|
||||
if err := s.fetchStatefulSet(ctx); err != nil {
|
||||
s.rolloutStatus.RolloutRetry(err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
releasedBefore, err := s.releaseStatefulSet(ctx, s.statefulSet)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !releasedBefore {
|
||||
// mark the resource finalized
|
||||
s.recorder.Event(s.parentController, event.Normal("Scale Finalized",
|
||||
fmt.Sprintf("Scale resource are finalized, succeed := %t", succeed)))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *StatefulSetScaleController) size(ctx context.Context) (int32, error) {
|
||||
if s.statefulSet == nil {
|
||||
if err := s.fetchStatefulSet(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return getStatefulSetReplicas(s.statefulSet), nil
|
||||
}
|
||||
|
||||
func (s *StatefulSetScaleController) fetchStatefulSet(ctx context.Context) error {
|
||||
workload := appsv1.StatefulSet{}
|
||||
if err := s.client.Get(ctx, s.targetNamespacedName, &workload); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
s.recorder.Event(s.parentController, event.Warning("Failed to get the StatefulSet", err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
s.statefulSet = &workload
|
||||
return nil
|
||||
}
|
||||
@@ -1,499 +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 workloads
|
||||
|
||||
import (
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
var _ = Describe("StatefulSet controller", func() {
|
||||
var (
|
||||
s StatefulSetScaleController
|
||||
ns corev1.Namespace
|
||||
name string
|
||||
namespace string
|
||||
statefulSet appsv1.StatefulSet
|
||||
namespacedName client.ObjectKey
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
namespace = "rollout-ns"
|
||||
name = "rollout1"
|
||||
appRollout := v1alpha1.Rollout{TypeMeta: metav1.TypeMeta{APIVersion: v1alpha1.SchemeGroupVersion.String(), Kind: v1alpha1.RolloutKind}, ObjectMeta: metav1.ObjectMeta{Name: name}}
|
||||
namespacedName = client.ObjectKey{Name: name, Namespace: namespace}
|
||||
|
||||
s = StatefulSetScaleController{
|
||||
statefulSetController: statefulSetController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
rolloutSpec: &v1alpha1.RolloutPlan{
|
||||
TargetSize: pointer.Int32(10),
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("20%"),
|
||||
},
|
||||
{
|
||||
Replicas: intstr.FromString("80%"),
|
||||
},
|
||||
},
|
||||
},
|
||||
rolloutStatus: &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState},
|
||||
parentController: &appRollout,
|
||||
recorder: event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout"),
|
||||
},
|
||||
targetNamespacedName: namespacedName,
|
||||
},
|
||||
}
|
||||
|
||||
statefulSet = appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String(), Kind: "StatefulSet"},
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Replicas: pointer.Int32(1),
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"env": "staging"},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"env": "staging"}},
|
||||
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: name, Image: "nginx"}}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ns = corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
By("Create a namespace")
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(Succeed(), &util.AlreadyExistMatcher{}))
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
By("clean up")
|
||||
k8sClient.Delete(ctx, &statefulSet)
|
||||
})
|
||||
|
||||
Context("TestNewStatefulSetScaleController", func() {
|
||||
It("init a StatefulSet Scale Controller", func() {
|
||||
recorder := event.NewAPIRecorder(mgr.GetEventRecorderFor("AppRollout")).
|
||||
WithAnnotations("controller", "AppRollout")
|
||||
parentController := &v1alpha1.Rollout{ObjectMeta: metav1.ObjectMeta{Name: name}}
|
||||
rolloutSpec := &v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{{
|
||||
Replicas: intstr.FromInt(1),
|
||||
}},
|
||||
}
|
||||
rolloutStatus := &v1alpha1.RolloutStatus{RollingState: v1alpha1.RolloutSucceedState}
|
||||
workloadNamespacedName := client.ObjectKey{Name: name, Namespace: namespace}
|
||||
got := NewStatefulSetScaleController(k8sClient, recorder, parentController, rolloutSpec, rolloutStatus, workloadNamespacedName)
|
||||
controller := &StatefulSetScaleController{
|
||||
statefulSetController: statefulSetController{
|
||||
workloadController: workloadController{
|
||||
client: k8sClient,
|
||||
recorder: recorder,
|
||||
parentController: parentController,
|
||||
rolloutSpec: rolloutSpec,
|
||||
rolloutStatus: rolloutStatus,
|
||||
},
|
||||
targetNamespacedName: workloadNamespacedName,
|
||||
},
|
||||
}
|
||||
Expect(got).Should(Equal(controller))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestVerifySpec", func() {
|
||||
It("rollout need a target size", func() {
|
||||
s.rolloutSpec.TargetSize = nil
|
||||
ligit, err := s.VerifySpec(ctx)
|
||||
Expect(ligit).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("without a target"))
|
||||
})
|
||||
|
||||
It("could not fetch StatefulSet workload", func() {
|
||||
ligit, err := s.VerifySpec(ctx)
|
||||
Expect(ligit).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("rollout batch doesn't fit scale target", func() {
|
||||
By("Create a StatefulSet")
|
||||
statefulSet.Spec.Replicas = pointer.Int32(15)
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("Verify should fail as the scale batches don't match")
|
||||
s.rolloutSpec.RolloutBatches[2].Replicas = intstr.FromInt(10)
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
})
|
||||
|
||||
It("the StatefulSet is in the middle of scaling", func() {
|
||||
By("Create a StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("verify should fail because replica does not match")
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("the StatefulSet is in the middle of updating", func() {
|
||||
By("Create a StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
By("Update the StatefulSet status")
|
||||
statefulSet.Status.Replicas = 1
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("verify should fail because replica are not upgraded")
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("spec is valid", func() {
|
||||
By("Create a StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
By("Update the StatefulSet status")
|
||||
statefulSet.Status.Replicas = 1
|
||||
statefulSet.Status.UpdatedReplicas = 1
|
||||
statefulSet.Status.ReadyReplicas = 1
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("verify should pass and record the size")
|
||||
consistent, err := s.VerifySpec(ctx)
|
||||
Expect(consistent).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.RolloutTargetSize).Should(BeEquivalentTo(10))
|
||||
Expect(s.rolloutStatus.RolloutOriginalSize).Should(BeEquivalentTo(1))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestInitialize", func() {
|
||||
It("could not fetch StatefulSet workload", func() {
|
||||
consistent, err := s.Initialize(ctx)
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("failed to patch the owner of StatefulSet", func() {
|
||||
By("Create a StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("initialize will fail because StatefulSet has wrong owner reference")
|
||||
initialized, err := s.Initialize(ctx)
|
||||
Expect(initialized).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("workload StatefulSet is controlled by appRollout already", func() {
|
||||
By("Create a StatefulSet")
|
||||
statefulSet.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("initialize succeed without patching")
|
||||
initialized, err := s.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("successfully initialized StatefulSet", func() {
|
||||
By("Create a StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("initialize succeeds")
|
||||
s.parentController.SetUID("1231586900")
|
||||
initialized, err := s.Initialize(ctx)
|
||||
Expect(initialized).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestRolloutOneBatchPods", func() {
|
||||
It("could not fetch StatefulSet workload", func() {
|
||||
consistent, err := s.RolloutOneBatchPods(ctx)
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("successfully rollout, current batch number is not equal to the expected one", func() {
|
||||
By("Create a StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("rollout the second batch of current StatefulSet")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 0
|
||||
s.rolloutStatus.RolloutTargetSize = 10
|
||||
done, err := s.RolloutOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReplicas).Should(BeEquivalentTo(3))
|
||||
Expect(k8sClient.Get(ctx, s.targetNamespacedName, &statefulSet)).Should(Succeed())
|
||||
Expect(*statefulSet.Spec.Replicas).Should(BeEquivalentTo(3))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestCheckOneBatchPods", func() {
|
||||
It("could not fetch StatefulSet workload", func() {
|
||||
consistent, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(consistent).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("current ready Pods are less than expected during scale-up", func() {
|
||||
By("Create the StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
By("Update the StatefulSet status")
|
||||
statefulSet.Status.Replicas = 3
|
||||
statefulSet.Status.ReadyReplicas = 3
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("checking should fail as not enough pod ready")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 2
|
||||
s.rolloutStatus.RolloutTargetSize = 10
|
||||
done, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(statefulSet.Status.ReadyReplicas))
|
||||
|
||||
perc := intstr.FromString("20%")
|
||||
s.rolloutSpec.RolloutBatches[1] = v1alpha1.RolloutBatch{
|
||||
Replicas: perc,
|
||||
MaxUnavailable: &perc,
|
||||
}
|
||||
By("checking one batch should succeed with unavailble allowed")
|
||||
done, err = s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(statefulSet.Status.ReadyReplicas))
|
||||
})
|
||||
|
||||
It("current ready Pods are more than expected during scale-down", func() {
|
||||
By("Create the StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
By("Update the StatefulSet status")
|
||||
statefulSet.Status.Replicas = 10
|
||||
statefulSet.Status.ReadyReplicas = 10
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("checking should fail as not enough pod ready")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 12
|
||||
s.rolloutStatus.RolloutTargetSize = 5
|
||||
done, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(statefulSet.Status.ReadyReplicas))
|
||||
|
||||
perc := intstr.FromString("20%")
|
||||
s.rolloutSpec.RolloutBatches[1] = v1alpha1.RolloutBatch{
|
||||
Replicas: perc,
|
||||
MaxUnavailable: &perc,
|
||||
}
|
||||
By("checking one batch should still fail even with unavailble allowed")
|
||||
done, err = s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(statefulSet.Status.ReadyReplicas))
|
||||
})
|
||||
|
||||
It("there are more pods increased during scale-up", func() {
|
||||
By("Create the StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
By("Update the StatefulSet status")
|
||||
statefulSet.Status.Replicas = 9
|
||||
statefulSet.Status.ReadyReplicas = 9
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("checking should pass when ready Pods are more than expected during scale-up")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 2
|
||||
s.rolloutStatus.RolloutTargetSize = 10
|
||||
done, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(statefulSet.Status.ReadyReplicas))
|
||||
})
|
||||
|
||||
It("there are more pods decreased during scale-down", func() {
|
||||
By("Create the StatefulSet")
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
By("Update the StatefulSet status")
|
||||
statefulSet.Status.Replicas = 6
|
||||
statefulSet.Status.ReadyReplicas = 6
|
||||
Expect(k8sClient.Status().Update(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("checking should pass when ready Pods are less than expected during scale-down")
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 12
|
||||
s.rolloutStatus.RolloutTargetSize = 5
|
||||
done, err := s.CheckOneBatchPods(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(s.rolloutStatus.UpgradedReadyReplicas).Should(BeEquivalentTo(statefulSet.Status.ReadyReplicas))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalizeOneBatch", func() {
|
||||
BeforeEach(func() {
|
||||
s.rolloutSpec.RolloutBatches[0] = v1alpha1.RolloutBatch{
|
||||
Replicas: intstr.FromInt(2),
|
||||
}
|
||||
})
|
||||
|
||||
It("test illegal batch partition", func() {
|
||||
By("finalizing one batch")
|
||||
s.rolloutSpec.BatchPartition = pointer.Int32(2)
|
||||
s.rolloutStatus.CurrentBatch = 3
|
||||
done, err := s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("the current batch value in the status is greater than the batch partition"))
|
||||
})
|
||||
|
||||
It("test finalize during scale-up", func() {
|
||||
By("finalizing one batch when there're too few upgraded Pods")
|
||||
s.rolloutStatus.UpgradedReplicas = 6
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 5
|
||||
s.rolloutStatus.RolloutTargetSize = 12
|
||||
done, err := s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("upgraded replica in the status is less than the lower bound"))
|
||||
|
||||
By("finalizing one batch with enough upgraded Pods (lower bound)")
|
||||
s.rolloutStatus.UpgradedReplicas = 7
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch with enough upgraded Pods (upper bound)")
|
||||
s.rolloutStatus.UpgradedReplicas = 9
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch when there're too many upgraded Pods")
|
||||
s.rolloutStatus.UpgradedReplicas = 10
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("upgraded replica in the status is greater than the upper bound"))
|
||||
})
|
||||
|
||||
It("test finalize during scale-down", func() {
|
||||
By("finalizing one batch when there're too many upgraded Pods")
|
||||
s.rolloutStatus.UpgradedReplicas = 13
|
||||
s.rolloutStatus.CurrentBatch = 1
|
||||
s.rolloutStatus.RolloutOriginalSize = 14
|
||||
s.rolloutStatus.RolloutTargetSize = 2
|
||||
done, err := s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("upgraded replica in the status is greater than the upper bound"))
|
||||
|
||||
By("finalizing one batch with enough upgraded Pods (upper bound)")
|
||||
s.rolloutStatus.UpgradedReplicas = 12
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch with enough upgraded Pods (lower bound)")
|
||||
s.rolloutStatus.UpgradedReplicas = 9
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeTrue())
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("finalizing one batch when there're too few upgraded Pods")
|
||||
s.rolloutStatus.UpgradedReplicas = 8
|
||||
done, err = s.FinalizeOneBatch(ctx)
|
||||
Expect(done).Should(BeFalse())
|
||||
Expect(err.Error()).Should(ContainSubstring("upgraded replica in the status is less than the lower bound"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("TestFinalize", func() {
|
||||
It("failed to fetch StatefulSet workload", func() {
|
||||
By("finalizing")
|
||||
finalized := s.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeFalse())
|
||||
})
|
||||
|
||||
It("Already finalize StatefulSet", func() {
|
||||
By("Create a StatefulSet")
|
||||
statefulSet.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: "notRollout",
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
}})
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("finalizing without patch")
|
||||
finalized := s.Finalize(ctx, true)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("successfully to finalize StatefulSet", func() {
|
||||
By("Create a StatefulSet")
|
||||
statefulSet.SetOwnerReferences([]metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha1.RolloutKind,
|
||||
Name: "def",
|
||||
UID: "123456",
|
||||
Controller: pointer.Bool(true),
|
||||
},
|
||||
{
|
||||
APIVersion: corev1.SchemeGroupVersion.String(),
|
||||
Kind: "StatefulSet",
|
||||
Name: "def",
|
||||
UID: "654321",
|
||||
},
|
||||
})
|
||||
Expect(k8sClient.Create(ctx, &statefulSet)).Should(Succeed())
|
||||
|
||||
By("finalizing with patch")
|
||||
finalized := s.Finalize(ctx, false)
|
||||
Expect(finalized).Should(BeTrue())
|
||||
Expect(k8sClient.Get(ctx, s.targetNamespacedName, &statefulSet)).Should(Succeed())
|
||||
Expect(len(statefulSet.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
Expect(statefulSet.GetOwnerReferences()[0].Kind).Should(Equal("StatefulSet"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,96 +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 workloads
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
|
||||
oamCore "github.com/oam-dev/kubevela/apis/core.oam.dev"
|
||||
)
|
||||
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
var ctx = context.Background()
|
||||
var mgr manager.Manager
|
||||
|
||||
func TestRollout(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Rollout Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
By("Bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{
|
||||
filepath.Join("../../../../..", "charts/vela-core/crds"),
|
||||
"testdata",
|
||||
},
|
||||
ErrorIfCRDPathMissing: true,
|
||||
UseExistingCluster: pointer.Bool(false),
|
||||
ControlPlaneStartTimeout: time.Minute,
|
||||
ControlPlaneStopTimeout: time.Minute,
|
||||
}
|
||||
var err error
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(cfg).ToNot(BeNil())
|
||||
|
||||
err = oamCore.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Expect(kruise.AddToScheme(scheme.Scheme)).NotTo(HaveOccurred())
|
||||
|
||||
By("Create the k8s client")
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(k8sClient).ToNot(BeNil())
|
||||
|
||||
By("Starting the controller in the background")
|
||||
mgr, err = ctrl.NewManager(cfg, ctrl.Options{
|
||||
Scheme: scheme.Scheme,
|
||||
MetricsBindAddress: "0",
|
||||
Port: 48081,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
go func() {
|
||||
defer GinkgoRecover()
|
||||
}()
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("Tearing down the test environment")
|
||||
err := testEnv.Stop()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
-366
@@ -1,366 +0,0 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.7.0
|
||||
creationTimestamp: null
|
||||
name: clonesets.apps.kruise.io
|
||||
spec:
|
||||
group: apps.kruise.io
|
||||
names:
|
||||
kind: CloneSet
|
||||
listKind: CloneSetList
|
||||
plural: clonesets
|
||||
shortNames:
|
||||
- clone
|
||||
singular: cloneset
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- description: The desired number of pods.
|
||||
jsonPath: .spec.replicas
|
||||
name: DESIRED
|
||||
type: integer
|
||||
- description: The number of pods updated.
|
||||
jsonPath: .status.updatedReplicas
|
||||
name: UPDATED
|
||||
type: integer
|
||||
- description: The number of pods updated and ready.
|
||||
jsonPath: .status.updatedReadyReplicas
|
||||
name: UPDATED_READY
|
||||
type: integer
|
||||
- description: The number of pods ready.
|
||||
jsonPath: .status.readyReplicas
|
||||
name: READY
|
||||
type: integer
|
||||
- description: The number of currently all pods.
|
||||
jsonPath: .status.replicas
|
||||
name: TOTAL
|
||||
type: integer
|
||||
- description: CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.
|
||||
jsonPath: .metadata.creationTimestamp
|
||||
name: AGE
|
||||
type: date
|
||||
- description: The containers of currently cloneset.
|
||||
jsonPath: .spec.template.spec.containers[*].name
|
||||
name: CONTAINERS
|
||||
priority: 1
|
||||
type: string
|
||||
- description: The images of currently cloneset.
|
||||
jsonPath: .spec.template.spec.containers[*].image
|
||||
name: IMAGES
|
||||
priority: 1
|
||||
type: string
|
||||
- description: The selector of currently cloneset.
|
||||
jsonPath: .status.labelSelector
|
||||
name: SELECTOR
|
||||
priority: 1
|
||||
type: string
|
||||
name: v1alpha1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: CloneSet is the Schema for the clonesets API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: CloneSetSpec defines the desired state of CloneSet
|
||||
properties:
|
||||
lifecycle:
|
||||
description: Lifecycle defines the lifecycle hooks for Pods pre-delete, in-place update.
|
||||
properties:
|
||||
inPlaceUpdate:
|
||||
description: InPlaceUpdate is the hook before Pod to update and after Pod has been updated.
|
||||
properties:
|
||||
finalizersHandler:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
labelsHandler:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
preDelete:
|
||||
description: PreDelete is the hook before Pod to be deleted.
|
||||
properties:
|
||||
finalizersHandler:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
labelsHandler:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
minReadySeconds:
|
||||
description: Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, for it to be considered available. Defaults to 0 (pod will be considered available as soon as it is ready)
|
||||
format: int32
|
||||
type: integer
|
||||
replicas:
|
||||
description: Replicas is the desired number of replicas of the given Template. These are replicas in the sense that they are instantiations of the same Template. If unspecified, defaults to 1.
|
||||
format: int32
|
||||
type: integer
|
||||
revisionHistoryLimit:
|
||||
description: RevisionHistoryLimit is the maximum number of revisions that will be maintained in the CloneSet's revision history. The revision history consists of all revisions not represented by a currently applied CloneSetSpec version. The default value is 10.
|
||||
format: int32
|
||||
type: integer
|
||||
scaleStrategy:
|
||||
description: ScaleStrategy indicates the ScaleStrategy that will be employed to create and delete Pods in the CloneSet.
|
||||
properties:
|
||||
maxUnavailable:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
description: The maximum number of pods that can be unavailable for scaled pods. This field can control the changes rate of replicas for CloneSet so as to minimize the impact for users' service. The scale will fail if the number of unavailable pods were greater than this MaxUnavailable at scaling up. MaxUnavailable works only when scaling up.
|
||||
x-kubernetes-int-or-string: true
|
||||
podsToDelete:
|
||||
description: PodsToDelete is the names of Pod should be deleted. Note that this list will be truncated for non-existing pod names.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
selector:
|
||||
description: 'Selector is a label query over pods that should match the replica count. It must match the pod template''s labels. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors'
|
||||
properties:
|
||||
matchExpressions:
|
||||
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
|
||||
items:
|
||||
description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
|
||||
properties:
|
||||
key:
|
||||
description: key is the label key that the selector applies to.
|
||||
type: string
|
||||
operator:
|
||||
description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
|
||||
type: string
|
||||
values:
|
||||
description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
type: object
|
||||
type: array
|
||||
matchLabels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
|
||||
type: object
|
||||
type: object
|
||||
template:
|
||||
description: Template describes the pods that will be created.
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
updateStrategy:
|
||||
description: UpdateStrategy indicates the UpdateStrategy that will be employed to update Pods in the CloneSet when a revision is made to Template.
|
||||
properties:
|
||||
inPlaceUpdateStrategy:
|
||||
description: InPlaceUpdateStrategy contains strategies for in-place update.
|
||||
properties:
|
||||
gracePeriodSeconds:
|
||||
description: GracePeriodSeconds is the timespan between set Pod status to not-ready and update images in Pod spec when in-place update a Pod.
|
||||
format: int32
|
||||
type: integer
|
||||
type: object
|
||||
maxSurge:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
description: 'The maximum number of pods that can be scheduled above the desired replicas during update or specified delete. Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). Absolute number is calculated from percentage by rounding up. Defaults to 0.'
|
||||
x-kubernetes-int-or-string: true
|
||||
maxUnavailable:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
description: 'The maximum number of pods that can be unavailable during update or scale. Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). Absolute number is calculated from percentage by rounding up by default. When maxSurge > 0, absolute number is calculated from percentage by rounding down. Defaults to 20%.'
|
||||
x-kubernetes-int-or-string: true
|
||||
partition:
|
||||
anyOf:
|
||||
- type: integer
|
||||
- type: string
|
||||
description: 'Partition is the desired number of pods in old revisions. Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). Absolute number is calculated from percentage by rounding up by default. It means when partition is set during pods updating, (replicas - partition value) number of pods will be updated. Default value is 0.'
|
||||
x-kubernetes-int-or-string: true
|
||||
paused:
|
||||
description: Paused indicates that the CloneSet is paused. Default value is false
|
||||
type: boolean
|
||||
priorityStrategy:
|
||||
description: Priorities are the rules for calculating the priority of updating pods. Each pod to be updated, will pass through these terms and get a sum of weights.
|
||||
properties:
|
||||
orderPriority:
|
||||
description: 'Order priority terms, pods will be sorted by the value of orderedKey. For example: ``` orderPriority: - orderedKey: key1 - orderedKey: key2 ``` First, all pods which have key1 in labels will be sorted by the value of key1. Then, the left pods which have no key1 but have key2 in labels will be sorted by the value of key2 and put behind those pods have key1.'
|
||||
items:
|
||||
description: UpdatePriorityOrder defines order priority.
|
||||
properties:
|
||||
orderedKey:
|
||||
description: Calculate priority by value of this key. Values of this key, will be sorted by GetInt(val). GetInt method will find the last int in value, such as getting 5 in value '5', getting 10 in value 'sts-10'.
|
||||
type: string
|
||||
required:
|
||||
- orderedKey
|
||||
type: object
|
||||
type: array
|
||||
weightPriority:
|
||||
description: Weight priority terms, pods will be sorted by the sum of all terms weight.
|
||||
items:
|
||||
description: UpdatePriorityWeightTerm defines weight priority.
|
||||
properties:
|
||||
matchSelector:
|
||||
description: MatchSelector is used to select by pod's labels.
|
||||
properties:
|
||||
matchExpressions:
|
||||
description: matchExpressions is a list of label selector requirements. The requirements are ANDed.
|
||||
items:
|
||||
description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.
|
||||
properties:
|
||||
key:
|
||||
description: key is the label key that the selector applies to.
|
||||
type: string
|
||||
operator:
|
||||
description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.
|
||||
type: string
|
||||
values:
|
||||
description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- key
|
||||
- operator
|
||||
type: object
|
||||
type: array
|
||||
matchLabels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed.
|
||||
type: object
|
||||
type: object
|
||||
weight:
|
||||
description: Weight associated with matching the corresponding matchExpressions, in the range 1-100.
|
||||
format: int32
|
||||
type: integer
|
||||
required:
|
||||
- matchSelector
|
||||
- weight
|
||||
type: object
|
||||
type: array
|
||||
type: object
|
||||
scatterStrategy:
|
||||
description: ScatterStrategy defines the scatter rules to make pods been scattered when update. This will avoid pods with the same key-value to be updated in one batch. - Note that pods will be scattered after priority sort. So, although priority strategy and scatter strategy can be applied together, we suggest to use either one of them. - If scatterStrategy is used, we suggest to just use one term. Otherwise, the update order can be hard to understand.
|
||||
items:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
- value
|
||||
type: object
|
||||
type: array
|
||||
type:
|
||||
description: Type indicates the type of the CloneSetUpdateStrategy. Default is ReCreate.
|
||||
type: string
|
||||
type: object
|
||||
volumeClaimTemplates:
|
||||
description: VolumeClaimTemplates is a list of claims that pods are allowed to reference. Note that PVC will be deleted when its pod has been deleted.
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
required:
|
||||
- selector
|
||||
- template
|
||||
type: object
|
||||
status:
|
||||
description: CloneSetStatus defines the observed state of CloneSet
|
||||
properties:
|
||||
availableReplicas:
|
||||
description: AvailableReplicas is the number of Pods created by the CloneSet controller that have a Ready Condition for at least minReadySeconds.
|
||||
format: int32
|
||||
type: integer
|
||||
collisionCount:
|
||||
description: CollisionCount is the count of hash collisions for the CloneSet. The CloneSet controller uses this field as a collision avoidance mechanism when it needs to create the name for the newest ControllerRevision.
|
||||
format: int32
|
||||
type: integer
|
||||
conditions:
|
||||
description: Conditions represents the latest available observations of a CloneSet's current state.
|
||||
items:
|
||||
description: CloneSetCondition describes the state of a CloneSet at a certain point.
|
||||
properties:
|
||||
lastTransitionTime:
|
||||
description: Last time the condition transitioned from one status to another.
|
||||
format: date-time
|
||||
type: string
|
||||
message:
|
||||
description: A human readable message indicating details about the transition.
|
||||
type: string
|
||||
reason:
|
||||
description: The reason for the condition's last transition.
|
||||
type: string
|
||||
status:
|
||||
description: Status of the condition, one of True, False, Unknown.
|
||||
type: string
|
||||
type:
|
||||
description: Type of CloneSet condition.
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- type
|
||||
type: object
|
||||
type: array
|
||||
currentRevision:
|
||||
description: currentRevision, if not empty, indicates the current revision version of the CloneSet.
|
||||
type: string
|
||||
labelSelector:
|
||||
description: LabelSelector is label selectors for query over pods that should match the replica count used by HPA.
|
||||
type: string
|
||||
observedGeneration:
|
||||
description: ObservedGeneration is the most recent generation observed for this CloneSet. It corresponds to the CloneSet's generation, which is updated on mutation by the API Server.
|
||||
format: int64
|
||||
type: integer
|
||||
readyReplicas:
|
||||
description: ReadyReplicas is the number of Pods created by the CloneSet controller that have a Ready Condition.
|
||||
format: int32
|
||||
type: integer
|
||||
replicas:
|
||||
description: Replicas is the number of Pods created by the CloneSet controller.
|
||||
format: int32
|
||||
type: integer
|
||||
updateRevision:
|
||||
description: UpdateRevision, if not empty, indicates the latest revision of the CloneSet.
|
||||
type: string
|
||||
updatedReadyReplicas:
|
||||
description: UpdatedReadyReplicas is the number of Pods created by the CloneSet controller from the CloneSet version indicated by updateRevision and have a Ready Condition.
|
||||
format: int32
|
||||
type: integer
|
||||
updatedReplicas:
|
||||
description: UpdatedReplicas is the number of Pods created by the CloneSet controller from the CloneSet version indicated by updateRevision.
|
||||
format: int32
|
||||
type: integer
|
||||
required:
|
||||
- availableReplicas
|
||||
- readyReplicas
|
||||
- replicas
|
||||
- updatedReadyReplicas
|
||||
- updatedReplicas
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
scale:
|
||||
labelSelectorPath: .status.labelSelector
|
||||
specReplicasPath: .spec.replicas
|
||||
statusReplicasPath: .status.replicas
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
@@ -1,32 +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 common
|
||||
|
||||
const (
|
||||
// RollingComponentsSep is the separator that divide the names in the newComponent annotation
|
||||
RollingComponentsSep = ","
|
||||
// DisableAllCaps disable all capabilities
|
||||
DisableAllCaps = "all"
|
||||
// DisableNoneCaps disable none of capabilities
|
||||
DisableNoneCaps = ""
|
||||
// HealthScopeControllerName is the controller name of healthScope controller
|
||||
HealthScopeControllerName = "healthscope"
|
||||
// RolloutControllerName is the controller name of rollout controller
|
||||
RolloutControllerName = "rollout"
|
||||
// EnvBindingControllerName is the controller name of envbinding
|
||||
EnvBindingControllerName = "envbinding"
|
||||
)
|
||||
@@ -27,21 +27,6 @@ import (
|
||||
// ApplyOnceOnlyMode enumerates ApplyOnceOnly modes.
|
||||
type ApplyOnceOnlyMode string
|
||||
|
||||
const (
|
||||
// ApplyOnceOnlyOff indicates workloads and traits should always be affected.
|
||||
// It means ApplyOnceOnly is disabled.
|
||||
ApplyOnceOnlyOff ApplyOnceOnlyMode = "off"
|
||||
|
||||
// ApplyOnceOnlyOn indicates workloads and traits should not be affected
|
||||
// if no spec change is made in the ApplicationConfiguration.
|
||||
ApplyOnceOnlyOn ApplyOnceOnlyMode = "on"
|
||||
|
||||
// ApplyOnceOnlyForce is a more strong case for ApplyOnceOnly, the workload
|
||||
// and traits won't be affected if no spec change is made in the ApplicationConfiguration,
|
||||
// even if the workload or trait has been deleted from cluster.
|
||||
ApplyOnceOnlyForce ApplyOnceOnlyMode = "force"
|
||||
)
|
||||
|
||||
// Args args used by controller
|
||||
type Args struct {
|
||||
|
||||
|
||||
@@ -1,239 +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 assemble
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
velatypes "github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
var _ = Describe("Test Assemble Options", func() {
|
||||
It("test assemble", func() {
|
||||
var (
|
||||
compName = "test-comp"
|
||||
namespace = "default"
|
||||
)
|
||||
|
||||
appRev := &v1beta1.ApplicationRevision{}
|
||||
b, err := os.ReadFile("./testdata/apprevision.yaml")
|
||||
/* appRevision test data is generated based on below application
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: test-assemble
|
||||
spec:
|
||||
components:
|
||||
- name: test-comp
|
||||
type: webservice
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
port: 8000
|
||||
traits:
|
||||
- type: ingress
|
||||
properties:
|
||||
domain: localhost
|
||||
http:
|
||||
"/": 8000
|
||||
*/
|
||||
Expect(err).Should(BeNil())
|
||||
err = yaml.Unmarshal(b, appRev)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
ao := NewAppManifests(appRev, appParser)
|
||||
workloads, traits, _, err := ao.GroupAssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("Verify amount of result resources")
|
||||
allResources, err := ao.AssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(allResources)).Should(Equal(3))
|
||||
|
||||
By("Verify amount of result grouped resources")
|
||||
Expect(len(workloads)).Should(Equal(1))
|
||||
Expect(len(traits[compName])).Should(Equal(2))
|
||||
|
||||
By("Verify workload metadata (name, namespace, labels, annotations, ownerRef)")
|
||||
wl := workloads[compName]
|
||||
Expect(wl.GetName()).Should(Equal(compName))
|
||||
Expect(wl.GetNamespace()).Should(Equal(namespace))
|
||||
labels := wl.GetLabels()
|
||||
labelKeys := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
labelKeys = append(labelKeys, k)
|
||||
}
|
||||
Expect(labelKeys).Should(ContainElements(
|
||||
oam.LabelAppName,
|
||||
oam.LabelAppRevision,
|
||||
oam.LabelAppRevisionHash,
|
||||
oam.LabelAppComponent,
|
||||
oam.LabelAppComponentRevision,
|
||||
oam.WorkloadTypeLabel,
|
||||
oam.LabelOAMResourceType))
|
||||
Expect(len(wl.GetAnnotations())).Should(Equal(1))
|
||||
|
||||
By("Verify trait metadata (name, namespace, labels, annotations, ownerRef)")
|
||||
trait := traits[compName][0]
|
||||
Expect(trait.GetName()).Should(ContainSubstring(compName))
|
||||
Expect(trait.GetNamespace()).Should(Equal(namespace))
|
||||
labels = trait.GetLabels()
|
||||
labelKeys = make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
labelKeys = append(labelKeys, k)
|
||||
}
|
||||
Expect(labelKeys).Should(ContainElements(
|
||||
oam.LabelAppName,
|
||||
oam.LabelAppRevision,
|
||||
oam.LabelAppRevisionHash,
|
||||
oam.LabelAppComponent,
|
||||
oam.LabelAppComponentRevision,
|
||||
oam.TraitTypeLabel,
|
||||
oam.LabelOAMResourceType))
|
||||
Expect(len(wl.GetAnnotations())).Should(Equal(1))
|
||||
|
||||
By("Verify referenced scopes")
|
||||
scopes, err := ao.ReferencedScopes()
|
||||
Expect(err).Should(BeNil())
|
||||
wlTypedRef := corev1.ObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: compName,
|
||||
}
|
||||
Expect(len(scopes[wlTypedRef]) > 0).Should(BeTrue())
|
||||
wlScope := scopes[wlTypedRef][0]
|
||||
wantScopeRef := corev1.ObjectReference{
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
Kind: "HealthScope",
|
||||
Name: "sample-health-scope",
|
||||
}
|
||||
Expect(wlScope).Should(Equal(wantScopeRef))
|
||||
})
|
||||
|
||||
It("test annotation and label filter", func() {
|
||||
var (
|
||||
compName = "frontend"
|
||||
workloadName = "test-workload"
|
||||
)
|
||||
appRev := &v1beta1.ApplicationRevision{}
|
||||
b, err := os.ReadFile("./testdata/filter_annotations.yaml")
|
||||
Expect(err).Should(BeNil())
|
||||
err = yaml.Unmarshal(b, appRev)
|
||||
Expect(err).Should(BeNil())
|
||||
getKeys := func(m map[string]string) []string {
|
||||
var keys []string
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
//this appRev is generated on below this app:
|
||||
/*
|
||||
metadata:
|
||||
name: website
|
||||
annotations:
|
||||
filter.oam.dev/annotation-keys: "notPassAnno1, notPassAnno2"
|
||||
filter.oam.dev/label-keys: "notPassLabel"
|
||||
notPassAnno1: "Annotation-filtered"
|
||||
notPassAnno2: "Annotation-filtered"
|
||||
canPassAnno: "Annotation-passed"
|
||||
labels:
|
||||
notPassLabel: "Label-filtered"
|
||||
canPassLabel: "Label-passed"
|
||||
spec:
|
||||
components:
|
||||
- name: frontend
|
||||
type: webservice
|
||||
properties:
|
||||
image: nginx
|
||||
*/
|
||||
|
||||
ao := NewAppManifests(appRev, appParser)
|
||||
workloads, _, _, err := ao.GroupAssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("verify labels specified should be filtered")
|
||||
wl := workloads[compName]
|
||||
labelKeys := getKeys(wl.GetLabels())
|
||||
|
||||
Expect(labelKeys).ShouldNot(ContainElements("notPassLabel"))
|
||||
Expect(labelKeys).Should(ContainElements("canPassLabel"))
|
||||
|
||||
By("verify annotations specified should be filtered")
|
||||
annotationKeys := getKeys(wl.GetAnnotations())
|
||||
Expect(annotationKeys).ShouldNot(ContainElements("notPassAnno1", "notPassAnno2"))
|
||||
Expect(annotationKeys).Should(ContainElements("canPassAnno"))
|
||||
|
||||
By("Verify workload metadata (name)")
|
||||
Expect(wl.GetName()).Should(Equal(workloadName))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Test handleCheckManageWorkloadTrait func", func() {
|
||||
It("Test every situation", func() {
|
||||
traitDefs := map[string]*v1beta1.TraitDefinition{
|
||||
"rollout": {
|
||||
Spec: v1beta1.TraitDefinitionSpec{
|
||||
ManageWorkload: true,
|
||||
},
|
||||
},
|
||||
"normal": {
|
||||
Spec: v1beta1.TraitDefinitionSpec{},
|
||||
},
|
||||
}
|
||||
appRev := v1beta1.ApplicationRevision{
|
||||
Spec: v1beta1.ApplicationRevisionSpec{
|
||||
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
|
||||
TraitDefinitions: traitDefs,
|
||||
},
|
||||
},
|
||||
}
|
||||
rolloutTrait := &unstructured.Unstructured{}
|
||||
rolloutTrait.SetLabels(map[string]string{oam.TraitTypeLabel: "rollout"})
|
||||
|
||||
normalTrait := &unstructured.Unstructured{}
|
||||
normalTrait.SetLabels(map[string]string{oam.TraitTypeLabel: "normal"})
|
||||
|
||||
workload := unstructured.Unstructured{}
|
||||
workload.SetLabels(map[string]string{
|
||||
oam.WorkloadTypeLabel: "webservice",
|
||||
})
|
||||
|
||||
comps := []*velatypes.ComponentManifest{
|
||||
{
|
||||
Traits: []*unstructured.Unstructured{
|
||||
rolloutTrait,
|
||||
normalTrait,
|
||||
},
|
||||
StandardWorkload: &workload,
|
||||
},
|
||||
}
|
||||
|
||||
HandleCheckManageWorkloadTrait(appRev, comps)
|
||||
Expect(len(rolloutTrait.GetLabels())).Should(BeEquivalentTo(2))
|
||||
Expect(rolloutTrait.GetLabels()[oam.LabelManageWorkloadTrait]).Should(BeEquivalentTo("true"))
|
||||
Expect(len(normalTrait.GetLabels())).Should(BeEquivalentTo(1))
|
||||
Expect(normalTrait.GetLabels()[oam.LabelManageWorkloadTrait]).Should(BeEquivalentTo(""))
|
||||
})
|
||||
})
|
||||
-94
File diff suppressed because one or more lines are too long
@@ -1,622 +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 application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/oam/testutil"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("Test application controller clean up ", func() {
|
||||
ctx := context.TODO()
|
||||
var namespace string
|
||||
var ns v1.Namespace
|
||||
|
||||
cd := &v1beta1.ComponentDefinition{}
|
||||
cdDefJson, _ := yaml.YAMLToJSON([]byte(normalCompDefYaml))
|
||||
|
||||
BeforeEach(func() {
|
||||
namespace = randomNamespaceName("clean-up-revision-test")
|
||||
ns = v1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
Expect(json.Unmarshal(cdDefJson, cd)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, cd.DeepCopy())).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
By("[TEST] Clean up resources after an integration test")
|
||||
Expect(k8sClient.Delete(ctx, &ns)).Should(SatisfyAny(BeNil()))
|
||||
})
|
||||
|
||||
It("Test clean up appRevision", func() {
|
||||
appName := "app-1"
|
||||
appKey := types.NamespacedName{Namespace: namespace, Name: appName}
|
||||
app := getApp(appName, namespace, "normal-worker")
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
checkApp := new(v1beta1.Application)
|
||||
for i := 0; i < appRevisionLimit+1; i++ {
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, i)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, ctrl.Request{NamespacedName: appKey})
|
||||
}
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(namespace),
|
||||
client.MatchingLabels{
|
||||
oam.LabelAppName: appName,
|
||||
},
|
||||
}
|
||||
appRevisionList := new(v1beta1.ApplicationRevisionList)
|
||||
Eventually(func() error {
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
|
||||
By("create new appRevision will remove appRevison1")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the oldest revision")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
|
||||
By("update app again will gc appRevision2")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property = fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 7)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: appName + "-v2"}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the revision-2")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test clean up component revision", func() {
|
||||
appName := "app-1"
|
||||
appKey := types.NamespacedName{Namespace: namespace, Name: appName}
|
||||
app := getApp(appName, namespace, "normal-worker")
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
checkApp := new(v1beta1.Application)
|
||||
for i := 0; i < appRevisionLimit+1; i++ {
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, i)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, ctrl.Request{NamespacedName: appKey})
|
||||
}
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(namespace),
|
||||
client.MatchingLabels{
|
||||
oam.LabelControllerRevisionComponent: "comp1",
|
||||
},
|
||||
}
|
||||
crList := new(appsv1.ControllerRevisionList)
|
||||
Eventually(func() error {
|
||||
err := k8sClient.List(ctx, crList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(crList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error comp revision number wants %d, actually %d", appRevisionLimit+1, len(crList.Items))
|
||||
}
|
||||
return nil
|
||||
}, time.Second*10, time.Millisecond*500).Should(BeNil())
|
||||
|
||||
By("create new appRevision will remove revision v1")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: "comp1-v1"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, crList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(crList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error comp revision number wants %d, actually %d", appRevisionLimit+1, len(crList.Items))
|
||||
}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the oldest revision")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*10, time.Millisecond*500).Should(BeNil())
|
||||
|
||||
By("update app again will gc revision v2")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property = fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 7)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: "comp1-v2"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, crList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(crList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error comp revision number wants %d, actually %d", appRevisionLimit+1, len(crList.Items))
|
||||
}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the oldest revision")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*10, time.Millisecond*500).Should(BeNil())
|
||||
|
||||
By("update app with comp as latest revision will not gc revision v3")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property = fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: "comp1-v3"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, crList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(crList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error comp revision number wants %d, actually %d", appRevisionLimit+1, len(crList.Items))
|
||||
}
|
||||
return k8sClient.Get(ctx, revKey, &appsv1.ControllerRevision{})
|
||||
}, time.Second*10, time.Millisecond*500).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test clean up rollout component revision", func() {
|
||||
appName := "app-2"
|
||||
appKey := types.NamespacedName{Namespace: namespace, Name: appName}
|
||||
app := getApp(appName, namespace, "normal-worker")
|
||||
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationAppRollout, "true")
|
||||
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationRollingComponent, "comp1")
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
checkApp := new(v1beta1.Application)
|
||||
for i := 0; i < appRevisionLimit+1; i++ {
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, i)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, ctrl.Request{NamespacedName: appKey})
|
||||
}
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(namespace),
|
||||
client.MatchingLabels{
|
||||
oam.LabelControllerRevisionComponent: "comp1",
|
||||
},
|
||||
}
|
||||
crList := new(appsv1.ControllerRevisionList)
|
||||
Eventually(func() error {
|
||||
err := k8sClient.List(ctx, crList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(crList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error comp revision number wants %d, actually %d", appRevisionLimit+1, len(crList.Items))
|
||||
}
|
||||
return nil
|
||||
}, time.Second*10, time.Millisecond*500).Should(BeNil())
|
||||
|
||||
By("create new appRevision will remove revision v1")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: "comp1-v1"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, crList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(crList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error comp revision number wants %d, actually %d", appRevisionLimit+1, len(crList.Items))
|
||||
}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the oldest revision")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*10, time.Millisecond*500).Should(BeNil())
|
||||
|
||||
By("update app again will gc revision v2")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property = fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 7)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: "comp1-v2"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, crList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(crList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error comp revision number wants %d, actually %d", appRevisionLimit+1, len(crList.Items))
|
||||
}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the oldest revision")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*10, time.Millisecond*500).Should(BeNil())
|
||||
|
||||
By("update app with comp as latest revision will not gc revision v3")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property = fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: "comp1-v3"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, crList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(crList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error comp revision number wants %d, actually %d", appRevisionLimit+1, len(crList.Items))
|
||||
}
|
||||
return k8sClient.Get(ctx, revKey, &appsv1.ControllerRevision{})
|
||||
}, time.Second*10, time.Millisecond*500).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test clean up rollout appRevision", func() {
|
||||
appName := "app-2"
|
||||
appKey := types.NamespacedName{Namespace: namespace, Name: appName}
|
||||
app := getApp(appName, namespace, "normal-worker")
|
||||
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationAppRollout, "true")
|
||||
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationRollingComponent, "comp1")
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
checkApp := new(v1beta1.Application)
|
||||
for i := 0; i < appRevisionLimit+1; i++ {
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, i)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, ctrl.Request{NamespacedName: appKey})
|
||||
}
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(namespace),
|
||||
client.MatchingLabels{
|
||||
oam.LabelAppName: appName,
|
||||
},
|
||||
}
|
||||
appRevisionList := new(v1beta1.ApplicationRevisionList)
|
||||
Eventually(func() error {
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
|
||||
By("create new appRevision will remove appRevison1")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the oldest revision")
|
||||
}
|
||||
if res, err := util.CheckAppRevision(appRevisionList.Items, []int{2, 3, 4, 5, 6, 7}); err != nil || !res {
|
||||
return fmt.Errorf("appRevision collection mismatch")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*10, time.Second*2).Should(BeNil())
|
||||
|
||||
By("update app again will gc appRevision2")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property = fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 7)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: appName + "-v2"}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the revision-2")
|
||||
}
|
||||
if res, err := util.CheckAppRevision(appRevisionList.Items, []int{3, 4, 5, 6, 7, 8}); err != nil || !res {
|
||||
return fmt.Errorf("appRevision collection mismatch")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test clean up rollout appRevision", func() {
|
||||
appName := "app-2"
|
||||
appKey := types.NamespacedName{Namespace: namespace, Name: appName}
|
||||
app := getApp(appName, namespace, "normal-worker")
|
||||
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationAppRollout, "true")
|
||||
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationRollingComponent, "comp1")
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
checkApp := new(v1beta1.Application)
|
||||
for i := 0; i < appRevisionLimit+1; i++ {
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, i)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, ctrl.Request{NamespacedName: appKey})
|
||||
}
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(namespace),
|
||||
client.MatchingLabels{
|
||||
oam.LabelAppName: appName,
|
||||
},
|
||||
}
|
||||
appRevisionList := new(v1beta1.ApplicationRevisionList)
|
||||
Eventually(func() error {
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
|
||||
By("create new appRevision will remove appRevison1")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the oldest revision")
|
||||
}
|
||||
if res, err := util.CheckAppRevision(appRevisionList.Items, []int{2, 3, 4, 5, 6, 7}); err != nil || !res {
|
||||
return fmt.Errorf("appRevision collection mismatch")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*10, time.Second*2).Should(BeNil())
|
||||
|
||||
By("update app again will gc appRevision2")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property = fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 7)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: appName + "-v2"}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the revision-2")
|
||||
}
|
||||
if res, err := util.CheckAppRevision(appRevisionList.Items, []int{3, 4, 5, 6, 7, 8}); err != nil || !res {
|
||||
return fmt.Errorf("appRevision collection mismatch")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test clean up appDeployment using appRevision", func() {
|
||||
appName := "app-4"
|
||||
appKey := types.NamespacedName{Namespace: namespace, Name: appName}
|
||||
app := getApp(appName, namespace, "normal-worker")
|
||||
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationAppRollout, "true")
|
||||
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationRollingComponent, "comp1")
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
checkApp := new(v1beta1.Application)
|
||||
for i := 0; i < appRevisionLimit+1; i++ {
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, i)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, ctrl.Request{NamespacedName: appKey})
|
||||
}
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(namespace),
|
||||
client.MatchingLabels{
|
||||
oam.LabelAppName: appName,
|
||||
},
|
||||
}
|
||||
appRevisionList := new(v1beta1.ApplicationRevisionList)
|
||||
Eventually(func() error {
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
|
||||
By("create new appRevision will remove appRevison1")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the oldest revision")
|
||||
}
|
||||
if res, err := util.CheckAppRevision(appRevisionList.Items, []int{2, 3, 4, 5, 6, 7}); err != nil || !res {
|
||||
return fmt.Errorf("appRevision collection mismatch")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*10, time.Second*2).Should(BeNil())
|
||||
})
|
||||
})
|
||||
@@ -1,178 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("CRD without definition can run in an ApplicationConfiguration", func() {
|
||||
ctx := context.Background()
|
||||
It("create an application without CRD", func() {
|
||||
|
||||
By("Creating CRD foo.crdtest1.com")
|
||||
// Create a crd for appconfig dependency test
|
||||
crd = crdv1.CustomResourceDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo.crdtest1.com",
|
||||
Labels: map[string]string{"crd": "dependency"},
|
||||
},
|
||||
Spec: crdv1.CustomResourceDefinitionSpec{
|
||||
Group: "crdtest1.com",
|
||||
Names: crdv1.CustomResourceDefinitionNames{
|
||||
Kind: "Foo",
|
||||
ListKind: "FooList",
|
||||
Plural: "foo",
|
||||
Singular: "foo",
|
||||
},
|
||||
Versions: []crdv1.CustomResourceDefinitionVersion{{
|
||||
Name: "v1",
|
||||
Served: true,
|
||||
Storage: true,
|
||||
Schema: &crdv1.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &crdv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]crdv1.JSONSchemaProps{
|
||||
"spec": {
|
||||
Type: "object",
|
||||
Properties: map[string]crdv1.JSONSchemaProps{
|
||||
"key": {Type: "string"},
|
||||
}}}}}},
|
||||
},
|
||||
Scope: crdv1.NamespaceScoped,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.Background(), &crd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
By("Creating namespace trait-no-def-test")
|
||||
namespace := "trait-no-def-test"
|
||||
var ns = corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
By("creating component using workload by foo.crdtest1.com without definition")
|
||||
tempFoo := &unstructured.Unstructured{}
|
||||
tempFoo.SetAPIVersion("crdtest1.com/v1")
|
||||
tempFoo.SetKind("Foo")
|
||||
tempFoo.SetNamespace(namespace)
|
||||
// Define a workload
|
||||
wl := tempFoo.DeepCopy()
|
||||
// Set Name so we can get easily
|
||||
wlname := "test-workload"
|
||||
wl.SetName(wlname)
|
||||
// Create a component
|
||||
componentName := "component"
|
||||
comp := v1alpha2.Component{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: componentName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ComponentSpec{
|
||||
Workload: runtime.RawExtension{
|
||||
Object: wl,
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, &comp)).Should(BeNil())
|
||||
|
||||
By("Create application configuration with trait using foo.crdtest1.com without definition")
|
||||
tr := tempFoo.DeepCopy()
|
||||
// Set Name so we can get easily
|
||||
trname := "test-trait"
|
||||
tr.SetName(trname)
|
||||
appConfigName := "appconfig-trait-no-def"
|
||||
appConfig := v1alpha2.ApplicationConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: appConfigName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ApplicationConfigurationSpec{
|
||||
Components: []v1alpha2.ApplicationConfigurationComponent{{
|
||||
ComponentName: componentName,
|
||||
Traits: []v1alpha2.ComponentTrait{{
|
||||
Trait: runtime.RawExtension{
|
||||
Object: tr,
|
||||
}}},
|
||||
}}},
|
||||
}
|
||||
By("Creating application config")
|
||||
Expect(k8sClient.Create(ctx, &appConfig)).Should(BeNil())
|
||||
|
||||
By("Reconcile")
|
||||
appconfigKey := client.ObjectKey{
|
||||
Name: appConfigName,
|
||||
Namespace: namespace,
|
||||
}
|
||||
req := reconcile.Request{NamespacedName: appconfigKey}
|
||||
Expect(func() error { _, err := reconciler.Reconcile(context.TODO(), req); return err }()).Should(BeNil())
|
||||
|
||||
By("Checking that workload should be created")
|
||||
workloadKey := client.ObjectKey{
|
||||
Name: wlname,
|
||||
Namespace: namespace,
|
||||
}
|
||||
workloadFoo := tempFoo.DeepCopy()
|
||||
Eventually(func() error {
|
||||
err := k8sClient.Get(ctx, workloadKey, workloadFoo)
|
||||
if err != nil {
|
||||
// Try 3 (= 1s/300ms) times
|
||||
reconciler.Reconcile(context.TODO(), req)
|
||||
}
|
||||
return err
|
||||
}, 30*time.Second, time.Second).Should(BeNil())
|
||||
|
||||
By("Checking that trait should be created")
|
||||
traitKey := client.ObjectKey{
|
||||
Name: trname,
|
||||
Namespace: namespace,
|
||||
}
|
||||
traitFoo := tempFoo.DeepCopy()
|
||||
Eventually(func() error {
|
||||
err := k8sClient.Get(ctx, traitKey, traitFoo)
|
||||
if err != nil {
|
||||
// Try 3 (= 1s/300ms) times
|
||||
reconciler.Reconcile(context.TODO(), req)
|
||||
}
|
||||
return err
|
||||
}, 10*time.Second, 300*time.Millisecond).Should(BeNil())
|
||||
|
||||
By("Checking the application status has right warning message")
|
||||
Expect(func() string {
|
||||
err := k8sClient.Get(ctx, appconfigKey, &appConfig)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return appConfig.Status.Workloads[0].Traits[0].Message
|
||||
}()).Should(Equal(util.DummyTraitMessage))
|
||||
})
|
||||
|
||||
})
|
||||
-793
@@ -1,793 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Crossplane 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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
ctrlrec "github.com/kubevela/pkg/controller/reconciler"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/klog/v2"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
oamtype "github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/common"
|
||||
core "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/apply"
|
||||
)
|
||||
|
||||
// Reconcile error strings.
|
||||
const (
|
||||
errGetAppConfig = "cannot get application configuration"
|
||||
errUpdateAppConfigStatus = "cannot update application configuration status"
|
||||
errExecutePrehooks = "failed to execute pre-hooks"
|
||||
errExecutePosthooks = "failed to execute post-hooks"
|
||||
errRenderComponents = "cannot render components"
|
||||
errApplyComponents = "cannot apply components"
|
||||
errGCComponent = "cannot garbage collect components"
|
||||
errFinalizeWorkloads = "failed to finalize workloads"
|
||||
)
|
||||
|
||||
// Reconcile event reasons.
|
||||
const (
|
||||
reasonRevision = "ACRevision"
|
||||
reasonRenderComponents = "RenderedComponents"
|
||||
reasonExecutePrehook = "ExecutePrehook"
|
||||
reasonExecutePosthook = "ExecutePosthook"
|
||||
reasonApplyComponents = "AppliedComponents"
|
||||
reasonGGComponent = "GarbageCollectedComponent"
|
||||
reasonCannotExecutePrehooks = "CannotExecutePrehooks"
|
||||
reasonCannotExecutePosthooks = "CannotExecutePosthooks"
|
||||
reasonCannotRenderComponents = "CannotRenderComponents"
|
||||
reasonCannotApplyComponents = "CannotApplyComponents"
|
||||
reasonCannotGGComponents = "CannotGarbageCollectComponents"
|
||||
reasonCannotFinalizeWorkloads = "CannotFinalizeWorkloads"
|
||||
)
|
||||
|
||||
// Setup adds a controller that reconciles ApplicationConfigurations.
|
||||
func Setup(mgr ctrl.Manager, args core.Args) error {
|
||||
name := "oam/" + strings.ToLower(v1alpha2.ApplicationConfigurationGroupKind)
|
||||
|
||||
builder := ctrl.NewControllerManagedBy(mgr)
|
||||
builder.WithOptions(controller.Options{
|
||||
MaxConcurrentReconciles: args.ConcurrentReconciles,
|
||||
})
|
||||
|
||||
return builder.
|
||||
Named(name).
|
||||
For(&v1alpha2.ApplicationConfiguration{}).
|
||||
Watches(&source.Kind{Type: &v1alpha2.Component{}}, &ComponentHandler{
|
||||
Client: mgr.GetClient(),
|
||||
RevisionLimit: args.RevisionLimit,
|
||||
CustomRevisionHookURL: args.CustomRevisionHookURL,
|
||||
}).
|
||||
Complete(NewReconciler(mgr,
|
||||
WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))),
|
||||
WithApplyOnceOnlyMode(args.ApplyMode),
|
||||
WithDependCheckWait(args.DependCheckWait)))
|
||||
}
|
||||
|
||||
// An OAMApplicationReconciler reconciles OAM ApplicationConfigurations by rendering and
|
||||
// instantiating their Components and Traits.
|
||||
type OAMApplicationReconciler struct {
|
||||
client client.Client
|
||||
components ComponentRenderer
|
||||
workloads WorkloadApplicator
|
||||
gc GarbageCollector
|
||||
scheme *runtime.Scheme
|
||||
record event.Recorder
|
||||
preHooks map[string]ControllerHooks
|
||||
postHooks map[string]ControllerHooks
|
||||
applyOnceOnlyMode core.ApplyOnceOnlyMode
|
||||
dependCheckWait time.Duration
|
||||
}
|
||||
|
||||
// A ReconcilerOption configures a Reconciler.
|
||||
type ReconcilerOption func(*OAMApplicationReconciler)
|
||||
|
||||
// WithRenderer specifies how the Reconciler should render workloads and traits.
|
||||
func WithRenderer(r ComponentRenderer) ReconcilerOption {
|
||||
return func(rc *OAMApplicationReconciler) {
|
||||
rc.components = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithApplicator specifies how the Reconciler should apply workloads and traits.
|
||||
func WithApplicator(a WorkloadApplicator) ReconcilerOption {
|
||||
return func(rc *OAMApplicationReconciler) {
|
||||
rc.workloads = a
|
||||
}
|
||||
}
|
||||
|
||||
// WithGarbageCollector specifies how the Reconciler should garbage collect
|
||||
// workloads and traits when an ApplicationConfiguration is edited to remove
|
||||
// them.
|
||||
func WithGarbageCollector(gc GarbageCollector) ReconcilerOption {
|
||||
return func(rc *OAMApplicationReconciler) {
|
||||
rc.gc = gc
|
||||
}
|
||||
}
|
||||
|
||||
// WithRecorder specifies how the Reconciler should record events.
|
||||
func WithRecorder(er event.Recorder) ReconcilerOption {
|
||||
return func(r *OAMApplicationReconciler) {
|
||||
r.record = er
|
||||
}
|
||||
}
|
||||
|
||||
// WithPrehook register a pre-hook to the Reconciler
|
||||
func WithPrehook(name string, hook ControllerHooks) ReconcilerOption {
|
||||
return func(r *OAMApplicationReconciler) {
|
||||
r.preHooks[name] = hook
|
||||
}
|
||||
}
|
||||
|
||||
// WithPosthook register a post-hook to the Reconciler
|
||||
func WithPosthook(name string, hook ControllerHooks) ReconcilerOption {
|
||||
return func(r *OAMApplicationReconciler) {
|
||||
r.postHooks[name] = hook
|
||||
}
|
||||
}
|
||||
|
||||
// WithApplyOnceOnlyMode indicates whether workloads and traits should be
|
||||
// affected if no spec change is made in the ApplicationConfiguration.
|
||||
func WithApplyOnceOnlyMode(mode core.ApplyOnceOnlyMode) ReconcilerOption {
|
||||
return func(r *OAMApplicationReconciler) {
|
||||
r.applyOnceOnlyMode = mode
|
||||
}
|
||||
}
|
||||
|
||||
// WithDependCheckWait set depend check wait
|
||||
func WithDependCheckWait(dependCheckWait time.Duration) ReconcilerOption {
|
||||
return func(r *OAMApplicationReconciler) {
|
||||
r.dependCheckWait = dependCheckWait
|
||||
}
|
||||
}
|
||||
|
||||
// NewReconciler returns an OAMApplicationReconciler that reconciles ApplicationConfigurations
|
||||
// by rendering and instantiating their Components and Traits.
|
||||
func NewReconciler(m ctrl.Manager, o ...ReconcilerOption) *OAMApplicationReconciler {
|
||||
r := &OAMApplicationReconciler{
|
||||
client: m.GetClient(),
|
||||
scheme: m.GetScheme(),
|
||||
components: &components{
|
||||
client: m.GetClient(),
|
||||
params: ParameterResolveFn(resolve),
|
||||
workload: ResourceRenderFn(renderWorkload),
|
||||
trait: ResourceRenderFn(renderTrait),
|
||||
},
|
||||
workloads: &workloads{
|
||||
applicator: apply.NewAPIApplicator(m.GetClient()),
|
||||
rawClient: m.GetClient(),
|
||||
},
|
||||
gc: GarbageCollectorFn(eligible),
|
||||
record: event.NewNopRecorder(),
|
||||
preHooks: make(map[string]ControllerHooks),
|
||||
postHooks: make(map[string]ControllerHooks),
|
||||
applyOnceOnlyMode: core.ApplyOnceOnlyOff,
|
||||
}
|
||||
|
||||
for _, ro := range o {
|
||||
ro(r)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// NOTE(negz): We don't validate anything against their definitions at the
|
||||
// controller level. We assume this will be done by validating admission
|
||||
// webhooks.
|
||||
|
||||
// Reconcile an OAM ApplicationConfigurations by rendering and instantiating its
|
||||
// Components and Traits.
|
||||
func (r *OAMApplicationReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
|
||||
ctx, cancel := ctrlrec.NewReconcileContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
klog.InfoS("Reconcile applicationConfiguration", "applicationConfiguration", klog.KRef(req.Namespace, req.Name))
|
||||
|
||||
ac := &v1alpha2.ApplicationConfiguration{}
|
||||
if err := r.client.Get(ctx, req.NamespacedName, ac); err != nil {
|
||||
return reconcile.Result{}, errors.Wrap(client.IgnoreNotFound(err), errGetAppConfig)
|
||||
}
|
||||
|
||||
ctx = util.SetNamespaceInCtx(ctx, ac.Namespace)
|
||||
if ac.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
if registerFinalizers(ac) {
|
||||
klog.V(common.LogDebug).InfoS("Register new finalizers", "finalizers", ac.ObjectMeta.Finalizers)
|
||||
return reconcile.Result{}, errors.Wrap(r.client.Update(ctx, ac), errUpdateAppConfigStatus)
|
||||
}
|
||||
} else {
|
||||
if err := r.workloads.Finalize(ctx, ac); err != nil {
|
||||
klog.InfoS("Failed to finalize workloads", "workloads status", ac.Status.Workloads,
|
||||
"err", err)
|
||||
r.record.Event(ac, event.Warning(reasonCannotFinalizeWorkloads, err))
|
||||
ac.SetConditions(condition.ReconcileError(errors.Wrap(err, errFinalizeWorkloads)))
|
||||
return reconcile.Result{}, errors.Wrap(r.UpdateStatus(ctx, ac), errUpdateAppConfigStatus)
|
||||
}
|
||||
return reconcile.Result{}, errors.Wrap(r.client.Update(ctx, ac), errUpdateAppConfigStatus)
|
||||
}
|
||||
|
||||
reconResult, err := r.ACReconcile(ctx, ac)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, util.EndReconcileWithNegativeCondition(ctx, r.client, ac, condition.ReconcileError(err))
|
||||
}
|
||||
// always update ac status and set the error
|
||||
if err := r.UpdateStatus(ctx, ac); err != nil {
|
||||
return ctrl.Result{}, util.EndReconcileWithNegativeCondition(ctx, r.client, ac, condition.ReconcileError(err))
|
||||
}
|
||||
return reconResult, nil
|
||||
}
|
||||
|
||||
// ACReconcile contains all the reconcile logic of an AC, it can be used by other controller
|
||||
func (r *OAMApplicationReconciler) ACReconcile(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) (result reconcile.Result, resultErr error) {
|
||||
|
||||
acPatch := ac.DeepCopy()
|
||||
// execute the posthooks at the end no matter what
|
||||
defer func() {
|
||||
updateObservedGeneration(ac)
|
||||
for name, hook := range r.postHooks {
|
||||
exeResult, err := hook.Exec(ctx, ac)
|
||||
if err != nil {
|
||||
klog.InfoS("Failed to execute post-hooks", "hook name", name, "err", err,
|
||||
"requeue-after", exeResult.RequeueAfter)
|
||||
r.record.Event(ac, event.Warning(reasonCannotExecutePosthooks, err))
|
||||
result = exeResult
|
||||
resultErr = errors.Wrap(err, errExecutePosthooks)
|
||||
return
|
||||
}
|
||||
r.record.Event(ac, event.Normal(reasonExecutePosthook, "Successfully executed a posthook",
|
||||
"posthook name", name))
|
||||
}
|
||||
}()
|
||||
|
||||
// execute the prehooks
|
||||
for name, hook := range r.preHooks {
|
||||
result, err := hook.Exec(ctx, ac)
|
||||
if err != nil {
|
||||
klog.InfoS("Failed to execute pre-hooks", "hook name", name, "requeue-after", result.RequeueAfter, "err", err)
|
||||
r.record.Event(ac, event.Warning(reasonCannotExecutePrehooks, err))
|
||||
return result, errors.Wrap(err, errExecutePrehooks)
|
||||
}
|
||||
r.record.Event(ac, event.Normal(reasonExecutePrehook, "Successfully executed a prehook", "prehook name ", name))
|
||||
}
|
||||
|
||||
klog.InfoS("ApplicationConfiguration", "uid", ac.GetUID(), "version", ac.GetResourceVersion())
|
||||
|
||||
// we have special logics for application generated applicationConfiguration
|
||||
if isControlledByApp(ac) {
|
||||
if ac.GetAnnotations()[oam.AnnotationAppRevision] == strconv.FormatBool(true) {
|
||||
msg := "Encounter an application revision, no need to reconcile"
|
||||
klog.Info(msg)
|
||||
r.record.Event(ac, event.Normal(reasonRevision, msg))
|
||||
ac.SetConditions(condition.Unavailable())
|
||||
ac.Status.RollingStatus = oamtype.InactiveAfterRollingCompleted
|
||||
// TODO: GC the traits/workloads
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
workloads, depStatus, err := r.components.Render(ctx, ac)
|
||||
if err != nil {
|
||||
klog.InfoS("Cannot render components", "err", err)
|
||||
r.record.Event(ac, event.Warning(reasonCannotRenderComponents, err))
|
||||
return reconcile.Result{}, errors.Wrap(err, errRenderComponents)
|
||||
}
|
||||
klog.V(common.LogDebug).InfoS("Successfully rendered components", "workloads", len(workloads))
|
||||
r.record.Event(ac, event.Normal(reasonRenderComponents, "Successfully rendered components",
|
||||
"workloads", strconv.Itoa(len(workloads))))
|
||||
|
||||
applyOpts := []apply.ApplyOption{apply.MustBeControllableBy(ac.GetUID()), applyOnceOnly(ac, r.applyOnceOnlyMode)}
|
||||
if err := r.workloads.Apply(ctx, ac.Status.Workloads, workloads, applyOpts...); err != nil {
|
||||
klog.InfoS("Cannot apply workload", "err", err)
|
||||
r.record.Event(ac, event.Warning(reasonCannotApplyComponents, err))
|
||||
return reconcile.Result{}, errors.Wrap(err, errApplyComponents)
|
||||
}
|
||||
// only change the status after the apply succeeds
|
||||
// TODO: take into account the templating object may not be applied if there are dependencies
|
||||
if ac.Status.RollingStatus == oamtype.RollingTemplating {
|
||||
klog.InfoS("mark the ac rolling status as templated", "appConfig", klog.KRef(ac.Namespace, ac.Name))
|
||||
ac.Status.RollingStatus = oamtype.RollingTemplated
|
||||
}
|
||||
klog.V(common.LogDebug).InfoS("Successfully applied components", "workloads", len(workloads))
|
||||
r.record.Event(ac, event.Normal(reasonApplyComponents, "Successfully applied components",
|
||||
"workloads", strconv.Itoa(len(workloads))))
|
||||
|
||||
// Kubernetes garbage collection will (by default) reap workloads and traits
|
||||
// when the appconfig that controls them (in the controller reference sense)
|
||||
// is deleted. Here we cover the case in which a component or one of its
|
||||
// traits is removed from an extant appconfig.
|
||||
for _, e := range r.gc.Eligible(ac.GetNamespace(), ac.Status.Workloads, workloads) {
|
||||
// https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable
|
||||
e := e
|
||||
klog.InfoS("Collect garbage ", "resource", klog.KRef(e.GetNamespace(), e.GetName()),
|
||||
"apiVersion", e.GetAPIVersion(), "kind", e.GetKind())
|
||||
record := r.record.WithAnnotations("kind", e.GetKind(), "name", e.GetName())
|
||||
|
||||
err := r.confirmDeleteOnApplyOnceMode(ctx, ac.GetNamespace(), &e)
|
||||
if err != nil {
|
||||
klog.InfoS("Confirm component can't be garbage collected", "err", err)
|
||||
record.Event(ac, event.Warning(reasonCannotGGComponents, err))
|
||||
return reconcile.Result{}, errors.Wrap(err, errGCComponent)
|
||||
}
|
||||
if err := r.client.Delete(ctx, &e); resource.IgnoreNotFound(err) != nil {
|
||||
klog.InfoS("Cannot garbage collect component", "err", err)
|
||||
record.Event(ac, event.Warning(reasonCannotGGComponents, err))
|
||||
return reconcile.Result{}, errors.Wrap(err, errGCComponent)
|
||||
}
|
||||
klog.V(common.LogDebug).Info("Garbage collected resource")
|
||||
record.Event(ac, event.Normal(reasonGGComponent, "Successfully garbage collected component"))
|
||||
}
|
||||
|
||||
// patch the final status on the client side, k8s sever can't merge them
|
||||
r.updateStatus(ctx, ac, acPatch, workloads)
|
||||
|
||||
ac.Status.Dependency = v1alpha2.DependencyStatus{}
|
||||
var waitTime time.Duration
|
||||
if len(depStatus.Unsatisfied) != 0 {
|
||||
waitTime = r.dependCheckWait
|
||||
ac.Status.Dependency = *depStatus
|
||||
}
|
||||
|
||||
// the defer function will do the final status update
|
||||
return reconcile.Result{RequeueAfter: waitTime}, nil
|
||||
}
|
||||
|
||||
// confirmDeleteOnApplyOnceMode will confirm whether the workload can be delete or not in apply once only enabled mode
|
||||
// currently only workload replicas with 0 can be delete
|
||||
func (r *OAMApplicationReconciler) confirmDeleteOnApplyOnceMode(ctx context.Context, namespace string, u *unstructured.Unstructured) error {
|
||||
if r.applyOnceOnlyMode == core.ApplyOnceOnlyOff {
|
||||
return nil
|
||||
}
|
||||
getU := u.DeepCopy()
|
||||
err := r.client.Get(ctx, client.ObjectKey{Name: u.GetName(), Namespace: namespace}, getU)
|
||||
if err != nil {
|
||||
// no need to check if workload not found
|
||||
return resource.IgnoreNotFound(err)
|
||||
}
|
||||
// only check for workload
|
||||
if labels := getU.GetLabels(); labels == nil || labels[oam.LabelOAMResourceType] != oam.ResourceTypeWorkload {
|
||||
return nil
|
||||
}
|
||||
paved := fieldpath.Pave(getU.Object)
|
||||
|
||||
// TODO: add more kinds of workload replica check here if needed
|
||||
// "spec.replicas" maybe not accurate for all kinds of workload, but it work for most of them(including Deployment/StatefulSet/CloneSet).
|
||||
// For workload which don't align with the `spec.replicas` schema, the check won't work
|
||||
replicas, err := paved.GetInteger("spec.replicas")
|
||||
if err != nil {
|
||||
// it's possible for workload without the `spec.replicas`, it's omitempty
|
||||
if strings.Contains(err.Error(), "no such field") {
|
||||
return nil
|
||||
}
|
||||
return errors.WithMessage(err, "fail to get 'spec.replicas' from workload")
|
||||
}
|
||||
if replicas > 0 {
|
||||
return errors.Errorf("can't delete workload with replicas %d in apply once only mode", replicas)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStatus updates v1alpha2.ApplicationConfiguration's Status with retry.RetryOnConflict
|
||||
func (r *OAMApplicationReconciler) UpdateStatus(ctx context.Context, ac *v1alpha2.ApplicationConfiguration, opts ...client.SubResourceUpdateOption) error {
|
||||
status := ac.DeepCopy().Status
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
if err = r.client.Get(ctx, types.NamespacedName{Namespace: ac.Namespace, Name: ac.Name}, ac); err != nil {
|
||||
return
|
||||
}
|
||||
ac.Status = status
|
||||
return r.client.Status().Update(ctx, ac, opts...)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *OAMApplicationReconciler) updateStatus(ctx context.Context, ac, acPatch *v1alpha2.ApplicationConfiguration, workloads []Workload) {
|
||||
ac.Status.Workloads = make([]v1alpha2.WorkloadStatus, len(workloads))
|
||||
historyWorkloads := make([]v1alpha2.HistoryWorkload, 0)
|
||||
for i, w := range workloads {
|
||||
ac.Status.Workloads[i] = workloads[i].Status()
|
||||
if !w.RevisionEnabled {
|
||||
continue
|
||||
}
|
||||
var ul unstructured.UnstructuredList
|
||||
ul.SetKind(w.Workload.GetKind())
|
||||
ul.SetAPIVersion(w.Workload.GetAPIVersion())
|
||||
if err := r.client.List(ctx, &ul, client.MatchingLabels{oam.LabelAppName: ac.Name,
|
||||
oam.LabelAppComponent: w.ComponentName, oam.LabelOAMResourceType: oam.ResourceTypeWorkload}); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, v := range ul.Items {
|
||||
if v.GetName() == w.ComponentRevisionName {
|
||||
continue
|
||||
}
|
||||
// These workload exists means the component is under progress of rollout
|
||||
// Trait will not work for these remaining workload
|
||||
historyWorkloads = append(historyWorkloads, v1alpha2.HistoryWorkload{
|
||||
Revision: v.GetName(),
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: v.GetAPIVersion(),
|
||||
Kind: v.GetKind(),
|
||||
Name: v.GetName(),
|
||||
UID: v.GetUID(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
ac.Status.HistoryWorkloads = historyWorkloads
|
||||
// patch the extra fields in the status that is wiped by the Status() function
|
||||
patchExtraStatusField(&ac.Status, acPatch.Status)
|
||||
ac.SetConditions(condition.ReconcileSuccess())
|
||||
}
|
||||
|
||||
func updateObservedGeneration(ac *v1alpha2.ApplicationConfiguration) {
|
||||
if ac.Status.ObservedGeneration != ac.Generation {
|
||||
ac.Status.ObservedGeneration = ac.Generation
|
||||
}
|
||||
for i, w := range ac.Status.Workloads {
|
||||
// only the workload meet all dependency can mean the generation applied successfully
|
||||
if w.AppliedComponentRevision != w.ComponentRevisionName && !w.DependencyUnsatisfied {
|
||||
ac.Status.Workloads[i].AppliedComponentRevision = w.ComponentRevisionName
|
||||
}
|
||||
for j, t := range w.Traits {
|
||||
if t.AppliedGeneration != ac.Generation && !t.DependencyUnsatisfied {
|
||||
ac.Status.Workloads[i].Traits[j].AppliedGeneration = ac.Generation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func patchExtraStatusField(acStatus *v1alpha2.ApplicationConfigurationStatus, acPatchStatus v1alpha2.ApplicationConfigurationStatus) {
|
||||
// patch the extra status back
|
||||
for i := range acStatus.Workloads {
|
||||
for _, w := range acPatchStatus.Workloads {
|
||||
// find the workload in the old status
|
||||
if acStatus.Workloads[i].ComponentRevisionName == w.ComponentRevisionName {
|
||||
if len(w.Status) > 0 {
|
||||
acStatus.Workloads[i].Status = w.Status
|
||||
}
|
||||
// find the trait
|
||||
for j := range acStatus.Workloads[i].Traits {
|
||||
for _, t := range w.Traits {
|
||||
tr := acStatus.Workloads[i].Traits[j].Reference
|
||||
if t.Reference.APIVersion == tr.APIVersion && t.Reference.Kind == tr.Kind && t.Reference.Name == tr.Name {
|
||||
if len(t.Status) > 0 {
|
||||
acStatus.Workloads[i].Traits[j].Status = t.Status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if any finalizers newly registered, return true
|
||||
func registerFinalizers(ac *v1alpha2.ApplicationConfiguration) bool {
|
||||
newFinalizer := false
|
||||
if !meta.FinalizerExists(&ac.ObjectMeta, workloadScopeFinalizer) && hasScope(ac) {
|
||||
meta.AddFinalizer(&ac.ObjectMeta, workloadScopeFinalizer)
|
||||
newFinalizer = true
|
||||
}
|
||||
return newFinalizer
|
||||
}
|
||||
|
||||
func hasScope(ac *v1alpha2.ApplicationConfiguration) bool {
|
||||
for _, c := range ac.Spec.Components {
|
||||
if len(c.Scopes) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// A Workload produced by an OAM ApplicationConfiguration.
|
||||
type Workload struct {
|
||||
// ComponentName that produced this workload.
|
||||
ComponentName string
|
||||
|
||||
// ComponentRevisionName of current component
|
||||
ComponentRevisionName string
|
||||
|
||||
// A Workload object.
|
||||
Workload *unstructured.Unstructured
|
||||
|
||||
// SkipApply indicates that the workload should not be applied
|
||||
SkipApply bool
|
||||
|
||||
// HasDep indicates whether this resource has dependencies and unready to be applied.
|
||||
HasDep bool
|
||||
|
||||
// Traits associated with this workload.
|
||||
Traits []*Trait
|
||||
|
||||
// RevisionEnabled means multiple workloads of same component will possibly be alive.
|
||||
RevisionEnabled bool
|
||||
|
||||
// Scopes associated with this workload.
|
||||
Scopes []unstructured.Unstructured
|
||||
|
||||
// Record the DataOutputs of this workload, key is name of DataOutput.
|
||||
DataOutputs map[string]v1alpha2.DataOutput
|
||||
|
||||
// Record the DataInputs of this workload.
|
||||
DataInputs []v1alpha2.DataInput
|
||||
}
|
||||
|
||||
// A Trait produced by an OAM ApplicationConfiguration.
|
||||
type Trait struct {
|
||||
Object unstructured.Unstructured
|
||||
|
||||
// HasDep indicates whether this resource has dependencies and unready to be applied.
|
||||
HasDep bool
|
||||
|
||||
// Definition indicates the trait's definition
|
||||
Definition v1alpha2.TraitDefinition
|
||||
|
||||
// Record the DataOutputs of this trait, key is name of DataOutput.
|
||||
DataOutputs map[string]v1alpha2.DataOutput
|
||||
|
||||
// Record the DataInputs of this trait.
|
||||
DataInputs []v1alpha2.DataInput
|
||||
}
|
||||
|
||||
// Status produces the status of this workload and its traits, suitable for use
|
||||
// in the status of an ApplicationConfiguration.
|
||||
func (w Workload) Status() v1alpha2.WorkloadStatus {
|
||||
acw := v1alpha2.WorkloadStatus{
|
||||
ComponentName: w.ComponentName,
|
||||
ComponentRevisionName: w.ComponentRevisionName,
|
||||
DependencyUnsatisfied: w.HasDep,
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: w.Workload.GetAPIVersion(),
|
||||
Kind: w.Workload.GetKind(),
|
||||
Name: w.Workload.GetName(),
|
||||
},
|
||||
Traits: make([]v1alpha2.WorkloadTrait, len(w.Traits)),
|
||||
Scopes: make([]v1alpha2.WorkloadScope, len(w.Scopes)),
|
||||
}
|
||||
for i, tr := range w.Traits {
|
||||
if tr.Definition.Name == util.Dummy && tr.Definition.Spec.Reference.Name == util.Dummy {
|
||||
acw.Traits[i].Message = util.DummyTraitMessage
|
||||
}
|
||||
acw.Traits[i].Reference = corev1.ObjectReference{
|
||||
APIVersion: w.Traits[i].Object.GetAPIVersion(),
|
||||
Kind: w.Traits[i].Object.GetKind(),
|
||||
Name: w.Traits[i].Object.GetName(),
|
||||
}
|
||||
acw.Traits[i].DependencyUnsatisfied = tr.HasDep
|
||||
}
|
||||
for i, s := range w.Scopes {
|
||||
acw.Scopes[i].Reference = corev1.ObjectReference{
|
||||
APIVersion: s.GetAPIVersion(),
|
||||
Kind: s.GetKind(),
|
||||
Name: s.GetName(),
|
||||
}
|
||||
}
|
||||
return acw
|
||||
}
|
||||
|
||||
// A GarbageCollector returns resource eligible for garbage collection. A
|
||||
// resource is considered eligible if a reference exists in the supplied slice
|
||||
// of workload statuses, but not in the supplied slice of workloads.
|
||||
type GarbageCollector interface {
|
||||
Eligible(namespace string, ws []v1alpha2.WorkloadStatus, w []Workload) []unstructured.Unstructured
|
||||
}
|
||||
|
||||
// A GarbageCollectorFn returns resource eligible for garbage collection.
|
||||
type GarbageCollectorFn func(namespace string, ws []v1alpha2.WorkloadStatus, w []Workload) []unstructured.Unstructured
|
||||
|
||||
// Eligible resources.
|
||||
func (fn GarbageCollectorFn) Eligible(namespace string, ws []v1alpha2.WorkloadStatus, w []Workload) []unstructured.Unstructured {
|
||||
return fn(namespace, ws, w)
|
||||
}
|
||||
|
||||
// IsRevisionWorkload check is a workload is an old revision Workload which shouldn't be garbage collected.
|
||||
func IsRevisionWorkload(status v1alpha2.WorkloadStatus, w []Workload) bool {
|
||||
if strings.HasPrefix(status.Reference.Name, status.ComponentName+"-") {
|
||||
// for compatibility, keep the old way
|
||||
return true
|
||||
}
|
||||
|
||||
// check all workload, with same componentName
|
||||
for _, wr := range w {
|
||||
if wr.ComponentName == status.ComponentName {
|
||||
return wr.RevisionEnabled
|
||||
}
|
||||
}
|
||||
// component not found, should be deleted
|
||||
return false
|
||||
}
|
||||
|
||||
func eligible(namespace string, ws []v1alpha2.WorkloadStatus, w []Workload) []unstructured.Unstructured {
|
||||
applied := make(map[corev1.ObjectReference]bool)
|
||||
for _, wl := range w {
|
||||
r := corev1.ObjectReference{
|
||||
APIVersion: wl.Workload.GetAPIVersion(),
|
||||
Kind: wl.Workload.GetKind(),
|
||||
Name: wl.Workload.GetName(),
|
||||
}
|
||||
applied[r] = true
|
||||
for _, t := range wl.Traits {
|
||||
r := corev1.ObjectReference{
|
||||
APIVersion: t.Object.GetAPIVersion(),
|
||||
Kind: t.Object.GetKind(),
|
||||
Name: t.Object.GetName(),
|
||||
}
|
||||
applied[r] = true
|
||||
}
|
||||
}
|
||||
eligible := make([]unstructured.Unstructured, 0)
|
||||
for _, s := range ws {
|
||||
|
||||
if !applied[s.Reference] && !IsRevisionWorkload(s, w) {
|
||||
w := &unstructured.Unstructured{}
|
||||
w.SetAPIVersion(s.Reference.APIVersion)
|
||||
w.SetKind(s.Reference.Kind)
|
||||
w.SetNamespace(namespace)
|
||||
w.SetName(s.Reference.Name)
|
||||
eligible = append(eligible, *w)
|
||||
}
|
||||
|
||||
for _, ts := range s.Traits {
|
||||
if !applied[ts.Reference] {
|
||||
t := &unstructured.Unstructured{}
|
||||
t.SetAPIVersion(ts.Reference.APIVersion)
|
||||
t.SetKind(ts.Reference.Kind)
|
||||
t.SetNamespace(namespace)
|
||||
t.SetName(ts.Reference.Name)
|
||||
eligible = append(eligible, *t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return eligible
|
||||
}
|
||||
|
||||
// GenerationUnchanged indicates the resource being applied has no generation changed
|
||||
// comparing to the existing one.
|
||||
type GenerationUnchanged struct{}
|
||||
|
||||
func (e *GenerationUnchanged) Error() string {
|
||||
return fmt.Sprint("apply-only-once enabled,",
|
||||
"and detect generation in the annotation unchanged, will not apply.",
|
||||
"Please ignore this error in other logic.")
|
||||
}
|
||||
|
||||
// applyOnceOnly is an ApplyOption that controls the applying mechanism for workload and trait.
|
||||
// More detail refers to the ApplyOnceOnlyMode type annotation
|
||||
func applyOnceOnly(ac *v1alpha2.ApplicationConfiguration, mode core.ApplyOnceOnlyMode) apply.ApplyOption {
|
||||
return apply.MakeCustomApplyOption(func(existing, desired client.Object) error {
|
||||
if mode == core.ApplyOnceOnlyOff {
|
||||
return nil
|
||||
}
|
||||
d, _ := desired.(metav1.Object)
|
||||
if d == nil {
|
||||
return errors.Errorf("cannot access metadata of object being applied: %q",
|
||||
desired.GetObjectKind().GroupVersionKind())
|
||||
}
|
||||
dLabels := d.GetLabels()
|
||||
dAnnots := d.GetAnnotations()
|
||||
if dLabels[oam.LabelOAMResourceType] != oam.ResourceTypeWorkload &&
|
||||
dLabels[oam.LabelOAMResourceType] != oam.ResourceTypeTrait {
|
||||
// this ApplyOption only works for workload and trait
|
||||
// skip if the resource is not workload nor trait, e.g., scope
|
||||
klog.InfoS("Ignore apply only once check, because resourceType is not workload or trait",
|
||||
oam.LabelOAMResourceType, dLabels[oam.LabelOAMResourceType])
|
||||
return nil
|
||||
}
|
||||
|
||||
// the resource doesn't exist (maybe not created before, or created but deleted by others)
|
||||
if existing == nil {
|
||||
if mode != core.ApplyOnceOnlyForce {
|
||||
// non-force mode will always create the resource if not exist.
|
||||
klog.InfoS("Apply only once with mode:" + string(mode) + ", but old resource not exist, will create a new one")
|
||||
return nil
|
||||
}
|
||||
|
||||
createdBefore := false
|
||||
var appliedRevision, appliedGeneration string
|
||||
for _, w := range ac.Status.Workloads {
|
||||
// traverse recorded workloads to find the one matching applied resource
|
||||
if w.Reference.GetObjectKind().GroupVersionKind() == desired.GetObjectKind().GroupVersionKind() &&
|
||||
w.Reference.Name == d.GetName() {
|
||||
// the workload matches applied resource
|
||||
createdBefore = true
|
||||
// for workload, when revision enabled, only when revision changed that can trigger to create a new one
|
||||
if dLabels[oam.LabelOAMResourceType] == oam.ResourceTypeWorkload &&
|
||||
w.AppliedComponentRevision == dLabels[oam.LabelAppComponentRevision] {
|
||||
// the revision is not changed, so return an error to abort creating it
|
||||
return &GenerationUnchanged{}
|
||||
}
|
||||
appliedRevision = w.AppliedComponentRevision
|
||||
break
|
||||
}
|
||||
// the workload is not matched, then traverse its traits to find matching one
|
||||
for _, t := range w.Traits {
|
||||
if t.Reference.GetObjectKind().GroupVersionKind() == desired.GetObjectKind().GroupVersionKind() &&
|
||||
t.Reference.Name == d.GetName() {
|
||||
// the trait matches applied resource
|
||||
createdBefore = true
|
||||
// the resource was created before and appConfig status recorded the resource version applied
|
||||
// if recorded AppliedGeneration and ComponentRevisionName both equal to the applied resource's,
|
||||
// that means its spec is not changed
|
||||
if dLabels[oam.LabelOAMResourceType] == oam.ResourceTypeTrait &&
|
||||
w.ComponentRevisionName == dLabels[oam.LabelAppComponentRevision] &&
|
||||
strconv.FormatInt(t.AppliedGeneration, 10) == dAnnots[oam.AnnotationAppGeneration] {
|
||||
// the revision is not changed, so return an error to abort creating it
|
||||
return &GenerationUnchanged{}
|
||||
}
|
||||
appliedGeneration = strconv.FormatInt(t.AppliedGeneration, 10)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
var message = "apply only once with mode: force, but resource not created before, will create new"
|
||||
if createdBefore {
|
||||
message = "apply only once with mode: force, but resource updated, will create new"
|
||||
}
|
||||
klog.InfoS(message, "appConfig", ac.Name, "gvk", desired.GetObjectKind().GroupVersionKind(), "name", d.GetName(),
|
||||
"resourceType", dLabels[oam.LabelOAMResourceType], "appliedCompRevision", appliedRevision,
|
||||
"labeledCompRevision", dLabels[oam.LabelAppComponentRevision],
|
||||
"appliedGeneration", appliedGeneration, "labeledGeneration", dAnnots[oam.AnnotationAppGeneration])
|
||||
|
||||
// no recorded workloads nor traits matches the applied resource
|
||||
// that means the resource is not created before, so create it
|
||||
return nil
|
||||
}
|
||||
|
||||
// the resource already exists
|
||||
e, _ := existing.(metav1.Object)
|
||||
if e == nil {
|
||||
return errors.Errorf("cannot access metadata of existing object: %q",
|
||||
existing.GetObjectKind().GroupVersionKind())
|
||||
}
|
||||
eLabels := e.GetLabels()
|
||||
// if existing resource's (observed)AppConfigGeneration and ComponentRevisionName both equal to the applied one's,
|
||||
// that means its spec is not changed
|
||||
if (e.GetAnnotations()[oam.AnnotationAppGeneration] != dAnnots[oam.AnnotationAppGeneration]) ||
|
||||
(eLabels[oam.LabelAppComponentRevision] != dLabels[oam.LabelAppComponentRevision]) {
|
||||
klog.InfoS("Apply only once with mode: "+string(mode)+", but new generation or revision created, will create new",
|
||||
oam.AnnotationAppGeneration, e.GetAnnotations()[oam.AnnotationAppGeneration]+"/"+dAnnots[oam.AnnotationAppGeneration],
|
||||
oam.LabelAppComponentRevision, eLabels[oam.LabelAppComponentRevision]+"/"+dLabels[oam.LabelAppComponentRevision])
|
||||
// its spec is changed, so apply new configuration to it
|
||||
return nil
|
||||
}
|
||||
// its spec is not changed, return an error to abort applying it
|
||||
return &GenerationUnchanged{}
|
||||
})
|
||||
}
|
||||
-1924
File diff suppressed because it is too large
Load Diff
@@ -1,493 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Crossplane 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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"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/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/apply"
|
||||
)
|
||||
|
||||
// Reconcile error strings.
|
||||
const (
|
||||
errFmtApplyWorkload = "cannot apply workload %q"
|
||||
errFmtSetWorkloadRef = "cannot set trait %q reference to %q"
|
||||
errFmtSetScopeWorkloadRef = "cannot set scope %q reference to %q"
|
||||
errFmtGetTraitDefinition = "cannot find trait definition %q %q %q"
|
||||
errFmtGetScopeDefinition = "cannot find scope definition %q %q %q"
|
||||
errFmtGetScopeWorkloadRef = "cannot find scope workloadRef %q %q %q with workloadRefsPath %q"
|
||||
errFmtGetScopeWorkloadRefsPath = "cannot get workloadRefsPath for scope to be dereferenced %q %q %q"
|
||||
errFmtApplyTrait = "cannot apply trait %q %q %q"
|
||||
errFmtApplyScope = "cannot apply scope %q %q %q"
|
||||
|
||||
workloadScopeFinalizer = "scope.finalizer.core.oam.dev"
|
||||
dot byte = '.'
|
||||
slash byte = '/'
|
||||
dQuotes byte = '"'
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvaildOperationType describes the error that Operator of DataOperation is not in defined DataOperator
|
||||
ErrInvaildOperationType = errors.New("invaild type in operation")
|
||||
// ErrInvaildOperationValueAndValueFrom describes the error that both value and valueFrom in DataOperation are empty
|
||||
ErrInvaildOperationValueAndValueFrom = errors.New("invaild value and valueFrom in operation: both are empty")
|
||||
)
|
||||
|
||||
// A WorkloadApplicator creates or updates or finalizes workloads and their traits.
|
||||
type WorkloadApplicator interface {
|
||||
// Apply a workload and its traits.
|
||||
Apply(ctx context.Context, status []v1alpha2.WorkloadStatus, w []Workload, ao ...apply.ApplyOption) error
|
||||
|
||||
// Finalize implements pre-delete hooks on workloads
|
||||
Finalize(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) error
|
||||
}
|
||||
|
||||
// A WorkloadApplyFns creates or updates or finalizes workloads and their traits.
|
||||
type WorkloadApplyFns struct {
|
||||
ApplyFn func(ctx context.Context, status []v1alpha2.WorkloadStatus, w []Workload, ao ...apply.ApplyOption) error
|
||||
FinalizeFn func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) error
|
||||
}
|
||||
|
||||
// Apply a workload and its traits. It employes the same mechanism as `kubectl apply`, that is, for each resource being applied,
|
||||
// computing a three-way diff merge in client side based on its current state, modified stated and last-applied-state which is
|
||||
// tracked through an specific annotaion. If the resource doesn't exist before, Apply will create it.
|
||||
func (fn WorkloadApplyFns) Apply(ctx context.Context, status []v1alpha2.WorkloadStatus, w []Workload, ao ...apply.ApplyOption) error {
|
||||
return fn.ApplyFn(ctx, status, w, ao...)
|
||||
}
|
||||
|
||||
// Finalize workloads and its traits/scopes.
|
||||
func (fn WorkloadApplyFns) Finalize(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) error {
|
||||
return fn.FinalizeFn(ctx, ac)
|
||||
}
|
||||
|
||||
type workloads struct {
|
||||
applicator apply.Applicator
|
||||
rawClient client.Client
|
||||
}
|
||||
|
||||
func (a *workloads) Apply(ctx context.Context, status []v1alpha2.WorkloadStatus, w []Workload,
|
||||
ao ...apply.ApplyOption) error {
|
||||
if len(w) == 0 {
|
||||
return nil
|
||||
}
|
||||
// they are all in the same namespace
|
||||
var namespace = w[0].Workload.GetNamespace()
|
||||
for _, wl := range w {
|
||||
if !wl.HasDep {
|
||||
if wl.SkipApply {
|
||||
klog.InfoS("skip apply a workload due to rollout", "component name", wl.ComponentName, "component revision",
|
||||
wl.ComponentRevisionName)
|
||||
} else {
|
||||
// Apply the DataInputs to this workload
|
||||
if err := a.ApplyInputRef(ctx, wl.Workload, wl.DataInputs, namespace, ao...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.applicator.Apply(ctx, wl.Workload, ao...); err != nil {
|
||||
if !errors.Is(err, &GenerationUnchanged{}) {
|
||||
// GenerationUnchanged only aborts applying current workload
|
||||
// but not blocks the whole reconciliation through returning an error
|
||||
return errors.Wrapf(err, errFmtApplyWorkload, wl.Workload.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// Apply the ready DatatOutputs of this workload
|
||||
if err := a.ApplyOutputRef(ctx, wl.Workload, wl.DataOutputs, namespace, ao...); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, trait := range wl.Traits {
|
||||
if !trait.HasDep {
|
||||
if err := a.ApplyInputRef(ctx, &trait.Object, trait.DataInputs, namespace, ao...); err != nil {
|
||||
return err
|
||||
}
|
||||
t := trait.Object
|
||||
if err := a.applicator.Apply(ctx, &trait.Object, ao...); err != nil {
|
||||
if !errors.Is(err, &GenerationUnchanged{}) {
|
||||
// GenerationUnchanged only aborts applying current trait
|
||||
// but not blocks the whole reconciliation through returning an error
|
||||
return errors.Wrapf(err, errFmtApplyTrait, t.GetAPIVersion(), t.GetKind(), t.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := a.ApplyOutputRef(ctx, &trait.Object, trait.DataOutputs, namespace, ao...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
workloadRef := corev1.ObjectReference{
|
||||
APIVersion: wl.Workload.GetAPIVersion(),
|
||||
Kind: wl.Workload.GetKind(),
|
||||
Name: wl.Workload.GetName(),
|
||||
}
|
||||
for _, s := range wl.Scopes {
|
||||
if err := a.applyScope(ctx, wl, s, workloadRef); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return a.dereferenceScope(ctx, namespace, status, w)
|
||||
}
|
||||
|
||||
func (a *workloads) ApplyOutputRef(ctx context.Context, w *unstructured.Unstructured, outputs map[string]v1alpha2.DataOutput, namespace string, ao ...apply.ApplyOption) error {
|
||||
for _, output := range outputs {
|
||||
if reflect.DeepEqual(output, v1alpha2.DataOutput{}) || reflect.DeepEqual(output.OutputStore, v1alpha2.StoreReference{}) {
|
||||
continue
|
||||
}
|
||||
// Get the running workload
|
||||
runningW := &unstructured.Unstructured{}
|
||||
runningW.SetAPIVersion(w.GetAPIVersion())
|
||||
runningW.SetKind(w.GetKind())
|
||||
key := types.NamespacedName{
|
||||
Namespace: w.GetNamespace(),
|
||||
Name: w.GetName(),
|
||||
}
|
||||
if err := a.rawClient.Get(ctx, key, runningW); err != nil {
|
||||
return err
|
||||
}
|
||||
// Get the outputRef object
|
||||
ref := &unstructured.Unstructured{}
|
||||
ref.SetAPIVersion(output.OutputStore.APIVersion)
|
||||
ref.SetKind(output.OutputStore.Kind)
|
||||
key = types.NamespacedName{
|
||||
Namespace: namespace,
|
||||
Name: output.OutputStore.Name,
|
||||
}
|
||||
if err := a.rawClient.Get(ctx, key, ref); err != nil {
|
||||
if resource.IgnoreNotFound(err) != nil {
|
||||
return err
|
||||
}
|
||||
// Create the outputRef object if it doesn't exist
|
||||
ref.SetNamespace(namespace)
|
||||
ref.SetName(output.OutputStore.Name)
|
||||
ref.SetOwnerReferences(runningW.GetOwnerReferences())
|
||||
if err := a.applicator.Apply(ctx, ref, ao...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = a.rawClient.Get(ctx, key, ref); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, oper := range output.OutputStore.Operations {
|
||||
if err := operationProcess(ref, runningW, oper); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := a.applicator.Apply(ctx, ref, ao...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (a *workloads) ApplyInputRef(ctx context.Context, w *unstructured.Unstructured, inputs []v1alpha2.DataInput, namespace string, ao ...apply.ApplyOption) error {
|
||||
for _, input := range inputs {
|
||||
if reflect.DeepEqual(input, v1alpha2.DataInput{}) || reflect.DeepEqual(input.InputStore, v1alpha2.StoreReference{}) {
|
||||
continue
|
||||
}
|
||||
ref := &unstructured.Unstructured{}
|
||||
ref.SetAPIVersion(input.InputStore.APIVersion)
|
||||
ref.SetKind(input.InputStore.Kind)
|
||||
key := types.NamespacedName{
|
||||
Namespace: namespace,
|
||||
Name: input.InputStore.Name,
|
||||
}
|
||||
if err := a.rawClient.Get(ctx, key, ref); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, oper := range input.InputStore.Operations {
|
||||
if err := operationProcess(w, ref, oper); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func operationProcess(inputObj *unstructured.Unstructured, outputObj *unstructured.Unstructured, oper v1alpha2.DataOperation) error {
|
||||
switch oper.Type {
|
||||
case "jsonPatch":
|
||||
jsonBytes, err := json.Marshal(inputObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetJSON := []byte(gjson.GetBytes(jsonBytes, oper.ToFieldPath).String())
|
||||
value := ""
|
||||
switch {
|
||||
case len(oper.Value) != 0:
|
||||
value = oper.Value
|
||||
case len(oper.ValueFrom.FieldPath) != 0:
|
||||
v, err := getValueFromPath(outputObj, oper.ValueFrom.FieldPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vJSON, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value = string(vJSON)
|
||||
default:
|
||||
return ErrInvaildOperationValueAndValueFrom
|
||||
}
|
||||
targetJSON, err = jsonOperation(targetJSON, oper.Operator, oper.ToDataPath, value, oper.ToDataPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jsonBytes, err = jsonOperation(jsonBytes, v1alpha2.ReplaceOperator, oper.ToFieldPath, string(targetJSON), oper.ToDataPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(jsonBytes, inputObj); err != nil {
|
||||
return errors.Wrap(err, errUnmarshalWorkload)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return ErrInvaildOperationType
|
||||
}
|
||||
}
|
||||
|
||||
func (a *workloads) Finalize(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) error {
|
||||
var namespace = ac.GetNamespace()
|
||||
|
||||
if meta.FinalizerExists(&ac.ObjectMeta, workloadScopeFinalizer) {
|
||||
if err := a.dereferenceAllScopes(ctx, namespace, ac.Status.Workloads); err != nil {
|
||||
return err
|
||||
}
|
||||
meta.RemoveFinalizer(&ac.ObjectMeta, workloadScopeFinalizer)
|
||||
}
|
||||
|
||||
// add finalizer logic here
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *workloads) dereferenceScope(ctx context.Context, namespace string, status []v1alpha2.WorkloadStatus, w []Workload) error {
|
||||
for _, st := range status {
|
||||
toBeDeferenced := st.Scopes
|
||||
for _, wl := range w {
|
||||
if (st.Reference.APIVersion == wl.Workload.GetAPIVersion()) &&
|
||||
(st.Reference.Kind == wl.Workload.GetKind()) &&
|
||||
(st.Reference.Name == wl.Workload.GetName()) {
|
||||
toBeDeferenced = findDereferencedScopes(st.Scopes, wl.Scopes)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range toBeDeferenced {
|
||||
if err := a.applyScopeRemoval(ctx, namespace, st.Reference, s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dereferenceAllScope dereferences workloads owned by the appConfig being deleted from the scopes they belong to.
|
||||
func (a *workloads) dereferenceAllScopes(ctx context.Context, namespace string, status []v1alpha2.WorkloadStatus) error {
|
||||
for _, st := range status {
|
||||
for _, sc := range st.Scopes {
|
||||
if err := a.applyScopeRemoval(ctx, namespace, st.Reference, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findDereferencedScopes(statusScopes []v1alpha2.WorkloadScope, scopes []unstructured.Unstructured) []v1alpha2.WorkloadScope {
|
||||
toBeDeferenced := []v1alpha2.WorkloadScope{}
|
||||
for _, ss := range statusScopes {
|
||||
found := false
|
||||
for _, s := range scopes {
|
||||
if (s.GetAPIVersion() == ss.Reference.APIVersion) &&
|
||||
(s.GetKind() == ss.Reference.Kind) &&
|
||||
(s.GetName() == ss.Reference.Name) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
toBeDeferenced = append(toBeDeferenced, ss)
|
||||
}
|
||||
}
|
||||
|
||||
return toBeDeferenced
|
||||
}
|
||||
|
||||
func (a *workloads) applyScope(ctx context.Context, wl Workload, s unstructured.Unstructured, workloadRef corev1.ObjectReference) error {
|
||||
// get ScopeDefinition
|
||||
scopeDefinition, err := util.FetchScopeDefinition(ctx, a.rawClient, &s)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errFmtGetScopeDefinition, s.GetAPIVersion(), s.GetKind(), s.GetName())
|
||||
}
|
||||
// checkout whether scope asks for workloadRef
|
||||
workloadRefsPath := scopeDefinition.Spec.WorkloadRefsPath
|
||||
if len(workloadRefsPath) == 0 {
|
||||
// this scope does not ask for workloadRefs
|
||||
return nil
|
||||
}
|
||||
|
||||
var refs []interface{}
|
||||
if value, err := fieldpath.Pave(s.UnstructuredContent()).GetValue(workloadRefsPath); err == nil {
|
||||
refs = value.([]interface{})
|
||||
|
||||
for _, item := range refs {
|
||||
ref := item.(map[string]interface{})
|
||||
if (workloadRef.APIVersion == ref["apiVersion"]) &&
|
||||
(workloadRef.Kind == ref["kind"]) &&
|
||||
(workloadRef.Name == ref["name"]) {
|
||||
// workloadRef is already present, so no need to add it.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return errors.Wrapf(err, errFmtGetScopeWorkloadRef, s.GetAPIVersion(), s.GetKind(), s.GetName(), workloadRefsPath)
|
||||
}
|
||||
|
||||
refs = append(refs, workloadRef)
|
||||
if err := fieldpath.Pave(s.UnstructuredContent()).SetValue(workloadRefsPath, refs); err != nil {
|
||||
return errors.Wrapf(err, errFmtSetScopeWorkloadRef, s.GetName(), wl.Workload.GetName())
|
||||
}
|
||||
|
||||
if err := a.rawClient.Update(ctx, &s); err != nil {
|
||||
return errors.Wrapf(err, errFmtApplyScope, s.GetAPIVersion(), s.GetKind(), s.GetName())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyScopeRemoval remove the workload reference from the scope's reference list.
|
||||
// If the scope or scope definition is not found(deleted), it's still regarded as remove successfully.
|
||||
func (a *workloads) applyScopeRemoval(ctx context.Context, namespace string, wr corev1.ObjectReference, s v1alpha2.WorkloadScope) error {
|
||||
scopeObject := unstructured.Unstructured{}
|
||||
scopeObject.SetAPIVersion(s.Reference.APIVersion)
|
||||
scopeObject.SetKind(s.Reference.Kind)
|
||||
scopeObjectRef := types.NamespacedName{Namespace: namespace, Name: s.Reference.Name}
|
||||
if err := a.rawClient.Get(ctx, scopeObjectRef, &scopeObject); err != nil {
|
||||
// if the scope is already deleted
|
||||
// treat it as removal done to avoid blocking AppConfig finalizer
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrapf(err, errFmtApplyScope, s.Reference.APIVersion, s.Reference.Kind, s.Reference.Name)
|
||||
}
|
||||
|
||||
scopeDefinition, err := util.FetchScopeDefinition(ctx, a.rawClient, &scopeObject)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// if the scope definition is deleted
|
||||
// treat it as removal done to avoid blocking AppConfig finalizer
|
||||
return nil
|
||||
}
|
||||
return errors.Wrapf(err, errFmtGetScopeDefinition, scopeObject.GetAPIVersion(), scopeObject.GetKind(), scopeObject.GetName())
|
||||
}
|
||||
|
||||
workloadRefsPath := scopeDefinition.Spec.WorkloadRefsPath
|
||||
if len(workloadRefsPath) == 0 {
|
||||
// Scopes to be dereferenced MUST have workloadRefsPath
|
||||
return errors.Errorf(errFmtGetScopeWorkloadRefsPath, scopeObject.GetAPIVersion(), scopeObject.GetKind(), scopeObject.GetName())
|
||||
}
|
||||
|
||||
if value, err := fieldpath.Pave(scopeObject.UnstructuredContent()).GetValue(workloadRefsPath); err == nil {
|
||||
refs := value.([]interface{})
|
||||
|
||||
workloadRefIndex := -1
|
||||
for i, item := range refs {
|
||||
ref := item.(map[string]interface{})
|
||||
if (wr.APIVersion == ref["apiVersion"]) &&
|
||||
(wr.Kind == ref["kind"]) &&
|
||||
(wr.Name == ref["name"]) {
|
||||
workloadRefIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if workloadRefIndex >= 0 {
|
||||
// Remove the element at index i.
|
||||
refs[workloadRefIndex] = refs[len(refs)-1]
|
||||
refs = refs[:len(refs)-1]
|
||||
|
||||
if err := fieldpath.Pave(scopeObject.UnstructuredContent()).SetValue(workloadRefsPath, refs); err != nil {
|
||||
return errors.Wrapf(err, errFmtSetScopeWorkloadRef, s.Reference.Name, wr.Name)
|
||||
}
|
||||
|
||||
if err := a.rawClient.Update(ctx, &scopeObject); err != nil {
|
||||
return errors.Wrapf(err, errFmtApplyScope, s.Reference.APIVersion, s.Reference.Kind, s.Reference.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return errors.Wrapf(err, errFmtGetScopeWorkloadRef,
|
||||
scopeObject.GetAPIVersion(), scopeObject.GetKind(), scopeObject.GetName(), workloadRefsPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getValueFromPath(w *unstructured.Unstructured, path string) (interface{}, error) {
|
||||
paved := fieldpath.Pave(w.UnstructuredContent())
|
||||
rawval, err := paved.GetValue(path)
|
||||
if err != nil {
|
||||
if fieldpath.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("%s not found in object", path)
|
||||
}
|
||||
err = fmt.Errorf("failed to get field value (%s) in object (%s:%s): %w", path, w.GetNamespace(), w.GetName(), err)
|
||||
return nil, err
|
||||
}
|
||||
return rawval, nil
|
||||
}
|
||||
|
||||
func jsonOperation(jsonBytes []byte, op v1alpha2.DataOperator, path, value, toDataPath string) ([]byte, error) {
|
||||
if len(jsonBytes) == 0 || len(path) == 0 {
|
||||
return []byte(value), nil
|
||||
}
|
||||
patchJSON := []byte(`[{"op": "` + string(op) + `", "path": "`)
|
||||
// \. is used to escape dot, @@@DOTDOTDOT@@@ is used to avoid replacement of dot in following operation
|
||||
path = strings.ReplaceAll(path, `\.`, `@@@DOTDOTDOT@@@`)
|
||||
path = strings.ReplaceAll(path, string(dot), string(slash))
|
||||
path = strings.ReplaceAll(path, `@@@DOTDOTDOT@@@`, `.`)
|
||||
if path[0] != slash {
|
||||
patchJSON = append(patchJSON, slash)
|
||||
}
|
||||
value = strings.ReplaceAll(strings.ReplaceAll(value, `\"`, `"`), `\\`, `\`)
|
||||
if len(value) > 1 && value[0] == dQuotes && value[len(value)-1] == dQuotes {
|
||||
value = string(dQuotes) + strings.ReplaceAll(strings.ReplaceAll(value[1:len(value)-1], `\`, `\\`), `"`, `\"`) + string(dQuotes)
|
||||
} else if len(toDataPath) > 0 {
|
||||
value = string(dQuotes) + strings.ReplaceAll(strings.ReplaceAll(value, `\`, `\\`), `"`, `\"`) + string(dQuotes)
|
||||
}
|
||||
patchJSON = append(patchJSON, []byte(path+`", "value": `+value+`}]`)...)
|
||||
patch, err := jsonpatch.DecodePatch(patchJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return patch.Apply(jsonBytes)
|
||||
}
|
||||
-880
@@ -1,880 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
core "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/testutil"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("Test apply (workloads/traits) once only", func() {
|
||||
const (
|
||||
namespace = "apply-once-only-test"
|
||||
appName = "example-app"
|
||||
compName = "example-comp"
|
||||
traitName = "example-trait"
|
||||
image1 = "wordpress:latest"
|
||||
image2 = "nginx:latest"
|
||||
traitSpecValue1 = "test1"
|
||||
traitSpecValue2 = "test2"
|
||||
)
|
||||
var (
|
||||
ctx = context.Background()
|
||||
cw appsv1.Deployment
|
||||
component v1alpha2.Component
|
||||
fakeTrait *unstructured.Unstructured
|
||||
appConfig v1alpha2.ApplicationConfiguration
|
||||
cwObjKey = client.ObjectKey{
|
||||
Name: compName,
|
||||
Namespace: namespace,
|
||||
}
|
||||
traitObjKey = client.ObjectKey{
|
||||
Name: traitName,
|
||||
Namespace: namespace,
|
||||
}
|
||||
appConfigKey = client.ObjectKey{
|
||||
Name: appName,
|
||||
Namespace: namespace,
|
||||
}
|
||||
req = reconcile.Request{NamespacedName: appConfigKey}
|
||||
ns corev1.Namespace
|
||||
)
|
||||
|
||||
metataDataLabels := make(map[string]string)
|
||||
metataDataLabels["app"] = "wordpress"
|
||||
|
||||
var labelSelector = new(metav1.LabelSelector)
|
||||
labelSelector.MatchLabels = metataDataLabels
|
||||
BeforeEach(func() {
|
||||
cw = appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: labelSelector,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Image: image1,
|
||||
Name: "wordpress",
|
||||
},
|
||||
},
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: metataDataLabels,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
component = v1alpha2.Component{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "core.oam.dev/v1alpha2",
|
||||
Kind: "Component",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: compName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ComponentSpec{
|
||||
Workload: runtime.RawExtension{
|
||||
Object: &cw,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeTrait = &unstructured.Unstructured{}
|
||||
fakeTrait.SetAPIVersion("example.com/v1")
|
||||
fakeTrait.SetKind("Foo")
|
||||
fakeTrait.SetNamespace(namespace)
|
||||
fakeTrait.SetName(traitName)
|
||||
unstructured.SetNestedField(fakeTrait.Object, traitSpecValue1, "spec", "key")
|
||||
|
||||
appConfig = v1alpha2.ApplicationConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: appName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ApplicationConfigurationSpec{
|
||||
Components: []v1alpha2.ApplicationConfigurationComponent{
|
||||
{
|
||||
ComponentName: compName,
|
||||
Traits: []v1alpha2.ComponentTrait{
|
||||
{Trait: runtime.RawExtension{Object: fakeTrait}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
By("Create namespace")
|
||||
ns = corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
Eventually(
|
||||
func() error {
|
||||
return k8sClient.Create(ctx, &ns)
|
||||
},
|
||||
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
By("Create Component")
|
||||
Expect(k8sClient.Create(ctx, &component)).Should(Succeed())
|
||||
cmpV1 := &v1alpha2.Component{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compName}, cmpV1)).Should(Succeed())
|
||||
|
||||
By("Creat appConfig & check successfully")
|
||||
Expect(k8sClient.Create(ctx, &appConfig)).Should(Succeed())
|
||||
Eventually(func() error {
|
||||
return k8sClient.Get(ctx, appConfigKey, &appConfig)
|
||||
}, time.Second, 300*time.Millisecond).Should(BeNil())
|
||||
|
||||
By("Reconcile")
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
logf.Log.Info("Clean up previous resources")
|
||||
Expect(k8sClient.DeleteAllOf(ctx, &appConfig, client.InNamespace(namespace))).Should(Succeed())
|
||||
Expect(k8sClient.DeleteAllOf(ctx, &cw, client.InNamespace(namespace))).Should(Succeed())
|
||||
Expect(k8sClient.DeleteAllOf(ctx, &component, client.InNamespace(namespace))).Should(Succeed())
|
||||
var deleteTrait unstructured.Unstructured
|
||||
deleteTrait.SetAPIVersion("example.com/v1")
|
||||
deleteTrait.SetKind("Foo")
|
||||
Expect(k8sClient.DeleteAllOf(ctx, &deleteTrait, client.InNamespace(namespace))).Should(Succeed())
|
||||
// restore as default value
|
||||
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyOff
|
||||
})
|
||||
|
||||
When("ApplyOnceOnly is enabled", func() {
|
||||
It("should not revert changes of workload/trait made by others", func() {
|
||||
By("Enable ApplyOnceOnly")
|
||||
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyOn
|
||||
|
||||
By("Get workload instance & Check workload spec")
|
||||
cwObj := appsv1.Deployment{}
|
||||
Eventually(func() error {
|
||||
By("Reconcile")
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
return k8sClient.Get(ctx, cwObjKey, &cwObj)
|
||||
}, 5*time.Second, time.Second).Should(BeNil())
|
||||
Expect(cwObj.Spec.Template.Spec.Containers[0].Image).Should(Equal(image1))
|
||||
|
||||
By("Get trait instance & Check trait spec")
|
||||
fooObj := &unstructured.Unstructured{}
|
||||
fooObj.SetAPIVersion("example.com/v1")
|
||||
fooObj.SetKind("Foo")
|
||||
Eventually(func() error {
|
||||
return k8sClient.Get(ctx, traitObjKey, fooObj)
|
||||
}, 3*time.Second, time.Second).Should(BeNil())
|
||||
fooObjV, _, _ := unstructured.NestedString(fooObj.Object, "spec", "key")
|
||||
Expect(fooObjV).Should(Equal(traitSpecValue1))
|
||||
|
||||
By("Modify workload spec & Apply changed workload")
|
||||
cwObj.Spec.Template.Spec.Containers[0].Image = image2
|
||||
Expect(k8sClient.Patch(ctx, &cwObj, client.Merge)).Should(Succeed())
|
||||
|
||||
By("Modify trait spec & Apply changed trait")
|
||||
unstructured.SetNestedField(fooObj.Object, traitSpecValue2, "spec", "key")
|
||||
Expect(k8sClient.Patch(ctx, fooObj, client.Merge)).Should(Succeed())
|
||||
|
||||
By("Get updated workload instance & Check workload spec")
|
||||
updateCwObj := appsv1.Deployment{}
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
return updateCwObj.Spec.Template.Spec.Containers[0].Image
|
||||
}, 3*time.Second, time.Second).Should(Equal(image2))
|
||||
|
||||
By("Get updated trait instance & Check trait spec")
|
||||
updatedFooObj := &unstructured.Unstructured{}
|
||||
updatedFooObj.SetAPIVersion("example.com/v1")
|
||||
updatedFooObj.SetKind("Foo")
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
v, _, _ := unstructured.NestedString(fooObj.Object, "spec", "key")
|
||||
return v
|
||||
}, 3*time.Second, time.Second).Should(Equal(traitSpecValue2))
|
||||
|
||||
By("Reconcile")
|
||||
Expect(func() error { _, err := reconciler.Reconcile(context.TODO(), req); return err }()).Should(BeNil())
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
By("Check workload is not changed by reconciliation")
|
||||
updateCwObj = appsv1.Deployment{}
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
return updateCwObj.Spec.Template.Spec.Containers[0].Image
|
||||
}, 3*time.Second, time.Second).Should(Equal(image2))
|
||||
|
||||
By("Check trait is not changed by reconciliation")
|
||||
updatedFooObj = &unstructured.Unstructured{}
|
||||
updatedFooObj.SetAPIVersion("example.com/v1")
|
||||
updatedFooObj.SetKind("Foo")
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
v, _, _ := unstructured.NestedString(updatedFooObj.Object, "spec", "key")
|
||||
return v
|
||||
}, 3*time.Second, time.Second).Should(Equal(traitSpecValue2))
|
||||
|
||||
By("Disable ApplyOnceOnly & Reconcile again")
|
||||
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyOff
|
||||
Expect(func() error { _, err := reconciler.Reconcile(context.TODO(), req); return err }()).Should(BeNil())
|
||||
|
||||
By("Check workload is changed by reconciliation")
|
||||
updateCwObj = appsv1.Deployment{}
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
return updateCwObj.Spec.Template.Spec.Containers[0].Image
|
||||
}, 3*time.Second, time.Second).Should(Equal(image1))
|
||||
|
||||
By("Check trait is changed by reconciliation")
|
||||
updatedFooObj = &unstructured.Unstructured{}
|
||||
updatedFooObj.SetAPIVersion("example.com/v1")
|
||||
updatedFooObj.SetKind("Foo")
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
v, _, _ := unstructured.NestedString(updatedFooObj.Object, "spec", "key")
|
||||
return v
|
||||
}, 3*time.Second, time.Second).Should(Equal(traitSpecValue1))
|
||||
})
|
||||
|
||||
It("should re-create workload/trait if it's delete by others", func() {
|
||||
By("Enable ApplyOnceOnly")
|
||||
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyOn
|
||||
|
||||
By("Get workload instance & Check workload spec")
|
||||
cwObj := appsv1.Deployment{}
|
||||
Eventually(func() error {
|
||||
By("Reconcile")
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
return k8sClient.Get(ctx, cwObjKey, &cwObj)
|
||||
}, 5*time.Second, time.Second).Should(BeNil())
|
||||
|
||||
By("Delete the workload")
|
||||
Expect(k8sClient.Delete(ctx, &cwObj)).Should(Succeed())
|
||||
Expect(k8sClient.Get(ctx, cwObjKey, &appsv1.Deployment{})).Should(util.NotFoundMatcher{})
|
||||
|
||||
By("Reconcile")
|
||||
Expect(func() error { _, err := reconciler.Reconcile(context.TODO(), req); return err }()).Should(BeNil())
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
By("Check workload is created by reconciliation")
|
||||
recreatedCwObj := appsv1.Deployment{}
|
||||
Expect(k8sClient.Get(ctx, cwObjKey, &recreatedCwObj)).Should(Succeed())
|
||||
})
|
||||
})
|
||||
|
||||
When("ApplyOnceOnlyForce is enabled", func() {
|
||||
It("tests the situation where workload is not applied at the first because of unsatisfied dependency",
|
||||
func() {
|
||||
componentHandler := &ComponentHandler{Client: k8sClient, RevisionLimit: 100}
|
||||
|
||||
By("Enable ApplyOnceOnlyForce")
|
||||
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
|
||||
|
||||
tempFoo := &unstructured.Unstructured{}
|
||||
tempFoo.SetAPIVersion("example.com/v1")
|
||||
tempFoo.SetKind("Foo")
|
||||
tempFoo.SetNamespace(namespace)
|
||||
|
||||
inName := "data-input"
|
||||
inputWorkload := &unstructured.Unstructured{}
|
||||
inputWorkload.SetAPIVersion("example.com/v1")
|
||||
inputWorkload.SetKind("Foo")
|
||||
inputWorkload.SetNamespace(namespace)
|
||||
inputWorkload.SetName(inName)
|
||||
|
||||
compInName := "comp-in"
|
||||
compIn := v1alpha2.Component{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: compInName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ComponentSpec{
|
||||
Workload: runtime.RawExtension{
|
||||
Object: inputWorkload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
outName := "data-output"
|
||||
outputTrait := tempFoo.DeepCopy()
|
||||
outputTrait.SetName(outName)
|
||||
|
||||
acWithDepName := "ac-dep"
|
||||
acWithDep := v1alpha2.ApplicationConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: acWithDepName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ApplicationConfigurationSpec{
|
||||
Components: []v1alpha2.ApplicationConfigurationComponent{
|
||||
{
|
||||
ComponentName: compInName,
|
||||
DataInputs: []v1alpha2.DataInput{
|
||||
{
|
||||
ValueFrom: v1alpha2.DataInputValueFrom{
|
||||
DataOutputName: "trait-output",
|
||||
},
|
||||
ToFieldPaths: []string{"spec.key"},
|
||||
},
|
||||
},
|
||||
Traits: []v1alpha2.ComponentTrait{{
|
||||
Trait: runtime.RawExtension{Object: outputTrait},
|
||||
DataOutputs: []v1alpha2.DataOutput{{
|
||||
Name: "trait-output",
|
||||
FieldPath: "status.key",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
By("Create Component")
|
||||
Expect(k8sClient.Create(ctx, &compIn)).Should(Succeed())
|
||||
cmp := &v1alpha2.Component{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName}, cmp)).Should(Succeed())
|
||||
|
||||
cmpV1 := cmp.DeepCopy()
|
||||
By("component handler will automatically create controller revision")
|
||||
Expect(func() bool {
|
||||
_, ok := componentHandler.createControllerRevision(cmpV1, cmpV1)
|
||||
return ok
|
||||
}()).Should(BeTrue())
|
||||
|
||||
By("Creat appConfig & check successfully")
|
||||
Expect(k8sClient.Create(ctx, &acWithDep)).Should(Succeed())
|
||||
Eventually(func() error {
|
||||
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep)
|
||||
}, time.Second, 300*time.Millisecond).Should(BeNil())
|
||||
|
||||
By("Reconcile & check successfully")
|
||||
|
||||
reqDep := reconcile.Request{
|
||||
NamespacedName: client.ObjectKey{Namespace: namespace, Name: acWithDepName},
|
||||
}
|
||||
Eventually(func() bool {
|
||||
testutil.ReconcileRetry(reconciler, reqDep)
|
||||
acWithDep = v1alpha2.ApplicationConfiguration{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep); err != nil {
|
||||
return false
|
||||
}
|
||||
return len(acWithDep.Status.Workloads) == 1
|
||||
}, time.Second, 300*time.Millisecond).Should(BeTrue())
|
||||
|
||||
// because dependency is not satisfied so the workload should not be created
|
||||
By("Check the workload is NOT created")
|
||||
workloadIn := tempFoo.DeepCopy()
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: inName}, workloadIn)).Should(&util.NotFoundMatcher{})
|
||||
|
||||
// modify the trait to make it satisfy comp's dependency
|
||||
outputTrait = tempFoo.DeepCopy()
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: outName}, outputTrait)).Should(Succeed())
|
||||
err := unstructured.SetNestedField(outputTrait.Object, "test", "status", "key")
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(k8sClient.Status().Update(ctx, outputTrait)).Should(Succeed())
|
||||
Eventually(func() string {
|
||||
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: outName}, outputTrait)
|
||||
data, _, _ := unstructured.NestedString(outputTrait.Object, "status", "key")
|
||||
return data
|
||||
}, 3*time.Second, time.Second).Should(Equal("test"))
|
||||
|
||||
By("Reconcile & check ac is satisfied")
|
||||
Eventually(func() []v1alpha2.UnstaifiedDependency {
|
||||
testutil.ReconcileRetry(reconciler, reqDep)
|
||||
acWithDep = v1alpha2.ApplicationConfiguration{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep); err != nil {
|
||||
return []v1alpha2.UnstaifiedDependency{{Reason: err.Error()}}
|
||||
}
|
||||
return acWithDep.Status.Dependency.Unsatisfied
|
||||
}, time.Second, 300*time.Millisecond).Should(BeNil())
|
||||
|
||||
By("Reconcile & check workload is created")
|
||||
Eventually(func() error {
|
||||
testutil.ReconcileRetry(reconciler, reqDep)
|
||||
// the workload is created now because its dependency is satisfied
|
||||
workloadIn := tempFoo.DeepCopy()
|
||||
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: inName}, workloadIn)
|
||||
}, time.Second, 300*time.Millisecond).Should(BeNil())
|
||||
|
||||
By("Delete the workload")
|
||||
recreatedWL := tempFoo.DeepCopy()
|
||||
recreatedWL.SetName(inName)
|
||||
Expect(k8sClient.Delete(ctx, recreatedWL)).Should(Succeed())
|
||||
outputTrait = tempFoo.DeepCopy()
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: inName}, outputTrait)).Should(util.NotFoundMatcher{})
|
||||
|
||||
By("Reconcile")
|
||||
Expect(func() error { _, err := reconciler.Reconcile(context.TODO(), req); return err }()).Should(BeNil())
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
By("Check workload is not re-created by reconciliation")
|
||||
inputWorkload = tempFoo.DeepCopy()
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: inName}, inputWorkload)).Should(util.NotFoundMatcher{})
|
||||
})
|
||||
|
||||
It("tests the situation where workload is not applied at the first because of unsatisfied dependency and revision specified",
|
||||
func() {
|
||||
componentHandler := &ComponentHandler{Client: k8sClient, RevisionLimit: 100}
|
||||
|
||||
By("Enable ApplyOnceOnlyForce")
|
||||
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
|
||||
|
||||
tempFoo := &unstructured.Unstructured{}
|
||||
tempFoo.SetAPIVersion("example.com/v1")
|
||||
tempFoo.SetKind("Foo")
|
||||
tempFoo.SetNamespace(namespace)
|
||||
|
||||
inputWorkload := &unstructured.Unstructured{}
|
||||
inputWorkload.SetAPIVersion("example.com/v1")
|
||||
inputWorkload.SetKind("Foo")
|
||||
inputWorkload.SetNamespace(namespace)
|
||||
|
||||
compInName := "comp-in-revision"
|
||||
compIn := v1alpha2.Component{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: compInName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ComponentSpec{
|
||||
Workload: runtime.RawExtension{
|
||||
Object: inputWorkload,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
outName := "data-output"
|
||||
outputTrait := tempFoo.DeepCopy()
|
||||
outputTrait.SetName(outName)
|
||||
|
||||
acWithDepName := "ac-dep"
|
||||
acWithDep := v1alpha2.ApplicationConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: acWithDepName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ApplicationConfigurationSpec{
|
||||
Components: []v1alpha2.ApplicationConfigurationComponent{
|
||||
{
|
||||
RevisionName: compInName + "-v1",
|
||||
DataInputs: []v1alpha2.DataInput{
|
||||
{
|
||||
ValueFrom: v1alpha2.DataInputValueFrom{
|
||||
DataOutputName: "trait-output",
|
||||
},
|
||||
ToFieldPaths: []string{"spec.key"},
|
||||
},
|
||||
},
|
||||
Traits: []v1alpha2.ComponentTrait{{
|
||||
Trait: runtime.RawExtension{Object: outputTrait},
|
||||
DataOutputs: []v1alpha2.DataOutput{{
|
||||
Name: "trait-output",
|
||||
FieldPath: "status.key",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
By("Create Component")
|
||||
Expect(k8sClient.Create(ctx, &compIn)).Should(Succeed())
|
||||
cmp := &v1alpha2.Component{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName}, cmp)).Should(Succeed())
|
||||
|
||||
cmpV1 := cmp.DeepCopy()
|
||||
By("component handler will automatically create controller revision")
|
||||
Expect(func() bool {
|
||||
_, ok := componentHandler.createControllerRevision(cmpV1, cmpV1)
|
||||
return ok
|
||||
}()).Should(BeTrue())
|
||||
|
||||
By("Creat appConfig & check successfully")
|
||||
Expect(k8sClient.Create(ctx, &acWithDep)).Should(Succeed())
|
||||
Eventually(func() error {
|
||||
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep)
|
||||
}, time.Second, 300*time.Millisecond).Should(BeNil())
|
||||
|
||||
By("Reconcile & check successfully")
|
||||
|
||||
reqDep := reconcile.Request{
|
||||
NamespacedName: client.ObjectKey{Namespace: namespace, Name: acWithDepName},
|
||||
}
|
||||
Eventually(func() bool {
|
||||
testutil.ReconcileRetry(reconciler, reqDep)
|
||||
acWithDep = v1alpha2.ApplicationConfiguration{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep); err != nil {
|
||||
return false
|
||||
}
|
||||
return len(acWithDep.Status.Workloads) == 1
|
||||
}, time.Second, 300*time.Millisecond).Should(BeTrue())
|
||||
|
||||
// because dependency is not satisfied so the workload should not be created
|
||||
By("Check the workload is NOT created")
|
||||
workloadIn := tempFoo.DeepCopy()
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName + "-v1"}, workloadIn)).Should(&util.NotFoundMatcher{})
|
||||
|
||||
// modify the trait to make it satisfy comp's dependency
|
||||
outputTrait = tempFoo.DeepCopy()
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: outName}, outputTrait)).Should(Succeed())
|
||||
err := unstructured.SetNestedField(outputTrait.Object, "test", "status", "key")
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(k8sClient.Status().Update(ctx, outputTrait)).Should(Succeed())
|
||||
Eventually(func() string {
|
||||
k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: outName}, outputTrait)
|
||||
data, _, _ := unstructured.NestedString(outputTrait.Object, "status", "key")
|
||||
return data
|
||||
}, 3*time.Second, time.Second).Should(Equal("test"))
|
||||
|
||||
By("Reconcile & check ac is satisfied")
|
||||
Eventually(func() []v1alpha2.UnstaifiedDependency {
|
||||
testutil.ReconcileRetry(reconciler, reqDep)
|
||||
acWithDep = v1alpha2.ApplicationConfiguration{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: acWithDepName}, &acWithDep); err != nil {
|
||||
return []v1alpha2.UnstaifiedDependency{{Reason: err.Error()}}
|
||||
}
|
||||
return acWithDep.Status.Dependency.Unsatisfied
|
||||
}, time.Second, 300*time.Millisecond).Should(BeNil())
|
||||
|
||||
By("Reconcile & check workload is created")
|
||||
Eventually(func() error {
|
||||
testutil.ReconcileRetry(reconciler, reqDep)
|
||||
// the workload should be created now because its dependency is satisfied
|
||||
workloadIn := tempFoo.DeepCopy()
|
||||
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName}, workloadIn)
|
||||
}, time.Second, 300*time.Millisecond).Should(BeNil())
|
||||
|
||||
By("Delete the workload")
|
||||
recreatedWL := tempFoo.DeepCopy()
|
||||
recreatedWL.SetName(compInName)
|
||||
Expect(k8sClient.Delete(ctx, recreatedWL)).Should(Succeed())
|
||||
inputWorkload2 := tempFoo.DeepCopy()
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName}, inputWorkload2)).Should(util.NotFoundMatcher{})
|
||||
|
||||
By("Reconcile")
|
||||
Expect(func() error { _, err := reconciler.Reconcile(context.TODO(), req); return err }()).Should(BeNil())
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
By("Check workload is not re-created by reconciliation")
|
||||
inputWorkload = tempFoo.DeepCopy()
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compInName}, inputWorkload)).Should(util.NotFoundMatcher{})
|
||||
})
|
||||
|
||||
It("should normally create workload/trait resources at fist time", func() {
|
||||
By("Enable ApplyOnceOnlyForce")
|
||||
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
|
||||
component2 := v1alpha2.Component{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "core.oam.dev/v1alpha2",
|
||||
Kind: "Component",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "mycomp2",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ComponentSpec{
|
||||
Workload: runtime.RawExtension{
|
||||
Object: &cw,
|
||||
},
|
||||
},
|
||||
}
|
||||
newFakeTrait := fakeTrait.DeepCopy()
|
||||
newFakeTrait.SetName("mytrait2")
|
||||
appConfig2 := v1alpha2.ApplicationConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "myac2",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ApplicationConfigurationSpec{
|
||||
Components: []v1alpha2.ApplicationConfigurationComponent{
|
||||
{
|
||||
ComponentName: "mycomp2",
|
||||
Traits: []v1alpha2.ComponentTrait{
|
||||
{Trait: runtime.RawExtension{Object: newFakeTrait}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
By("Create Component")
|
||||
Expect(k8sClient.Create(ctx, &component2)).Should(Succeed())
|
||||
time.Sleep(time.Second)
|
||||
|
||||
By("Creat appConfig & check successfully")
|
||||
Expect(k8sClient.Create(ctx, &appConfig2)).Should(Succeed())
|
||||
time.Sleep(time.Second)
|
||||
|
||||
By("Reconcile")
|
||||
Expect(func() error {
|
||||
_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "myac2", Namespace: namespace}})
|
||||
return err
|
||||
}()).Should(BeNil())
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
By("Get workload instance & Check workload spec")
|
||||
cwObj := appsv1.Deployment{}
|
||||
Eventually(func() error {
|
||||
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: "mycomp2"}, &cwObj)
|
||||
}, 5*time.Second, time.Second).Should(BeNil())
|
||||
Expect(cwObj.Spec.Template.Spec.Containers[0].Image).Should(Equal(image1))
|
||||
|
||||
By("Get trait instance & Check trait spec")
|
||||
fooObj := &unstructured.Unstructured{}
|
||||
fooObj.SetAPIVersion("example.com/v1")
|
||||
fooObj.SetKind("Foo")
|
||||
Eventually(func() error {
|
||||
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: "mytrait2"}, fooObj)
|
||||
}, 3*time.Second, time.Second).Should(BeNil())
|
||||
fooObjV, _, _ := unstructured.NestedString(fooObj.Object, "spec", "key")
|
||||
Expect(fooObjV).Should(Equal(traitSpecValue1))
|
||||
|
||||
})
|
||||
|
||||
It("should not revert changes of workload/trait made by others", func() {
|
||||
By("Enable ApplyOnceOnlyForce")
|
||||
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
|
||||
|
||||
By("Get workload instance & Check workload spec")
|
||||
cwObj := appsv1.Deployment{}
|
||||
Eventually(func() error {
|
||||
By("Reconcile")
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
return k8sClient.Get(ctx, cwObjKey, &cwObj)
|
||||
}, 5*time.Second, time.Second).Should(BeNil())
|
||||
Expect(cwObj.Spec.Template.Spec.Containers[0].Image).Should(Equal(image1))
|
||||
|
||||
By("Get trait instance & Check trait spec")
|
||||
fooObj := &unstructured.Unstructured{}
|
||||
fooObj.SetAPIVersion("example.com/v1")
|
||||
fooObj.SetKind("Foo")
|
||||
Eventually(func() error {
|
||||
return k8sClient.Get(ctx, traitObjKey, fooObj)
|
||||
}, 3*time.Second, time.Second).Should(BeNil())
|
||||
fooObjV, _, _ := unstructured.NestedString(fooObj.Object, "spec", "key")
|
||||
Expect(fooObjV).Should(Equal(traitSpecValue1))
|
||||
|
||||
By("Modify workload spec & Apply changed workload")
|
||||
cwObj.Spec.Template.Spec.Containers[0].Image = image2
|
||||
Expect(k8sClient.Patch(ctx, &cwObj, client.Merge)).Should(Succeed())
|
||||
|
||||
By("Modify trait spec & Apply changed trait")
|
||||
unstructured.SetNestedField(fooObj.Object, traitSpecValue2, "spec", "key")
|
||||
Expect(k8sClient.Patch(ctx, fooObj, client.Merge)).Should(Succeed())
|
||||
|
||||
By("Get updated workload instance & Check workload spec")
|
||||
updateCwObj := appsv1.Deployment{}
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
return updateCwObj.Spec.Template.Spec.Containers[0].Image
|
||||
}, 3*time.Second, time.Second).Should(Equal(image2))
|
||||
|
||||
By("Get updated trait instance & Check trait spec")
|
||||
updatedFooObj := &unstructured.Unstructured{}
|
||||
updatedFooObj.SetAPIVersion("example.com/v1")
|
||||
updatedFooObj.SetKind("Foo")
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
v, _, _ := unstructured.NestedString(fooObj.Object, "spec", "key")
|
||||
return v
|
||||
}, 3*time.Second, time.Second).Should(Equal(traitSpecValue2))
|
||||
|
||||
By("Reconcile")
|
||||
Expect(func() error { _, err := reconciler.Reconcile(context.TODO(), req); return err }()).Should(BeNil())
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
By("Check workload is not changed by reconciliation")
|
||||
updateCwObj = appsv1.Deployment{}
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
return updateCwObj.Spec.Template.Spec.Containers[0].Image
|
||||
}, 3*time.Second, time.Second).Should(Equal(image2))
|
||||
|
||||
By("Check trait is not changed by reconciliation")
|
||||
updatedFooObj = &unstructured.Unstructured{}
|
||||
updatedFooObj.SetAPIVersion("example.com/v1")
|
||||
updatedFooObj.SetKind("Foo")
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
v, _, _ := unstructured.NestedString(updatedFooObj.Object, "spec", "key")
|
||||
return v
|
||||
}, 3*time.Second, time.Second).Should(Equal(traitSpecValue2))
|
||||
|
||||
By("Disable ApplyOnceOnly & Reconcile again")
|
||||
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyOff
|
||||
Expect(func() error { _, err := reconciler.Reconcile(context.TODO(), req); return err }()).Should(BeNil())
|
||||
|
||||
By("Check workload is changed by reconciliation")
|
||||
updateCwObj = appsv1.Deployment{}
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, cwObjKey, &updateCwObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
return updateCwObj.Spec.Template.Spec.Containers[0].Image
|
||||
}, 3*time.Second, time.Second).Should(Equal(image1))
|
||||
|
||||
By("Check trait is changed by reconciliation")
|
||||
updatedFooObj = &unstructured.Unstructured{}
|
||||
updatedFooObj.SetAPIVersion("example.com/v1")
|
||||
updatedFooObj.SetKind("Foo")
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, traitObjKey, updatedFooObj); err != nil {
|
||||
return ""
|
||||
}
|
||||
v, _, _ := unstructured.NestedString(updatedFooObj.Object, "spec", "key")
|
||||
return v
|
||||
}, 3*time.Second, time.Second).Should(Equal(traitSpecValue1))
|
||||
})
|
||||
|
||||
It("should not re-create workload/trait if it's delete by others", func() {
|
||||
By("Enable ApplyOnceOnlyForce")
|
||||
reconciler.applyOnceOnlyMode = core.ApplyOnceOnlyForce
|
||||
|
||||
By("Get workload instance")
|
||||
cwObj := appsv1.Deployment{}
|
||||
Eventually(func() error {
|
||||
By("Reconcile")
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
return k8sClient.Get(ctx, cwObjKey, &cwObj)
|
||||
}, 5*time.Second, time.Second).Should(BeNil())
|
||||
|
||||
By("Get trait instance & Check trait spec")
|
||||
fooObj := unstructured.Unstructured{}
|
||||
fooObj.SetAPIVersion("example.com/v1")
|
||||
fooObj.SetKind("Foo")
|
||||
Eventually(func() error {
|
||||
return k8sClient.Get(ctx, traitObjKey, &fooObj)
|
||||
}, 3*time.Second, time.Second).Should(BeNil())
|
||||
|
||||
By("Delete the workload")
|
||||
Expect(k8sClient.Delete(ctx, &cwObj)).Should(Succeed())
|
||||
Expect(k8sClient.Get(ctx, cwObjKey, &appsv1.Deployment{})).Should(util.NotFoundMatcher{})
|
||||
|
||||
By("Delete the trait")
|
||||
Expect(k8sClient.Delete(ctx, &fooObj)).Should(Succeed())
|
||||
Expect(k8sClient.Get(ctx, traitObjKey, &fooObj)).Should(util.NotFoundMatcher{})
|
||||
|
||||
By("Reconcile")
|
||||
Expect(func() error { _, err := reconciler.Reconcile(context.TODO(), req); return err }()).Should(BeNil())
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
By("Check workload is not re-created by reconciliation")
|
||||
recreatedCwObj := appsv1.Deployment{}
|
||||
Expect(k8sClient.Get(ctx, cwObjKey, &recreatedCwObj)).Should(util.NotFoundMatcher{})
|
||||
|
||||
By("Check trait is not re-created by reconciliation")
|
||||
recreatedFooObj := unstructured.Unstructured{}
|
||||
recreatedFooObj.SetAPIVersion("example.com/v1")
|
||||
recreatedFooObj.SetKind("Foo")
|
||||
Expect(k8sClient.Get(ctx, traitObjKey, &recreatedFooObj)).Should(util.NotFoundMatcher{})
|
||||
|
||||
By("Update AppConfig to trigger generation updated")
|
||||
unstructured.SetNestedField(fakeTrait.Object, "newvalue", "spec", "key")
|
||||
appConfig = v1alpha2.ApplicationConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: appName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ApplicationConfigurationSpec{
|
||||
Components: []v1alpha2.ApplicationConfigurationComponent{
|
||||
{
|
||||
ComponentName: compName,
|
||||
Traits: []v1alpha2.ComponentTrait{
|
||||
{Trait: runtime.RawExtension{Object: fakeTrait}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Patch(ctx, &appConfig, client.Merge)).Should(Succeed())
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
By("Check AppConfig is updated successfully")
|
||||
updateAC := v1alpha2.ApplicationConfiguration{}
|
||||
Eventually(func() int64 {
|
||||
if err := k8sClient.Get(ctx, appConfigKey, &updateAC); err != nil {
|
||||
return 0
|
||||
}
|
||||
return updateAC.GetGeneration()
|
||||
}, 3*time.Second, time.Second).Should(Equal(int64(2)))
|
||||
By("Reconcile")
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
By("Check workload was not created by reconciliation")
|
||||
Eventually(func() error {
|
||||
By("Reconcile")
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
recreatedCwObj = appsv1.Deployment{}
|
||||
return k8sClient.Get(ctx, cwObjKey, &recreatedCwObj)
|
||||
}, 5*time.Second, time.Second).Should(SatisfyAll(util.NotFoundMatcher{}))
|
||||
|
||||
By("Check trait is re-created by reconciliation")
|
||||
recreatedFooObj = unstructured.Unstructured{}
|
||||
recreatedFooObj.SetAPIVersion("example.com/v1")
|
||||
recreatedFooObj.SetKind("Foo")
|
||||
Expect(k8sClient.Get(ctx, traitObjKey, &recreatedFooObj)).Should(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,797 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Crossplane 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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"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/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/apply"
|
||||
)
|
||||
|
||||
// ApplyFn mocks apply.Applicator for test convenience.
|
||||
type ApplyFn func(context.Context, client.Object, ...apply.ApplyOption) error
|
||||
|
||||
// Apply implements apply.Applicator
|
||||
func (fn ApplyFn) Apply(ctx context.Context, o client.Object, ao ...apply.ApplyOption) error {
|
||||
return fn(ctx, o, ao...)
|
||||
}
|
||||
|
||||
func TestApplyWorkloads(t *testing.T) {
|
||||
errBoom := errors.New("boom")
|
||||
namespace := "ns"
|
||||
|
||||
workload := &unstructured.Unstructured{}
|
||||
workload.SetAPIVersion("workload.oam.dev")
|
||||
workload.SetKind("workloadKind")
|
||||
workload.SetNamespace(namespace)
|
||||
workload.SetName("workload-example")
|
||||
workload.SetUID(types.UID("workload-uid"))
|
||||
|
||||
trait := &unstructured.Unstructured{}
|
||||
trait.SetAPIVersion("trait.oam.dev")
|
||||
trait.SetKind("traitKind")
|
||||
trait.SetNamespace(namespace)
|
||||
trait.SetName("trait-example")
|
||||
trait.SetUID(types.UID("trait-uid"))
|
||||
|
||||
scope, _ := util.Object2Unstructured(&v1alpha2.HealthScope{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "scope-example",
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "scope.oam.dev/v1alpha2",
|
||||
Kind: "scopeKind",
|
||||
},
|
||||
Spec: v1alpha2.HealthScopeSpec{
|
||||
// set an empty ref to enable wrokloadRefs field
|
||||
WorkloadReferences: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: "",
|
||||
Kind: "",
|
||||
Name: "",
|
||||
UID: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// scope with Ref
|
||||
scopeWithRef, _ := util.Object2Unstructured(&v1alpha2.HealthScope{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "scope-example",
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "scope.oam.dev/v1alpha2",
|
||||
Kind: "scopeKind",
|
||||
},
|
||||
Spec: v1alpha2.HealthScopeSpec{
|
||||
WorkloadReferences: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: workload.GetAPIVersion(),
|
||||
Kind: workload.GetKind(),
|
||||
Name: workload.GetName(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
scopeDefinition := v1alpha2.ScopeDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ScopeDefinition",
|
||||
APIVersion: "scopeDef.oam.dev",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "scope-example.scope.oam.dev",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ScopeDefinitionSpec{
|
||||
Reference: common.DefinitionReference{
|
||||
Name: "scope-example.scope.oam.dev",
|
||||
},
|
||||
WorkloadRefsPath: "spec.workloadRefs",
|
||||
},
|
||||
}
|
||||
|
||||
type args struct {
|
||||
ws []v1alpha2.WorkloadStatus
|
||||
w []Workload
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
applicator apply.Applicator
|
||||
rawClient client.Client
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
"ApplyWorkloadError": {
|
||||
reason: "Errors applying a workload should be reflected as a status condition",
|
||||
applicator: ApplyFn(func(_ context.Context, o client.Object, _ ...apply.ApplyOption) error {
|
||||
if w, ok := o.(*unstructured.Unstructured); ok && w.GetUID() == workload.GetUID() {
|
||||
return errBoom
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
rawClient: nil,
|
||||
args: args{
|
||||
w: []Workload{{Workload: workload, Traits: []*Trait{{Object: *trait}}}},
|
||||
ws: []v1alpha2.WorkloadStatus{}},
|
||||
want: errors.Wrapf(errBoom, errFmtApplyWorkload, workload.GetName()),
|
||||
},
|
||||
"ApplyTraitError": {
|
||||
reason: "Errors applying a trait should be reflected as a status condition",
|
||||
applicator: ApplyFn(func(_ context.Context, o client.Object, _ ...apply.ApplyOption) error {
|
||||
if t, ok := o.(*unstructured.Unstructured); ok && t.GetUID() == trait.GetUID() {
|
||||
return errBoom
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
rawClient: &test.MockClient{MockGet: test.NewMockGetFn(nil)},
|
||||
args: args{
|
||||
w: []Workload{{Workload: workload, Traits: []*Trait{{Object: *trait}}}},
|
||||
ws: []v1alpha2.WorkloadStatus{}},
|
||||
want: errors.Wrapf(errBoom, errFmtApplyTrait, trait.GetAPIVersion(), trait.GetKind(), trait.GetName()),
|
||||
},
|
||||
"Success": {
|
||||
reason: "Applied workloads and traits should be returned as a set of UIDs.",
|
||||
applicator: ApplyFn(func(_ context.Context, o client.Object, _ ...apply.ApplyOption) error {
|
||||
if o.GetObjectKind().GroupVersionKind().Kind == trait.GetKind() {
|
||||
// check that the trait should not have a workload ref since we didn't return a special traitDefinition
|
||||
obj, _ := util.Object2Map(o)
|
||||
if _, ok := obj["spec"]; ok {
|
||||
return fmt.Errorf("should not get workload ref on %q", obj["kind"])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
rawClient: &test.MockClient{MockGet: test.NewMockGetFn(nil)},
|
||||
args: args{
|
||||
w: []Workload{{Workload: workload, Traits: []*Trait{{Object: *trait}}}},
|
||||
ws: []v1alpha2.WorkloadStatus{},
|
||||
},
|
||||
},
|
||||
"SuccessWithScope": {
|
||||
reason: "Applied workloads refs to scopes.",
|
||||
applicator: ApplyFn(func(_ context.Context, o client.Object, _ ...apply.ApplyOption) error { return nil }),
|
||||
rawClient: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if scopeDef, ok := obj.(*v1alpha2.ScopeDefinition); ok {
|
||||
*scopeDef = scopeDefinition
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
w: []Workload{{
|
||||
Workload: workload,
|
||||
Traits: []*Trait{{Object: *trait.DeepCopy()}},
|
||||
Scopes: []unstructured.Unstructured{*scope.DeepCopy()},
|
||||
}},
|
||||
ws: []v1alpha2.WorkloadStatus{
|
||||
{
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: workload.GetAPIVersion(),
|
||||
Kind: workload.GetKind(),
|
||||
Name: workload.GetName(),
|
||||
},
|
||||
Scopes: []v1alpha2.WorkloadScope{
|
||||
{
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: scope.GetAPIVersion(),
|
||||
Kind: scope.GetKind(),
|
||||
Name: scope.GetName(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"SuccessWithScopeNoOp": {
|
||||
reason: "Scope already has workloadRef.",
|
||||
applicator: ApplyFn(func(_ context.Context, o client.Object, _ ...apply.ApplyOption) error { return nil }),
|
||||
rawClient: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if scopeDef, ok := obj.(*v1alpha2.ScopeDefinition); ok {
|
||||
*scopeDef = scopeDefinition
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
||||
return fmt.Errorf("update is not expected in this test")
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
w: []Workload{{
|
||||
Workload: workload,
|
||||
Traits: []*Trait{{Object: *trait.DeepCopy()}},
|
||||
Scopes: []unstructured.Unstructured{*scopeWithRef.DeepCopy()},
|
||||
}},
|
||||
ws: []v1alpha2.WorkloadStatus{
|
||||
{
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: workload.GetAPIVersion(),
|
||||
Kind: workload.GetKind(),
|
||||
Name: workload.GetName(),
|
||||
},
|
||||
Scopes: []v1alpha2.WorkloadScope{
|
||||
{
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: scope.GetAPIVersion(),
|
||||
Kind: scope.GetKind(),
|
||||
Name: scope.GetName(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"SuccessRemoving": {
|
||||
reason: "Removes workload refs from scopes.",
|
||||
applicator: ApplyFn(func(_ context.Context, o client.Object, _ ...apply.ApplyOption) error { return nil }),
|
||||
rawClient: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if key.Name == scope.GetName() {
|
||||
scope := obj.(*unstructured.Unstructured)
|
||||
|
||||
refs := []interface{}{
|
||||
map[string]interface{}{
|
||||
"apiVersion": workload.GetAPIVersion(),
|
||||
"kind": workload.GetKind(),
|
||||
"name": workload.GetName(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := fieldpath.Pave(scope.UnstructuredContent()).SetValue("spec.workloadRefs", refs); err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
if scopeDef, ok := obj.(*v1alpha2.ScopeDefinition); ok {
|
||||
*scopeDef = scopeDefinition
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
w: []Workload{{
|
||||
Workload: workload,
|
||||
Traits: []*Trait{{Object: *trait.DeepCopy()}},
|
||||
Scopes: []unstructured.Unstructured{},
|
||||
}},
|
||||
ws: []v1alpha2.WorkloadStatus{
|
||||
{
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: workload.GetAPIVersion(),
|
||||
Kind: workload.GetKind(),
|
||||
Name: workload.GetName(),
|
||||
},
|
||||
Scopes: []v1alpha2.WorkloadScope{
|
||||
{
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: scope.GetAPIVersion(),
|
||||
Kind: scope.GetKind(),
|
||||
Name: scope.GetName(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"SuccessRemovingWhenScopeDefinitionNotFound": {
|
||||
reason: "ScopeDefinition not found should not block dereference",
|
||||
applicator: ApplyFn(func(_ context.Context, o client.Object, _ ...apply.ApplyOption) error { return nil }),
|
||||
rawClient: &test.MockClient{
|
||||
MockGet: func(_ context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if key.Name == scope.GetName() {
|
||||
scope := obj.(*unstructured.Unstructured)
|
||||
|
||||
refs := []interface{}{
|
||||
map[string]interface{}{
|
||||
"apiVersion": workload.GetAPIVersion(),
|
||||
"kind": workload.GetKind(),
|
||||
"name": workload.GetName(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := fieldpath.Pave(scope.UnstructuredContent()).SetValue("spec.workloadRefs", refs); err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
if _, ok := obj.(*v1alpha2.ScopeDefinition); ok {
|
||||
return apierrors.NewNotFound(schema.GroupResource{}, "test")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
w: []Workload{{
|
||||
Workload: workload,
|
||||
Traits: []*Trait{{Object: *trait.DeepCopy()}},
|
||||
Scopes: []unstructured.Unstructured{},
|
||||
}},
|
||||
ws: []v1alpha2.WorkloadStatus{
|
||||
{
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: workload.GetAPIVersion(),
|
||||
Kind: workload.GetKind(),
|
||||
Name: workload.GetName(),
|
||||
},
|
||||
Scopes: []v1alpha2.WorkloadScope{
|
||||
{
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: scope.GetAPIVersion(),
|
||||
Kind: scope.GetKind(),
|
||||
Name: scope.GetName(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
w := workloads{applicator: tc.applicator, rawClient: tc.rawClient}
|
||||
err := w.Apply(context.TODO(), tc.args.ws, tc.args.w)
|
||||
|
||||
if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nw.Apply(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalizeWorkloadScopes(t *testing.T) {
|
||||
namespace := "ns"
|
||||
errMock := errors.New("mock error")
|
||||
workload := &unstructured.Unstructured{}
|
||||
workload.SetAPIVersion("workload.oam.dev")
|
||||
workload.SetKind("workloadKind")
|
||||
workload.SetNamespace(namespace)
|
||||
workload.SetName("workload-example")
|
||||
workload.SetUID(types.UID("workload-uid"))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
scope, _ := util.Object2Unstructured(&v1alpha2.HealthScope{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "scope-example",
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "scope.oam.dev/v1alpha2",
|
||||
Kind: "scopeKind",
|
||||
},
|
||||
Spec: v1alpha2.HealthScopeSpec{
|
||||
WorkloadReferences: []corev1.ObjectReference{
|
||||
{
|
||||
APIVersion: workload.GetAPIVersion(),
|
||||
Kind: workload.GetKind(),
|
||||
Name: workload.GetName(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
scopeDefinition := v1alpha2.ScopeDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ScopeDefinition",
|
||||
APIVersion: "scopeDef.oam.dev",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "scope-example.scope.oam.dev",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ScopeDefinitionSpec{
|
||||
Reference: common.DefinitionReference{
|
||||
Name: "scope-example.scope.oam.dev",
|
||||
},
|
||||
WorkloadRefsPath: "spec.workloadRefs",
|
||||
},
|
||||
}
|
||||
|
||||
ac := v1alpha2.ApplicationConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Finalizers: []string{workloadScopeFinalizer},
|
||||
},
|
||||
Status: v1alpha2.ApplicationConfigurationStatus{
|
||||
Workloads: []v1alpha2.WorkloadStatus{
|
||||
{
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: workload.GetAPIVersion(),
|
||||
Kind: workload.GetKind(),
|
||||
Name: workload.GetName(),
|
||||
},
|
||||
Scopes: []v1alpha2.WorkloadScope{
|
||||
{
|
||||
Reference: corev1.ObjectReference{
|
||||
APIVersion: scope.GetAPIVersion(),
|
||||
Kind: scope.GetKind(),
|
||||
Name: scope.GetName(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
caseName string
|
||||
applicator apply.Applicator
|
||||
rawClient client.Client
|
||||
wantErr error
|
||||
wantFinalizers []string
|
||||
}{
|
||||
{
|
||||
caseName: "Finalization successes",
|
||||
applicator: ApplyFn(func(_ context.Context, o client.Object, _ ...apply.ApplyOption) error { return nil }),
|
||||
rawClient: &test.MockClient{
|
||||
MockGet: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
if key.Name == scope.GetName() {
|
||||
scope := obj.(*unstructured.Unstructured)
|
||||
|
||||
refs := []interface{}{
|
||||
map[string]interface{}{
|
||||
"apiVersion": workload.GetAPIVersion(),
|
||||
"kind": workload.GetKind(),
|
||||
"name": workload.GetName(),
|
||||
},
|
||||
}
|
||||
|
||||
if err := fieldpath.Pave(scope.UnstructuredContent()).SetValue("spec.workloadRefs", refs); err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
if scopeDef, ok := obj.(*v1alpha2.ScopeDefinition); ok {
|
||||
*scopeDef = scopeDefinition
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
wantFinalizers: []string{},
|
||||
},
|
||||
{
|
||||
caseName: "Finalization fails for error",
|
||||
applicator: ApplyFn(func(_ context.Context, o client.Object, _ ...apply.ApplyOption) error { return nil }),
|
||||
rawClient: &test.MockClient{
|
||||
MockGet: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
return errMock
|
||||
},
|
||||
MockUpdate: func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
wantErr: errors.Wrapf(errMock, errFmtApplyScope, scope.GetAPIVersion(), scope.GetKind(), scope.GetName()),
|
||||
wantFinalizers: []string{workloadScopeFinalizer},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.caseName, func(t *testing.T) {
|
||||
acTest := ac
|
||||
w := workloads{applicator: tc.applicator, rawClient: tc.rawClient}
|
||||
err := w.Finalize(ctx, &acTest)
|
||||
|
||||
if diff := cmp.Diff(tc.wantErr, err, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nw.Apply(...): -want error, +got error:\n%s", tc.caseName, diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantFinalizers, acTest.ObjectMeta.Finalizers); diff != "" {
|
||||
t.Errorf("\n%s\nw.Apply(...): -want error, +got error:\n%s", tc.caseName, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestApplyOutputRef(t *testing.T) {
|
||||
workload := &unstructured.Unstructured{}
|
||||
workload.SetAPIVersion("v1")
|
||||
workload.SetKind("Workload")
|
||||
workload.SetNamespace("test-ns")
|
||||
workload.SetName("test-workload")
|
||||
|
||||
runningW := workload.DeepCopy()
|
||||
err := unstructured.SetNestedField(runningW.Object, "value-in-workload", "status", "key")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
refConfigMap := &unstructured.Unstructured{}
|
||||
refConfigMap.SetAPIVersion("v1")
|
||||
refConfigMap.SetKind("ConfigMap")
|
||||
refConfigMap.SetNamespace("test-ns")
|
||||
refConfigMap.SetName("ref-configmap")
|
||||
err = unstructured.SetNestedField(refConfigMap.Object, "value-in-configmap", "status", "key")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
workload *unstructured.Unstructured
|
||||
trait *unstructured.Unstructured
|
||||
outputs map[string]v1alpha2.DataOutput
|
||||
}
|
||||
jsonPatchOper := "jsonPatch"
|
||||
|
||||
cases := map[string]struct {
|
||||
args args
|
||||
want func(*unstructured.Unstructured) *unstructured.Unstructured
|
||||
}{
|
||||
"configmap with jsonPath operations": {
|
||||
args: args{
|
||||
workload: runningW,
|
||||
outputs: map[string]v1alpha2.DataOutput{
|
||||
"test": {
|
||||
OutputStore: v1alpha2.StoreReference{
|
||||
ObjectReference: corev1.ObjectReference{
|
||||
APIVersion: refConfigMap.GetAPIVersion(),
|
||||
Kind: refConfigMap.GetKind(),
|
||||
Name: refConfigMap.GetName(),
|
||||
},
|
||||
Operations: []v1alpha2.DataOperation{{
|
||||
Type: jsonPatchOper,
|
||||
Operator: v1alpha2.AddOperator,
|
||||
ToFieldPath: "status.key",
|
||||
Value: `"{}"`,
|
||||
}, {
|
||||
Type: jsonPatchOper,
|
||||
Operator: v1alpha2.AddOperator,
|
||||
ToFieldPath: "status.key",
|
||||
ToDataPath: "value",
|
||||
ValueFrom: v1alpha2.ValueFrom{FieldPath: "status.key"},
|
||||
Conditions: []v1alpha2.ConditionRequirement{{
|
||||
Operator: v1alpha2.ConditionNotEqual,
|
||||
Value: "",
|
||||
FieldPath: "status.key",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
want: func(outRef *unstructured.Unstructured) *unstructured.Unstructured {
|
||||
expect := outRef.DeepCopy()
|
||||
err := unstructured.SetNestedField(expect.Object, `{"value":"value-in-workload"}`, "status", "key")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return nil
|
||||
}
|
||||
return expect
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
wl := workloads{
|
||||
rawClient: &test.MockClient{
|
||||
MockGet: test.MockGetFn(func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if obj.GetObjectKind().GroupVersionKind().Kind == "Workload" {
|
||||
b, err := json.Marshal(tc.args.workload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = json.Unmarshal(b, obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if obj.GetObjectKind().GroupVersionKind().Kind == "Trait" {
|
||||
b, err := json.Marshal(tc.args.trait)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = json.Unmarshal(b, obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if obj.GetObjectKind().GroupVersionKind().Kind == "ConfigMap" {
|
||||
b, err := json.Marshal(refConfigMap)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = json.Unmarshal(b, obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
applicator: ApplyFn(func(_ context.Context, o client.Object, _ ...apply.ApplyOption) error {
|
||||
if diff := cmp.Diff(o, tc.want(refConfigMap)); diff != "" {
|
||||
return errors.New(diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
err = wl.ApplyOutputRef(context.Background(), workload, tc.args.outputs, tc.args.workload.GetNamespace())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestApplyInputRef(t *testing.T) {
|
||||
workload := &unstructured.Unstructured{}
|
||||
workload.SetAPIVersion("v1")
|
||||
workload.SetKind("Workload")
|
||||
workload.SetNamespace("test-ns")
|
||||
workload.SetName("test-workload")
|
||||
err := unstructured.SetNestedField(workload.Object, "test", "status", "key")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
refConfigMap := &unstructured.Unstructured{}
|
||||
refConfigMap.SetAPIVersion("v1")
|
||||
refConfigMap.SetKind("ConfigMap")
|
||||
refConfigMap.SetNamespace("test-ns")
|
||||
refConfigMap.SetName("ref-configmap")
|
||||
err = unstructured.SetNestedField(refConfigMap.Object, "value-in-configmap", "status", "key")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
workload *unstructured.Unstructured
|
||||
trait *unstructured.Unstructured
|
||||
inputs []v1alpha2.DataInput
|
||||
}
|
||||
jsonPatchOper := "jsonPatch"
|
||||
|
||||
cases := map[string]struct {
|
||||
args args
|
||||
want func(*unstructured.Unstructured) *unstructured.Unstructured
|
||||
}{
|
||||
"jsonPatch add operation": {
|
||||
args: args{
|
||||
workload: workload.DeepCopy(),
|
||||
inputs: []v1alpha2.DataInput{{
|
||||
InputStore: v1alpha2.StoreReference{
|
||||
ObjectReference: corev1.ObjectReference{
|
||||
APIVersion: refConfigMap.GetAPIVersion(),
|
||||
Kind: refConfigMap.GetKind(),
|
||||
Name: refConfigMap.GetName(),
|
||||
},
|
||||
Operations: []v1alpha2.DataOperation{{
|
||||
Type: jsonPatchOper,
|
||||
Operator: v1alpha2.AddOperator,
|
||||
ToFieldPath: "status.key",
|
||||
Value: `"{}"`,
|
||||
}, {
|
||||
Type: jsonPatchOper,
|
||||
Operator: v1alpha2.AddOperator,
|
||||
ToFieldPath: "status.key",
|
||||
ToDataPath: "value",
|
||||
ValueFrom: v1alpha2.ValueFrom{FieldPath: "status.key"},
|
||||
Conditions: []v1alpha2.ConditionRequirement{{
|
||||
Operator: v1alpha2.ConditionNotEqual,
|
||||
Value: "",
|
||||
FieldPath: "status.key",
|
||||
}},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
want: func(workload *unstructured.Unstructured) *unstructured.Unstructured {
|
||||
expectWorkload := workload.DeepCopy()
|
||||
err := unstructured.SetNestedField(expectWorkload.Object, `{"value":"value-in-configmap"}`, "status", "key")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return nil
|
||||
}
|
||||
return expectWorkload
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
wl := workloads{
|
||||
rawClient: &test.MockClient{
|
||||
MockGet: test.MockGetFn(func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if obj.GetObjectKind().GroupVersionKind().Kind == "Workload" {
|
||||
b, err := json.Marshal(tc.args.workload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = json.Unmarshal(b, obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if obj.GetObjectKind().GroupVersionKind().Kind == "Trait" {
|
||||
b, err := json.Marshal(tc.args.trait)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = json.Unmarshal(b, obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if obj.GetObjectKind().GroupVersionKind().Kind == "ConfigMap" {
|
||||
b, err := json.Marshal(refConfigMap)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = json.Unmarshal(b, obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
}
|
||||
err = wl.ApplyInputRef(context.Background(), tc.args.workload, tc.args.inputs, tc.args.workload.GetNamespace())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if diff := cmp.Diff(tc.args.workload, tc.want(workload)); diff != "" {
|
||||
t.Error(diff)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,471 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/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/pkg/oam/testutil"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("Test apply changes to trait", func() {
|
||||
const (
|
||||
namespace = "update-apply-trait-test"
|
||||
appName = "example-app"
|
||||
compName = "example-comp"
|
||||
fakeTraitCRDName = "bars.example.com"
|
||||
fakeTraitGroup = "example.com"
|
||||
fakeTraitKind = "Bar"
|
||||
)
|
||||
var (
|
||||
ctx = context.Background()
|
||||
cw appsv1.Deployment
|
||||
component v1alpha2.Component
|
||||
traitDef v1alpha2.TraitDefinition
|
||||
appConfig v1alpha2.ApplicationConfiguration
|
||||
fakeTratiCRD crdv1.CustomResourceDefinition
|
||||
appConfigKey = client.ObjectKey{
|
||||
Name: appName,
|
||||
Namespace: namespace,
|
||||
}
|
||||
req = reconcile.Request{NamespacedName: appConfigKey}
|
||||
)
|
||||
|
||||
metataDataLabels := make(map[string]string)
|
||||
metataDataLabels["app"] = "wordpress"
|
||||
|
||||
var labelSelector = new(metav1.LabelSelector)
|
||||
labelSelector.MatchLabels = metataDataLabels
|
||||
|
||||
BeforeEach(func() {
|
||||
cw = appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: labelSelector,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Image: "wordpress:4.6.1-apache",
|
||||
Name: "wordpress",
|
||||
},
|
||||
},
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: metataDataLabels,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
component = v1alpha2.Component{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "core.oam.dev/v1alpha2",
|
||||
Kind: "Component",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: compName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.ComponentSpec{
|
||||
Workload: runtime.RawExtension{
|
||||
Object: &cw,
|
||||
},
|
||||
},
|
||||
}
|
||||
appConfig = v1alpha2.ApplicationConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: appName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
|
||||
By("Create namespace")
|
||||
ns := corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
Eventually(
|
||||
func() error {
|
||||
return k8sClient.Create(ctx, &ns)
|
||||
},
|
||||
time.Second*3, time.Millisecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
By("Create a CRD used as fake trait")
|
||||
fakeTratiCRD = crdv1.CustomResourceDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fakeTraitCRDName,
|
||||
Labels: map[string]string{"crd": namespace},
|
||||
},
|
||||
Spec: crdv1.CustomResourceDefinitionSpec{
|
||||
Group: fakeTraitGroup,
|
||||
Names: crdv1.CustomResourceDefinitionNames{
|
||||
Kind: fakeTraitKind,
|
||||
ListKind: "BarList",
|
||||
Plural: "bars",
|
||||
Singular: "bar",
|
||||
},
|
||||
Versions: []crdv1.CustomResourceDefinitionVersion{{
|
||||
Name: "v1",
|
||||
Served: true,
|
||||
Storage: true,
|
||||
Schema: &crdv1.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &crdv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]crdv1.JSONSchemaProps{
|
||||
"spec": {
|
||||
Type: "object",
|
||||
Properties: map[string]crdv1.JSONSchemaProps{
|
||||
"unchanged": {Type: "string"},
|
||||
"removed": {Type: "string"},
|
||||
"valueChanged": {Type: "string"},
|
||||
"added": {Type: "string"},
|
||||
}}}}}},
|
||||
},
|
||||
Scope: crdv1.NamespaceScoped,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.Background(), &fakeTratiCRD)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
By("Create Component")
|
||||
Expect(k8sClient.Create(ctx, &component)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
cmpV1 := &v1alpha2.Component{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: compName}, cmpV1)).Should(Succeed())
|
||||
|
||||
By("Creat TraitDefinition")
|
||||
traitDef = v1alpha2.TraitDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "core.oam.dev/v1alpha2",
|
||||
APIVersion: "TraitDefinition",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bars.example.com",
|
||||
Namespace: "vela-system",
|
||||
},
|
||||
Spec: v1alpha2.TraitDefinitionSpec{
|
||||
Reference: common.DefinitionReference{
|
||||
Name: fakeTraitCRDName,
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, &traitDef)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
By("Create an ApplicationConfiguration")
|
||||
appConfigYAML := `
|
||||
apiVersion: core.oam.dev/v1alpha2
|
||||
kind: ApplicationConfiguration
|
||||
metadata:
|
||||
name: example-app
|
||||
spec:
|
||||
components:
|
||||
- componentName: example-comp
|
||||
traits:
|
||||
- trait:
|
||||
apiVersion: example.com/v1
|
||||
kind: Bar
|
||||
metadata:
|
||||
labels:
|
||||
test.label: test
|
||||
spec:
|
||||
unchanged: bar
|
||||
removed: bar
|
||||
valueChanged: bar`
|
||||
Expect(yaml.Unmarshal([]byte(appConfigYAML), &appConfig)).Should(BeNil())
|
||||
|
||||
By("Creat appConfig & check trait is created")
|
||||
Expect(k8sClient.Create(ctx, &appConfig)).Should(Succeed())
|
||||
Eventually(func() int64 {
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
if err := k8sClient.Get(ctx, appConfigKey, &appConfig); err != nil {
|
||||
return 0
|
||||
}
|
||||
if appConfig.Status.Workloads == nil {
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
return 0
|
||||
}
|
||||
var traitObj unstructured.Unstructured
|
||||
traitName := appConfig.Status.Workloads[0].Traits[0].Reference.Name
|
||||
traitObj.SetAPIVersion("example.com/v1")
|
||||
traitObj.SetKind("Bar")
|
||||
if err := k8sClient.Get(ctx,
|
||||
client.ObjectKey{Namespace: namespace, Name: traitName}, &traitObj); err != nil {
|
||||
return 0
|
||||
}
|
||||
return traitObj.GetGeneration()
|
||||
}, 3*time.Second, 500*time.Millisecond).Should(Equal(int64(1)))
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(k8sClient.DeleteAllOf(ctx, &appConfig, client.InNamespace(namespace))).Should(Succeed())
|
||||
Expect(k8sClient.DeleteAllOf(ctx, &cw, client.InNamespace(namespace))).Should(Succeed())
|
||||
Expect(k8sClient.DeleteAllOf(ctx, &component, client.InNamespace(namespace))).Should(Succeed())
|
||||
var deleteTrait unstructured.Unstructured
|
||||
deleteTrait.SetAPIVersion("example.com/v1")
|
||||
deleteTrait.SetKind("Bar")
|
||||
Expect(k8sClient.DeleteAllOf(ctx, &deleteTrait, client.InNamespace(namespace))).Should(Succeed())
|
||||
})
|
||||
|
||||
When("update an ApplicationConfiguration with traits changed", func() {
|
||||
It("should apply all changes(add/reset/remove fields) to the trait", func() {
|
||||
By("Modify the ApplicationConfiguration")
|
||||
appConfigYAMLUpdated := `
|
||||
apiVersion: core.oam.dev/v1alpha2
|
||||
kind: ApplicationConfiguration
|
||||
metadata:
|
||||
name: example-app
|
||||
spec:
|
||||
components:
|
||||
- componentName: example-comp
|
||||
traits:
|
||||
- trait:
|
||||
apiVersion: example.com/v1
|
||||
kind: Bar
|
||||
spec:
|
||||
unchanged: bar
|
||||
valueChanged: foo
|
||||
added: bar`
|
||||
// remove metadata.labels
|
||||
// change a field (valueChanged: bar ==> foo)
|
||||
// added a field
|
||||
// removed a field
|
||||
appConfigUpdated := v1alpha2.ApplicationConfiguration{}
|
||||
Expect(yaml.Unmarshal([]byte(appConfigYAMLUpdated), &appConfigUpdated)).Should(BeNil())
|
||||
appConfigUpdated.SetNamespace(namespace)
|
||||
|
||||
By("Apply appConfig & check successfully")
|
||||
Expect(k8sClient.Patch(ctx, &appConfigUpdated, client.Merge)).Should(Succeed())
|
||||
Eventually(func() int64 {
|
||||
if err := k8sClient.Get(ctx, appConfigKey, &appConfig); err != nil {
|
||||
return 0
|
||||
}
|
||||
return appConfig.GetGeneration()
|
||||
}, time.Second, 300*time.Millisecond).Should(Equal(int64(2)))
|
||||
|
||||
By("Reconcile & check updated trait")
|
||||
var traitObj unstructured.Unstructured
|
||||
Eventually(func() int64 {
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
if err := k8sClient.Get(ctx, appConfigKey, &appConfig); err != nil {
|
||||
return 0
|
||||
}
|
||||
if appConfig.Status.Workloads == nil {
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
return 0
|
||||
}
|
||||
traitName := appConfig.Status.Workloads[0].Traits[0].Reference.Name
|
||||
traitObj.SetAPIVersion("example.com/v1")
|
||||
traitObj.SetKind("Bar")
|
||||
if err := k8sClient.Get(ctx,
|
||||
client.ObjectKey{Namespace: namespace, Name: traitName}, &traitObj); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// TODO(roywang) 2021/04/13 remove below 'By' if this case no longer breaks.
|
||||
v, _, _ := unstructured.NestedString(traitObj.UnstructuredContent(), "spec", "valueChanged")
|
||||
By(fmt.Sprintf(`trait field: want "foo", got %q`, v))
|
||||
|
||||
return traitObj.GetGeneration()
|
||||
}, 60*time.Second, time.Second).Should(Equal(int64(2)))
|
||||
|
||||
By("Check labels are removed")
|
||||
_, found, _ := unstructured.NestedString(traitObj.UnstructuredContent(), "metadata", "labels", "test.label")
|
||||
Expect(found).Should(Equal(false))
|
||||
By("Check unchanged field")
|
||||
v, _, _ := unstructured.NestedString(traitObj.UnstructuredContent(), "spec", "unchanged")
|
||||
Expect(v).Should(Equal("bar"))
|
||||
By("Check changed field")
|
||||
v, _, _ = unstructured.NestedString(traitObj.UnstructuredContent(), "spec", "valueChanged")
|
||||
Expect(v).Should(Equal("foo"))
|
||||
By("Check added field")
|
||||
v, _, _ = unstructured.NestedString(traitObj.UnstructuredContent(), "spec", "added")
|
||||
Expect(v).Should(Equal("bar"))
|
||||
By("Check removed field")
|
||||
_, found, _ = unstructured.NestedString(traitObj.UnstructuredContent(), "spec", "removed")
|
||||
Expect(found).Should(Equal(false))
|
||||
})
|
||||
})
|
||||
|
||||
// others means anything except AppConfig controller
|
||||
// e.g. trait controllers
|
||||
When("trait instance is changed by others", func() {
|
||||
// if others make changes on the fields managed by AppConfig controller
|
||||
// these changes will reverted but not retained.
|
||||
It("should retain changes by others, even after AppConfig spec is changed", func() {
|
||||
By("Get the trait newly created")
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, appConfigKey, &appConfig); err != nil {
|
||||
return ""
|
||||
}
|
||||
if appConfig.Status.Workloads == nil {
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
return ""
|
||||
}
|
||||
return appConfig.Status.Workloads[0].Traits[0].Reference.Name
|
||||
}, 5*time.Second, time.Second).ShouldNot(BeEmpty())
|
||||
traitName := appConfig.Status.Workloads[0].Traits[0].Reference.Name
|
||||
var traitObj unstructured.Unstructured
|
||||
traitObj.SetAPIVersion("example.com/v1")
|
||||
traitObj.SetKind("Bar")
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: traitName}, &traitObj)).Should(Succeed())
|
||||
|
||||
By("Others change the trait")
|
||||
// add a field
|
||||
unstructured.SetNestedField(traitObj.Object, "bar", "spec", "added")
|
||||
Expect(k8sClient.Patch(ctx, &traitObj, client.Merge)).Should(Succeed())
|
||||
|
||||
By("Check the change works")
|
||||
var changedTrait unstructured.Unstructured
|
||||
changedTrait.SetAPIVersion("example.com/v1")
|
||||
changedTrait.SetKind("Bar")
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: traitName}, &changedTrait)).Should(Succeed())
|
||||
v, _, _ := unstructured.NestedString(changedTrait.UnstructuredContent(), "spec", "added")
|
||||
Expect(v).Should(Equal("bar"))
|
||||
|
||||
By("Reconcile")
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
|
||||
By("Check others' change is retained")
|
||||
changedTrait = unstructured.Unstructured{}
|
||||
changedTrait.SetAPIVersion("example.com/v1")
|
||||
changedTrait.SetKind("Bar")
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: traitName}, &changedTrait)).Should(Succeed())
|
||||
v, _, _ = unstructured.NestedString(changedTrait.UnstructuredContent(), "spec", "added")
|
||||
Expect(v).Should(Equal("bar"))
|
||||
|
||||
By("Modify AppConfig without touching the field changed by others")
|
||||
appConfigYAMLUpdated := `
|
||||
apiVersion: core.oam.dev/v1alpha2
|
||||
kind: ApplicationConfiguration
|
||||
metadata:
|
||||
name: example-app
|
||||
spec:
|
||||
components:
|
||||
- componentName: example-comp
|
||||
traits:
|
||||
- trait:
|
||||
apiVersion: example.com/v1
|
||||
kind: Bar
|
||||
spec:
|
||||
unchanged: bar
|
||||
valueChanged: foo`
|
||||
appConfigUpdated := v1alpha2.ApplicationConfiguration{}
|
||||
Expect(yaml.Unmarshal([]byte(appConfigYAMLUpdated), &appConfigUpdated)).Should(BeNil())
|
||||
appConfigUpdated.SetNamespace(namespace)
|
||||
|
||||
By("Apply appConfig & check successfully")
|
||||
Expect(k8sClient.Patch(ctx, &appConfigUpdated, client.Merge)).Should(Succeed())
|
||||
Eventually(func() int64 {
|
||||
if err := k8sClient.Get(ctx, appConfigKey, &appConfig); err != nil {
|
||||
return 0
|
||||
}
|
||||
return appConfig.GetGeneration()
|
||||
}, time.Second, 300*time.Millisecond).Should(Equal(int64(2)))
|
||||
|
||||
Eventually(func() int64 {
|
||||
By("Reconcile")
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
changedTrait = unstructured.Unstructured{}
|
||||
changedTrait.SetAPIVersion("example.com/v1")
|
||||
changedTrait.SetKind("Bar")
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: traitName}, &changedTrait); err != nil {
|
||||
return 0
|
||||
}
|
||||
return changedTrait.GetGeneration()
|
||||
}, 5*time.Second, time.Second).Should(Equal(int64(3)))
|
||||
|
||||
By("Check AppConfig's change works")
|
||||
// changed a field
|
||||
v, _, _ = unstructured.NestedString(changedTrait.UnstructuredContent(), "spec", "valueChanged")
|
||||
Expect(v).Should(Equal("foo"))
|
||||
// removed a field
|
||||
_, found, _ := unstructured.NestedString(changedTrait.UnstructuredContent(), "spec", "removed")
|
||||
Expect(found).Should(Equal(false))
|
||||
|
||||
By("Check others' change is still retained")
|
||||
v, _, _ = unstructured.NestedString(changedTrait.UnstructuredContent(), "spec", "added")
|
||||
Expect(v).Should(Equal("bar"))
|
||||
})
|
||||
|
||||
It("should override others' changes on fields rendered from AppConfig", func() {
|
||||
By("Get the trait newly created")
|
||||
Eventually(func() string {
|
||||
if err := k8sClient.Get(ctx, appConfigKey, &appConfig); err != nil {
|
||||
return ""
|
||||
}
|
||||
if appConfig.Status.Workloads == nil {
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
return ""
|
||||
}
|
||||
return appConfig.Status.Workloads[0].Traits[0].Reference.Name
|
||||
}, 5*time.Second, time.Second).ShouldNot(BeEmpty())
|
||||
traitName := appConfig.Status.Workloads[0].Traits[0].Reference.Name
|
||||
var traitObj unstructured.Unstructured
|
||||
traitObj.SetAPIVersion("example.com/v1")
|
||||
traitObj.SetKind("Bar")
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: traitName}, &traitObj)).Should(Succeed())
|
||||
|
||||
By("Others change the field which should be rendered from AppConfig")
|
||||
// unchanged: bar ==> foo
|
||||
unstructured.SetNestedField(traitObj.Object, "foo", "spec", "unchanged")
|
||||
Expect(k8sClient.Patch(ctx, &traitObj, client.Merge)).Should(Succeed())
|
||||
|
||||
By("Check the change works")
|
||||
var changedTrait unstructured.Unstructured
|
||||
changedTrait.SetAPIVersion("example.com/v1")
|
||||
changedTrait.SetKind("Bar")
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: traitName}, &changedTrait)).Should(Succeed())
|
||||
v, _, _ := unstructured.NestedString(changedTrait.UnstructuredContent(), "spec", "unchanged")
|
||||
Expect(v).Should(Equal("foo"))
|
||||
|
||||
By("Reconcile")
|
||||
testutil.ReconcileRetry(reconciler, req)
|
||||
|
||||
By("Check others' change is overrided(reset)")
|
||||
changedTrait = unstructured.Unstructured{}
|
||||
changedTrait.SetAPIVersion("example.com/v1")
|
||||
changedTrait.SetKind("Bar")
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: traitName}, &changedTrait)).Should(Succeed())
|
||||
v, _, _ = unstructured.NestedString(changedTrait.UnstructuredContent(), "spec", "unchanged")
|
||||
// unchanged: foo ==> bar
|
||||
Expect(v).Should(Equal("bar"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,314 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"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/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
pkgutil "github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
// ControllerRevisionComponentLabel indicate which component the revision belong to
|
||||
// This label is to filter revision by client api
|
||||
const ControllerRevisionComponentLabel = oam.LabelControllerRevisionComponent
|
||||
|
||||
// ComponentHandler will watch component change and generate Revision automatically.
|
||||
type ComponentHandler struct {
|
||||
Client client.Client
|
||||
RevisionLimit int
|
||||
CustomRevisionHookURL string
|
||||
}
|
||||
|
||||
// Create implements EventHandler
|
||||
func (c *ComponentHandler) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
|
||||
reqs, succeed := c.createControllerRevision(evt.Object, evt.Object)
|
||||
if !succeed {
|
||||
// No revision created, return
|
||||
return
|
||||
}
|
||||
for _, req := range reqs {
|
||||
q.Add(req)
|
||||
}
|
||||
}
|
||||
|
||||
// Update implements EventHandler
|
||||
func (c *ComponentHandler) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
|
||||
reqs, succeed := c.createControllerRevision(evt.ObjectNew, evt.ObjectNew)
|
||||
if !succeed {
|
||||
// No revision created, return
|
||||
return
|
||||
}
|
||||
// Note(wonderflow): MetaOld => MetaNew, requeue once is enough
|
||||
for _, req := range reqs {
|
||||
q.Add(req)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete implements EventHandler
|
||||
func (c *ComponentHandler) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
|
||||
// controllerRevision will be deleted by ownerReference mechanism
|
||||
// so we don't need to delete controllerRevision here.
|
||||
// but trigger an event to AppConfig controller, let it know.
|
||||
for _, req := range c.getRelatedAppConfig(evt.Object) {
|
||||
q.Add(req)
|
||||
}
|
||||
}
|
||||
|
||||
// Generic implements EventHandler
|
||||
func (c *ComponentHandler) Generic(_ event.GenericEvent, _ workqueue.RateLimitingInterface) {
|
||||
// Generic is called in response to an event of an unknown type or a synthetic event triggered as a cron or
|
||||
// external trigger request - e.g. reconcile Autoscaling, or a Webhook.
|
||||
// so we need to do nothing here.
|
||||
}
|
||||
|
||||
func isMatch(appConfigs *v1alpha2.ApplicationConfigurationList, compName string) (bool, types.NamespacedName) {
|
||||
for _, app := range appConfigs.Items {
|
||||
for _, comp := range app.Spec.Components {
|
||||
if comp.ComponentName == compName || utils.ExtractComponentName(comp.RevisionName) == compName {
|
||||
return true, types.NamespacedName{Namespace: app.Namespace, Name: app.Name}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, types.NamespacedName{}
|
||||
}
|
||||
|
||||
func (c *ComponentHandler) getRelatedAppConfig(object metav1.Object) []reconcile.Request {
|
||||
var appConfigs v1alpha2.ApplicationConfigurationList
|
||||
err := c.Client.List(context.Background(), &appConfigs)
|
||||
if err != nil {
|
||||
klog.Info(fmt.Sprintf("error list all applicationConfigurations %v", err))
|
||||
return nil
|
||||
}
|
||||
var reqs []reconcile.Request
|
||||
if match, namespaceName := isMatch(&appConfigs, object.GetName()); match {
|
||||
reqs = append(reqs, reconcile.Request{NamespacedName: namespaceName})
|
||||
}
|
||||
return reqs
|
||||
}
|
||||
|
||||
// IsRevisionDiff check whether there's any different between two component revision
|
||||
func (c *ComponentHandler) IsRevisionDiff(mt klog.KMetadata, curComp *v1alpha2.Component) (bool, int64) {
|
||||
if curComp.Status.LatestRevision == nil {
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// client in controller-runtime will use informer cache
|
||||
// use client will be more efficient
|
||||
needNewRevision, err := utils.CompareWithRevision(context.TODO(), c.Client, mt.GetName(), mt.GetNamespace(),
|
||||
curComp.Status.LatestRevision.Name, &curComp.Spec)
|
||||
// TODO: this might be a bug that we treat all errors getting from k8s as a new revision
|
||||
// but the client go event handler doesn't handle an error. We need to see if we can retry this
|
||||
if err != nil {
|
||||
klog.InfoS(fmt.Sprintf("Failed to compare the component with its latest revision with err = %+v", err),
|
||||
"component", mt.GetName(), "latest revision", curComp.Status.LatestRevision.Name)
|
||||
return true, curComp.Status.LatestRevision.Revision
|
||||
}
|
||||
return needNewRevision, curComp.Status.LatestRevision.Revision
|
||||
}
|
||||
|
||||
func (c *ComponentHandler) createControllerRevision(mt metav1.Object, obj client.Object) ([]reconcile.Request, bool) {
|
||||
curComp := obj.(*v1alpha2.Component)
|
||||
comp := curComp.DeepCopy()
|
||||
// No generation changed, will not create revision
|
||||
if comp.Generation == comp.Status.ObservedGeneration {
|
||||
return nil, false
|
||||
}
|
||||
diff, curRevision := c.IsRevisionDiff(mt, comp)
|
||||
if !diff {
|
||||
// No difference, no need to create new revision.
|
||||
return nil, false
|
||||
}
|
||||
|
||||
reqs := c.getRelatedAppConfig(mt)
|
||||
// Hook to custom revision service if exist
|
||||
if err := c.customComponentRevisionHook(reqs, comp); err != nil {
|
||||
klog.InfoS(fmt.Sprintf("fail to hook from custom revision service(%s) %v", c.CustomRevisionHookURL, err), "componentName", mt.GetName())
|
||||
return nil, false
|
||||
}
|
||||
|
||||
nextRevision := curRevision + 1
|
||||
revisionName := utils.ConstructRevisionName(mt.GetName(), nextRevision)
|
||||
|
||||
if comp.Status.ObservedGeneration != comp.Generation {
|
||||
comp.Status.ObservedGeneration = comp.Generation
|
||||
}
|
||||
|
||||
comp.Status.LatestRevision = &common.Revision{
|
||||
Name: revisionName,
|
||||
Revision: nextRevision,
|
||||
}
|
||||
compRaw, err := json.Marshal(comp)
|
||||
if err != nil {
|
||||
klog.InfoS(fmt.Sprintf("json.Marshal failed: %v", err), "componentName", mt.GetName())
|
||||
return nil, false
|
||||
}
|
||||
// set annotation to component
|
||||
revision := &appsv1.ControllerRevision{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: revisionName,
|
||||
Namespace: comp.Namespace,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: v1alpha2.SchemeGroupVersion.String(),
|
||||
Kind: v1alpha2.ComponentKind,
|
||||
Name: comp.Name,
|
||||
UID: comp.UID,
|
||||
Controller: pointer.Bool(true),
|
||||
},
|
||||
},
|
||||
Labels: map[string]string{
|
||||
ControllerRevisionComponentLabel: pkgutil.EscapeResourceNameToLabelValue(comp.Name),
|
||||
},
|
||||
},
|
||||
Revision: nextRevision,
|
||||
Data: runtime.RawExtension{Raw: compRaw},
|
||||
}
|
||||
|
||||
// TODO: we should update the status first. otherwise, the subsequent create will all fail if the update fails
|
||||
err = c.Client.Create(context.TODO(), revision)
|
||||
if err != nil {
|
||||
klog.InfoS(fmt.Sprintf("error create controllerRevision %v", err), "componentName", mt.GetName())
|
||||
return nil, false
|
||||
}
|
||||
|
||||
err = c.UpdateStatus(context.Background(), comp)
|
||||
if err != nil {
|
||||
klog.InfoS(fmt.Sprintf("update component status latestRevision %s err %v", revisionName, err), "componentName", mt.GetName())
|
||||
return nil, false
|
||||
}
|
||||
|
||||
klog.InfoS("Create ControllerRevision", "name", revisionName)
|
||||
// garbage collect
|
||||
if int64(c.RevisionLimit) < nextRevision {
|
||||
if err := c.cleanupControllerRevision(comp); err != nil {
|
||||
klog.Info(fmt.Sprintf("failed to clean up revisions of Component %v.", err))
|
||||
}
|
||||
}
|
||||
return reqs, true
|
||||
}
|
||||
|
||||
// get sorted controllerRevisions, prepare to delete controllerRevisions
|
||||
func sortedControllerRevision(appConfigs []v1alpha2.ApplicationConfiguration, revisions []appsv1.ControllerRevision,
|
||||
revisionLimit int) (sortedRevisions []appsv1.ControllerRevision, toKill int, liveHashes map[string]bool) {
|
||||
liveHashes = make(map[string]bool)
|
||||
sortedRevisions = revisions
|
||||
|
||||
// get all revisions used and skipped
|
||||
for _, appConfig := range appConfigs {
|
||||
for _, component := range appConfig.Spec.Components {
|
||||
if component.RevisionName != "" {
|
||||
liveHashes[component.RevisionName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toKeep := revisionLimit + len(liveHashes)
|
||||
toKill = len(sortedRevisions) - toKeep
|
||||
if toKill <= 0 {
|
||||
toKill = 0
|
||||
return
|
||||
}
|
||||
// Clean up old revisions from smallest to highest revision (from oldest to newest)
|
||||
sort.Sort(historiesByRevision(sortedRevisions))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// clean revisions when over limits
|
||||
func (c *ComponentHandler) cleanupControllerRevision(curComp *v1alpha2.Component) error {
|
||||
labels := &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
ControllerRevisionComponentLabel: pkgutil.EscapeResourceNameToLabelValue(curComp.Name),
|
||||
},
|
||||
}
|
||||
selector, err := metav1.LabelSelectorAsSelector(labels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// List and Get Object, controller-runtime will create Informer cache
|
||||
// and will get objects from cache
|
||||
revisions := &appsv1.ControllerRevisionList{}
|
||||
if err := c.Client.List(context.TODO(), revisions, &client.ListOptions{LabelSelector: selector}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get appConfigs and workloads filter controllerRevision used
|
||||
appConfigs := &v1alpha2.ApplicationConfigurationList{}
|
||||
if err := c.Client.List(context.Background(), appConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get sorted revisions
|
||||
controllerRevisions, toKill, liveHashes := sortedControllerRevision(appConfigs.Items, revisions.Items, c.RevisionLimit)
|
||||
for _, revision := range controllerRevisions {
|
||||
if toKill <= 0 {
|
||||
break
|
||||
}
|
||||
if hash := revision.GetName(); liveHashes[hash] {
|
||||
continue
|
||||
}
|
||||
// Clean up
|
||||
revisionToClean := revision
|
||||
if err := c.Client.Delete(context.TODO(), &revisionToClean); err != nil {
|
||||
return err
|
||||
}
|
||||
klog.InfoS("Delete controllerRevision", "name", revision.Name)
|
||||
toKill--
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStatus updates v1alpha2.Component's Status with retry.RetryOnConflict
|
||||
func (c *ComponentHandler) UpdateStatus(ctx context.Context, comp *v1alpha2.Component, opts ...client.SubResourceUpdateOption) error {
|
||||
status := comp.DeepCopy().Status
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
if err = c.Client.Get(ctx, types.NamespacedName{Namespace: comp.Namespace, Name: comp.Name}, comp); err != nil {
|
||||
return
|
||||
}
|
||||
comp.Status = status
|
||||
return c.Client.Status().Update(ctx, comp, opts...)
|
||||
})
|
||||
}
|
||||
|
||||
// historiesByRevision sort controllerRevision by revision
|
||||
type historiesByRevision []appsv1.ControllerRevision
|
||||
|
||||
func (h historiesByRevision) Len() int { return len(h) }
|
||||
func (h historiesByRevision) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||
func (h historiesByRevision) Less(i, j int) bool {
|
||||
return h[i].Revision < h[j].Revision
|
||||
}
|
||||
-72
@@ -1,72 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
)
|
||||
|
||||
// RevisionHookRequest is request body for custom component revision hook
|
||||
type RevisionHookRequest struct {
|
||||
RelatedApps []reconcile.Request `json:"relatedApps"`
|
||||
Comp *v1alpha2.Component `json:"component"`
|
||||
}
|
||||
|
||||
// ContentTypeJSON : json
|
||||
const ContentTypeJSON = "application/json"
|
||||
|
||||
func (c *ComponentHandler) customComponentRevisionHook(relatedApps []reconcile.Request, comp *v1alpha2.Component) error {
|
||||
if c.CustomRevisionHookURL == "" {
|
||||
return nil
|
||||
}
|
||||
req := RevisionHookRequest{
|
||||
RelatedApps: relatedApps,
|
||||
Comp: comp.DeepCopy(),
|
||||
}
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpRequest, err := http.NewRequestWithContext(context.Background(), http.MethodPost, c.CustomRevisionHookURL, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpRequest.Header.Set("Content-Type", ContentTypeJSON)
|
||||
resp, err := http.DefaultClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//nolint:errcheck
|
||||
defer resp.Body.Close()
|
||||
respData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("httpcode(%d) err: %s", resp.StatusCode, string(respData))
|
||||
}
|
||||
return json.Unmarshal(respData, comp)
|
||||
}
|
||||
-91
@@ -1,91 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
)
|
||||
|
||||
var RevisionHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req RevisionHookRequest
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(data, &req)
|
||||
if err != nil {
|
||||
w.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
fmt.Println("got request from", req.Comp.Name)
|
||||
|
||||
if len(req.RelatedApps) != 1 {
|
||||
var abc []string
|
||||
for _, v := range req.RelatedApps {
|
||||
abc = append(abc, v.Name)
|
||||
}
|
||||
// we can add a check here for real world handler
|
||||
fmt.Printf("we should have only one relatedApps, but now %d: %s\n", len(req.RelatedApps), strings.Join(abc, ", "))
|
||||
}
|
||||
if req.Comp.Annotations == nil {
|
||||
req.Comp.Annotations = make(map[string]string)
|
||||
}
|
||||
if len(req.RelatedApps) > 0 {
|
||||
req.Comp.Annotations["app-name"] = req.RelatedApps[0].Name
|
||||
req.Comp.Annotations["app-namespace"] = req.RelatedApps[0].Namespace
|
||||
}
|
||||
a := &unstructured.Unstructured{}
|
||||
_ = json.Unmarshal(req.Comp.Spec.Workload.Raw, a)
|
||||
a.SetAnnotations(map[string]string{"time": time.Now().Format(time.RFC3339Nano)})
|
||||
data, _ = json.Marshal(a)
|
||||
req.Comp.Spec.Workload.Raw = data
|
||||
newdata, err := json.Marshal(req.Comp)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.Write(newdata)
|
||||
})
|
||||
|
||||
func TestCustomRevisionHook(t *testing.T) {
|
||||
srv := httptest.NewServer(RevisionHandler)
|
||||
defer srv.Close()
|
||||
compHandler := ComponentHandler{
|
||||
CustomRevisionHookURL: srv.URL,
|
||||
}
|
||||
comp := &v1alpha2.Component{}
|
||||
err := compHandler.customComponentRevisionHook([]reconcile.Request{{NamespacedName: types.NamespacedName{Name: "app1", Namespace: "default1"}}}, comp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "app1", comp.Annotations["app-name"])
|
||||
assert.Equal(t, "default1", comp.Annotations["app-namespace"])
|
||||
}
|
||||
@@ -1,315 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v12 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllertest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
)
|
||||
|
||||
func TestComponentHandler(t *testing.T) {
|
||||
q := controllertest.Queue{Interface: workqueue.New()}
|
||||
var curComp = &v1alpha2.Component{
|
||||
ObjectMeta: metav1.ObjectMeta{Generation: 1},
|
||||
}
|
||||
var createdRevisions = []appsv1.ControllerRevision{}
|
||||
var instance = ComponentHandler{
|
||||
Client: &test.MockClient{
|
||||
MockList: test.NewMockListFn(nil, func(obj client.ObjectList) error {
|
||||
switch robj := obj.(type) {
|
||||
case *v1alpha2.ApplicationConfigurationList:
|
||||
lists := v1alpha2.ApplicationConfigurationList{
|
||||
Items: []v1alpha2.ApplicationConfiguration{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app1",
|
||||
},
|
||||
Spec: v1alpha2.ApplicationConfigurationSpec{
|
||||
Components: []v1alpha2.ApplicationConfigurationComponent{{
|
||||
ComponentName: "comp1",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
lists.DeepCopyInto(robj)
|
||||
return nil
|
||||
case *appsv1.ControllerRevisionList:
|
||||
lists := appsv1.ControllerRevisionList{
|
||||
Items: createdRevisions,
|
||||
}
|
||||
lists.DeepCopyInto(robj)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
MockStatusUpdate: test.NewMockSubResourceUpdateFn(nil, func(obj client.Object) error {
|
||||
switch robj := obj.(type) {
|
||||
case *v1alpha2.Component:
|
||||
robj.DeepCopyInto(curComp)
|
||||
case *appsv1.ControllerRevision:
|
||||
for _, revision := range createdRevisions {
|
||||
if revision.Name == robj.Name {
|
||||
robj.DeepCopyInto(&revision)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
MockCreate: test.NewMockCreateFn(nil, func(obj client.Object) error {
|
||||
cur, ok := obj.(*appsv1.ControllerRevision)
|
||||
if ok {
|
||||
createdRevisions = append(createdRevisions, *cur)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
switch robj := obj.(type) {
|
||||
case *appsv1.ControllerRevision:
|
||||
if len(createdRevisions) == 0 {
|
||||
return nil
|
||||
}
|
||||
// test frame can't get the key, and just return the newest revision
|
||||
createdRevisions[len(createdRevisions)-1].DeepCopyInto(robj)
|
||||
case *v1alpha2.Component:
|
||||
robj.DeepCopyInto(curComp)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
MockDelete: test.NewMockDeleteFn(nil, func(obj client.Object) error {
|
||||
if robj, ok := obj.(*appsv1.ControllerRevision); ok {
|
||||
newRevisions := []appsv1.ControllerRevision{}
|
||||
for _, revision := range createdRevisions {
|
||||
if revision.Name == robj.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
newRevisions = append(newRevisions, revision)
|
||||
}
|
||||
createdRevisions = newRevisions
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
RevisionLimit: 2,
|
||||
}
|
||||
comp := &v1alpha2.Component{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "biz", Name: "comp1", Generation: 1},
|
||||
Spec: v1alpha2.ComponentSpec{Workload: runtime.RawExtension{Object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: v12.PodTemplateSpec{Spec: v12.PodSpec{Containers: []v12.Container{{Image: "nginx:v1"}}}}}}}},
|
||||
}
|
||||
|
||||
// ============ Test Create Event Start ===================
|
||||
evt := event.CreateEvent{
|
||||
Object: comp,
|
||||
}
|
||||
instance.Create(evt, q)
|
||||
if q.Len() != 1 {
|
||||
t.Fatal("no event created, but suppose have one")
|
||||
}
|
||||
item, _ := q.Get()
|
||||
req := item.(reconcile.Request)
|
||||
// AppConfig event triggered, and compare revision created
|
||||
assert.Equal(t, req.Name, "app1")
|
||||
revisions := &appsv1.ControllerRevisionList{}
|
||||
err := instance.Client.List(context.TODO(), revisions)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(revisions.Items))
|
||||
assert.Equal(t, true, strings.HasPrefix(revisions.Items[0].Name, "comp1-"))
|
||||
var gotComp v1alpha2.Component
|
||||
_ = json.Unmarshal(revisions.Items[0].Data.Raw, &gotComp)
|
||||
var gotDeploy appsv1.Deployment
|
||||
_ = json.Unmarshal(gotComp.Spec.Workload.Raw, &gotDeploy)
|
||||
gotComp.Spec.Workload.Object = &gotDeploy
|
||||
gotComp.Spec.Workload.Raw = nil
|
||||
// check component's spec saved in corresponding controllerRevision
|
||||
assert.Equal(t, comp.Spec, gotComp.Spec)
|
||||
// check component's status saved in corresponding controllerRevision
|
||||
assert.Equal(t, gotComp.Status.LatestRevision.Name, revisions.Items[0].Name)
|
||||
assert.Equal(t, gotComp.Status.LatestRevision.Revision, revisions.Items[0].Revision)
|
||||
// check component's status AppliedGeneration
|
||||
assert.Equal(t, gotComp.Status.ObservedGeneration, comp.Generation)
|
||||
q.Done(item)
|
||||
// ============ Test Create Event End ===================
|
||||
|
||||
// ============ Test Update Event Start===================
|
||||
comp2 := &v1alpha2.Component{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "biz", Name: "comp1"},
|
||||
// change image
|
||||
Spec: v1alpha2.ComponentSpec{Workload: runtime.RawExtension{Object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: v12.PodTemplateSpec{Spec: v12.PodSpec{Containers: []v12.Container{{Image: "nginx:v2"}}}}}}}},
|
||||
}
|
||||
curComp.Status.DeepCopyInto(&comp2.Status)
|
||||
updateEvt := event.UpdateEvent{
|
||||
ObjectOld: comp,
|
||||
ObjectNew: comp2,
|
||||
}
|
||||
instance.Update(updateEvt, q)
|
||||
if q.Len() != 1 {
|
||||
t.Fatal("no event created, but suppose have one")
|
||||
}
|
||||
item, _ = q.Get()
|
||||
req = item.(reconcile.Request)
|
||||
// AppConfig event triggered, and compare revision created
|
||||
assert.Equal(t, req.Name, "app1")
|
||||
revisions = &appsv1.ControllerRevisionList{}
|
||||
err = instance.Client.List(context.TODO(), revisions)
|
||||
assert.NoError(t, err)
|
||||
// Component changed, we have two revision now.
|
||||
assert.Equal(t, 2, len(revisions.Items))
|
||||
for _, v := range revisions.Items {
|
||||
assert.Equal(t, true, strings.HasPrefix(v.Name, "comp1-"))
|
||||
if v.Revision == 2 {
|
||||
var gotComp v1alpha2.Component
|
||||
_ = json.Unmarshal(v.Data.Raw, &gotComp)
|
||||
var gotDeploy appsv1.Deployment
|
||||
_ = json.Unmarshal(gotComp.Spec.Workload.Raw, &gotDeploy)
|
||||
gotComp.Spec.Workload.Object = &gotDeploy
|
||||
gotComp.Spec.Workload.Raw = nil
|
||||
// check component's spec saved in corresponding controllerRevision
|
||||
assert.Equal(t, comp2.Spec, gotComp.Spec)
|
||||
// check component's status saved in corresponding controllerRevision
|
||||
assert.Equal(t, gotComp.Status.LatestRevision.Name, v.Name)
|
||||
assert.Equal(t, gotComp.Status.LatestRevision.Revision, v.Revision)
|
||||
}
|
||||
}
|
||||
q.Done(item)
|
||||
// test no changes with component spec
|
||||
comp3 := &v1alpha2.Component{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "biz", Name: "comp1", Labels: map[string]string{"bar": "foo"}},
|
||||
Spec: v1alpha2.ComponentSpec{Workload: runtime.RawExtension{Object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: v12.PodTemplateSpec{Spec: v12.PodSpec{Containers: []v12.Container{{Image: "nginx:v2"}}}}}}}},
|
||||
}
|
||||
curComp.Status.DeepCopyInto(&comp3.Status)
|
||||
updateEvt = event.UpdateEvent{
|
||||
ObjectOld: comp2,
|
||||
ObjectNew: comp3,
|
||||
}
|
||||
instance.Update(updateEvt, q)
|
||||
if q.Len() != 0 {
|
||||
t.Fatal("should not trigger event with nothing changed no change")
|
||||
}
|
||||
// ============ Test Update Event End ===================
|
||||
|
||||
// ============ Test Revisions Start ===================
|
||||
// test clean revision
|
||||
comp4 := &v1alpha2.Component{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "biz", Name: "comp1", Labels: map[string]string{"bar": "foo"}},
|
||||
Spec: v1alpha2.ComponentSpec{Workload: runtime.RawExtension{Object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: v12.PodTemplateSpec{Spec: v12.PodSpec{Containers: []v12.Container{{Image: "nginx:v3"}}}}}}}},
|
||||
}
|
||||
curComp.Status.DeepCopyInto(&comp4.Status)
|
||||
updateEvt = event.UpdateEvent{
|
||||
ObjectOld: comp2,
|
||||
ObjectNew: comp4,
|
||||
}
|
||||
instance.Update(updateEvt, q)
|
||||
revisions = &appsv1.ControllerRevisionList{}
|
||||
err = instance.Client.List(context.TODO(), revisions)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(revisions.Items), "Expected has two revisions")
|
||||
assert.Equal(t, "comp1", revisions.Items[0].Labels[ControllerRevisionComponentLabel],
|
||||
fmt.Sprintf("Expected revision has label %s: comp1", ControllerRevisionComponentLabel))
|
||||
// ============ Test Revisions End ===================
|
||||
}
|
||||
|
||||
func TestIsMatch(t *testing.T) {
|
||||
var appConfigs v1alpha2.ApplicationConfigurationList
|
||||
appConfigs.Items = []v1alpha2.ApplicationConfiguration{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo-app", Namespace: "foo-namespace"},
|
||||
Spec: v1alpha2.ApplicationConfigurationSpec{
|
||||
Components: []v1alpha2.ApplicationConfigurationComponent{{ComponentName: "foo"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "bar-app", Namespace: "bar-namespace"},
|
||||
Spec: v1alpha2.ApplicationConfigurationSpec{
|
||||
Components: []v1alpha2.ApplicationConfigurationComponent{{ComponentName: "bar"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
got, namespaceNamed := isMatch(&appConfigs, "foo")
|
||||
assert.Equal(t, true, got)
|
||||
assert.Equal(t, types.NamespacedName{Name: "foo-app", Namespace: "foo-namespace"}, namespaceNamed)
|
||||
got, _ = isMatch(&appConfigs, "foo1")
|
||||
assert.Equal(t, false, got)
|
||||
got, namespaceNamed = isMatch(&appConfigs, "bar")
|
||||
assert.Equal(t, true, got)
|
||||
assert.Equal(t, types.NamespacedName{Name: "bar-app", Namespace: "bar-namespace"}, namespaceNamed)
|
||||
appConfigs.Items = nil
|
||||
got, _ = isMatch(&appConfigs, "foo")
|
||||
assert.Equal(t, false, got)
|
||||
}
|
||||
|
||||
func TestSortedControllerRevision(t *testing.T) {
|
||||
appconfigs := []v1alpha2.ApplicationConfiguration{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo-app", Namespace: "test"},
|
||||
Spec: v1alpha2.ApplicationConfigurationSpec{
|
||||
Components: []v1alpha2.ApplicationConfigurationComponent{{ComponentName: "foo", RevisionName: "revision1"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
emptyAppconfigs := []v1alpha2.ApplicationConfiguration{}
|
||||
revision1 := appsv1.ControllerRevision{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "revision1", Namespace: "foo-namespace"},
|
||||
Revision: 3,
|
||||
}
|
||||
revision2 := appsv1.ControllerRevision{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "revision2", Namespace: "foo-namespace"},
|
||||
Revision: 1,
|
||||
}
|
||||
revision3 := appsv1.ControllerRevision{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "revision2", Namespace: "foo-namespace"},
|
||||
Revision: 2,
|
||||
}
|
||||
revisions := []appsv1.ControllerRevision{
|
||||
revision1,
|
||||
revision2,
|
||||
revision3,
|
||||
}
|
||||
expectedRevison := []appsv1.ControllerRevision{
|
||||
revision2,
|
||||
revision3,
|
||||
revision1,
|
||||
}
|
||||
|
||||
_, toKill, _ := sortedControllerRevision(appconfigs, revisions, 3)
|
||||
assert.Equal(t, 0, toKill, "Not over limit, needn't to delete")
|
||||
|
||||
sortedRevisions, toKill, _ := sortedControllerRevision(emptyAppconfigs, revisions, 2)
|
||||
assert.Equal(t, expectedRevison, sortedRevisions, "Export controllerRevision sorted ascending accord to revision")
|
||||
assert.Equal(t, 1, toKill, "Over limit")
|
||||
|
||||
_, toKill, liveHashes := sortedControllerRevision(appconfigs, revisions, 2)
|
||||
assert.Equal(t, 0, toKill, "Needn't to delete")
|
||||
assert.Equal(t, 1, len(liveHashes), "LiveHashes worked")
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Crossplane 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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
)
|
||||
|
||||
// A ControllerHooks provide customized reconcile logic for an ApplicationConfiguration
|
||||
type ControllerHooks interface {
|
||||
Exec(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) (reconcile.Result, error)
|
||||
}
|
||||
|
||||
// ControllerHooksFn reconciles an ApplicationConfiguration
|
||||
type ControllerHooksFn func(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) (reconcile.Result, error)
|
||||
|
||||
// Exec the customized reconcile logic on the ApplicationConfiguration
|
||||
func (fn ControllerHooksFn) Exec(ctx context.Context, ac *v1alpha2.ApplicationConfiguration) (reconcile.Result, error) {
|
||||
return fn(ctx, ac)
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Crossplane 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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
)
|
||||
|
||||
// dag is the dependency graph for an AppConfig.
|
||||
type dag struct {
|
||||
Sources map[string]*dagSource
|
||||
}
|
||||
|
||||
// dagSource represents the object information with DataOutput
|
||||
type dagSource struct {
|
||||
// ObjectRef refers to the object this source come from.
|
||||
ObjectRef *corev1.ObjectReference
|
||||
|
||||
Conditions []v1alpha2.ConditionRequirement
|
||||
}
|
||||
|
||||
// newDAG creates a fresh new DAG.
|
||||
func newDAG() *dag {
|
||||
return &dag{
|
||||
Sources: make(map[string]*dagSource),
|
||||
}
|
||||
}
|
||||
|
||||
// AddSource adds a data output source into the DAG.
|
||||
func (d *dag) AddSource(sourceName string, ref *corev1.ObjectReference, m []v1alpha2.ConditionRequirement) {
|
||||
d.Sources[sourceName] = &dagSource{
|
||||
ObjectRef: ref,
|
||||
Conditions: m,
|
||||
}
|
||||
}
|
||||
|
||||
func fillDataInputValue(obj *unstructured.Unstructured, fs []string, val interface{}, strategyMergeKeys []string) error {
|
||||
paved := fieldpath.Pave(obj.Object)
|
||||
for _, fp := range fs {
|
||||
toSet := val
|
||||
|
||||
// Special case for slice because we will append or strategyMerge instead of rewriting.
|
||||
if reflect.TypeOf(val).Kind() == reflect.Slice {
|
||||
raw, err := paved.GetValue(fp)
|
||||
if err != nil {
|
||||
if fieldpath.IsNotFound(err) {
|
||||
raw = make([]interface{}, 0)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
l := raw.([]interface{})
|
||||
toSet = strategyMergeSlice(l, val.([]interface{}), strategyMergeKeys)
|
||||
}
|
||||
|
||||
err := paved.SetValue(fp, toSet)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "paved.SetValue() failed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getElementValueByKeys(ele interface{}, keys []string) map[string]string {
|
||||
mappedEle, ok := ele.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
pavedEle := fieldpath.Pave(mappedEle)
|
||||
var result = make(map[string]string)
|
||||
for _, key := range keys {
|
||||
keyValuePatch, err := pavedEle.GetString(key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result[key] = keyValuePatch
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func compareResults(base, patch map[string]string) bool {
|
||||
for k, v := range patch {
|
||||
vv, ok := base[k]
|
||||
if ok && vv == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func strategyMergeSlice(base, patch []interface{}, keys []string) []interface{} {
|
||||
if len(keys) == 0 {
|
||||
// By default, no merge keys, append only.
|
||||
base = append(base, patch...)
|
||||
return base
|
||||
}
|
||||
for _, patchElement := range patch {
|
||||
// get all values by mergeKeys from patch element
|
||||
patchKeyResults := getElementValueByKeys(patchElement, keys)
|
||||
if len(patchKeyResults) == 0 {
|
||||
base = append(base, patchElement)
|
||||
continue
|
||||
}
|
||||
var match = false
|
||||
for idx, v := range base {
|
||||
// get all values by mergeKeys from base element
|
||||
baseKeyResults := getElementValueByKeys(v, keys)
|
||||
// compare the key value pairs
|
||||
match = compareResults(baseKeyResults, patchKeyResults)
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
base[idx] = patchElement
|
||||
break
|
||||
}
|
||||
if match {
|
||||
continue
|
||||
}
|
||||
// if no matches, append at last
|
||||
base = append(base, patchElement)
|
||||
}
|
||||
return base
|
||||
}
|
||||
@@ -1,199 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func TestFillDatainput(t *testing.T) {
|
||||
getObj1 := func() *unstructured.Unstructured {
|
||||
return &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"a": []interface{}{map[string]interface{}{
|
||||
"configMapRef": map[string]interface{}{
|
||||
"name": "my-a",
|
||||
"value": "my-a",
|
||||
},
|
||||
}, map[string]interface{}{
|
||||
"secretRef": map[string]interface{}{
|
||||
"name": "my-c",
|
||||
},
|
||||
}},
|
||||
},
|
||||
}}
|
||||
}
|
||||
obj1 := getObj1()
|
||||
obj1Copy := getObj1()
|
||||
tests := map[string]struct {
|
||||
obj *unstructured.Unstructured
|
||||
fs []string
|
||||
val interface{}
|
||||
strategyMergeKeys []string
|
||||
expObj *unstructured.Unstructured
|
||||
}{
|
||||
"normal case: use string as element": {
|
||||
obj: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"a": interface{}("a-val"),
|
||||
},
|
||||
}},
|
||||
fs: []string{"spec.a"},
|
||||
val: "a-val-b",
|
||||
expObj: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"a": interface{}("a-val-b"),
|
||||
},
|
||||
}},
|
||||
},
|
||||
"slice case: append with target field not exist": {
|
||||
obj: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{},
|
||||
}},
|
||||
fs: []string{"spec.a"},
|
||||
val: []interface{}{"a-val-b"},
|
||||
expObj: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"a": []interface{}{"a-val-b"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
"slice case: append with no keys": {
|
||||
obj: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"a": []interface{}{"a-val"},
|
||||
},
|
||||
}},
|
||||
fs: []string{"spec.a"},
|
||||
val: []interface{}{"a-val-b"},
|
||||
expObj: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"a": []interface{}{"a-val", "a-val-b"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
"slice case: append with keys match should update": {
|
||||
obj: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"a": []interface{}{map[string]interface{}{
|
||||
"name": "my",
|
||||
"value": "a-val",
|
||||
}},
|
||||
},
|
||||
}},
|
||||
fs: []string{"spec.a"},
|
||||
val: []interface{}{map[string]interface{}{
|
||||
"name": "my",
|
||||
"value": "a-val-b",
|
||||
}, map[string]interface{}{
|
||||
"name": "my2",
|
||||
"value": "a-val-c",
|
||||
}},
|
||||
strategyMergeKeys: []string{"name"},
|
||||
expObj: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"a": []interface{}{map[string]interface{}{
|
||||
"name": "my",
|
||||
"value": "a-val-b",
|
||||
}, map[string]interface{}{
|
||||
"name": "my2",
|
||||
"value": "a-val-c",
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
"slice case: append with complex keys match should update": {
|
||||
obj: obj1,
|
||||
fs: []string{"spec.a"},
|
||||
val: []interface{}{map[string]interface{}{
|
||||
"configMapRef": map[string]interface{}{
|
||||
"name": "my-a",
|
||||
"value": "mm-a",
|
||||
},
|
||||
}, map[string]interface{}{
|
||||
"secretRef": map[string]interface{}{
|
||||
"name": "my-b",
|
||||
},
|
||||
}},
|
||||
strategyMergeKeys: []string{"configMapRef.name", "secretRef.name"},
|
||||
expObj: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"a": []interface{}{map[string]interface{}{
|
||||
"configMapRef": map[string]interface{}{
|
||||
"name": "my-a",
|
||||
"value": "mm-a",
|
||||
},
|
||||
}, map[string]interface{}{
|
||||
"secretRef": map[string]interface{}{
|
||||
"name": "my-c",
|
||||
},
|
||||
}, map[string]interface{}{
|
||||
"secretRef": map[string]interface{}{
|
||||
"name": "my-b",
|
||||
},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
"slice case: no key match should just append": {
|
||||
obj: obj1Copy,
|
||||
fs: []string{"spec.a"},
|
||||
val: []interface{}{map[string]interface{}{
|
||||
"configMapRef": map[string]interface{}{
|
||||
"name": "my-a",
|
||||
"value": "mm-a",
|
||||
},
|
||||
}, map[string]interface{}{
|
||||
"secretRef": map[string]interface{}{
|
||||
"name": "my-b",
|
||||
},
|
||||
}},
|
||||
strategyMergeKeys: []string{"configMapRef.xx", "secretRef.yy"},
|
||||
expObj: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"spec": map[string]interface{}{
|
||||
"a": []interface{}{map[string]interface{}{
|
||||
"configMapRef": map[string]interface{}{
|
||||
"name": "my-a",
|
||||
"value": "my-a",
|
||||
},
|
||||
}, map[string]interface{}{
|
||||
"secretRef": map[string]interface{}{
|
||||
"name": "my-c",
|
||||
},
|
||||
}, map[string]interface{}{
|
||||
"configMapRef": map[string]interface{}{
|
||||
"name": "my-a",
|
||||
"value": "mm-a",
|
||||
},
|
||||
}, map[string]interface{}{
|
||||
"secretRef": map[string]interface{}{
|
||||
"name": "my-b",
|
||||
},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
for message, ti := range tests {
|
||||
err := fillDataInputValue(ti.obj, ti.fs, ti.val, ti.strategyMergeKeys)
|
||||
assert.NoError(t, err, message)
|
||||
assert.Equal(t, ti.expObj, ti.obj, message)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
-1159
File diff suppressed because it is too large
Load Diff
@@ -1,244 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/utils/pointer"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
controllerscheme "sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||
|
||||
core "github.com/oam-dev/kubevela/apis/core.oam.dev"
|
||||
"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/pkg/oam/util"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var reconciler *OAMApplicationReconciler
|
||||
var componentHandler *ComponentHandler
|
||||
var controllerDone context.CancelFunc
|
||||
var testEnv *envtest.Environment
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var scheme = runtime.NewScheme()
|
||||
var crd crdv1.CustomResourceDefinition
|
||||
|
||||
// OAM runtime is deprecated and we won't run test here.
|
||||
func TestReconcilerSuit(t *testing.T) {
|
||||
t.SkipNow()
|
||||
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecs(t, "OAM Core Resource Controller Unit test Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
By("Bootstrapping test environment")
|
||||
var yamlPath string
|
||||
if _, set := os.LookupEnv("COMPATIBILITY_TEST"); set {
|
||||
yamlPath = "../../../../../test/compatibility-test/testdata"
|
||||
} else {
|
||||
yamlPath = filepath.Join("../../../../..", "charts", "vela-core", "crds")
|
||||
}
|
||||
compCRD := "../../../../../charts/oam-runtime/crds/core.oam.dev_components.yaml"
|
||||
acCRD := "../../../../../charts/oam-runtime/crds/core.oam.dev_applicationconfigurations.yaml"
|
||||
logf.Log.Info("start applicationconfiguration suit test", "yaml_path", yamlPath)
|
||||
testEnv = &envtest.Environment{
|
||||
ControlPlaneStartTimeout: time.Minute,
|
||||
ControlPlaneStopTimeout: time.Minute,
|
||||
CRDDirectoryPaths: []string{
|
||||
yamlPath, // this has all the required CRDs,
|
||||
compCRD,
|
||||
acCRD,
|
||||
},
|
||||
}
|
||||
var err error
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(cfg).ToNot(BeNil())
|
||||
|
||||
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
|
||||
Expect(clientgoscheme.AddToScheme(scheme)).Should(BeNil())
|
||||
Expect(core.AddToScheme(scheme)).Should(BeNil())
|
||||
Expect(crdv1.AddToScheme(scheme)).Should(BeNil())
|
||||
depExample := &unstructured.Unstructured{}
|
||||
depExample.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: "example.com",
|
||||
Version: "v1",
|
||||
Kind: "Foo",
|
||||
})
|
||||
depSchemeBuilder := &controllerscheme.Builder{GroupVersion: schema.GroupVersion{Group: "example.com", Version: "v1"}}
|
||||
depSchemeBuilder.Register(depExample.DeepCopyObject())
|
||||
Expect(depSchemeBuilder.AddToScheme(scheme)).Should(BeNil())
|
||||
|
||||
By("Setting up kubernetes client")
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
logf.Log.Error(err, "failed to create a client")
|
||||
Fail("setup failed")
|
||||
}
|
||||
Expect(k8sClient).ShouldNot(BeNil())
|
||||
By("Finished setting up test environment")
|
||||
|
||||
By("Creating Reconciler for appconfig")
|
||||
mgr, err := ctrl.NewManager(cfg, ctrl.Options{Scheme: scheme, MetricsBindAddress: "0"})
|
||||
Expect(err).Should(BeNil())
|
||||
var ctx context.Context
|
||||
ctx, controllerDone = context.WithCancel(context.Background())
|
||||
go mgr.Start(ctx)
|
||||
|
||||
// Create a crd for appconfig dependency test
|
||||
crd = crdv1.CustomResourceDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo.example.com",
|
||||
Labels: map[string]string{"crd": "dependency"},
|
||||
},
|
||||
Spec: crdv1.CustomResourceDefinitionSpec{
|
||||
Group: "example.com",
|
||||
Names: crdv1.CustomResourceDefinitionNames{
|
||||
Kind: "Foo",
|
||||
ListKind: "FooList",
|
||||
Plural: "foo",
|
||||
Singular: "foo",
|
||||
},
|
||||
Versions: []crdv1.CustomResourceDefinitionVersion{{
|
||||
Name: "v1",
|
||||
Served: true,
|
||||
Storage: true,
|
||||
Subresources: &crdv1.CustomResourceSubresources{Status: &crdv1.CustomResourceSubresourceStatus{}},
|
||||
Schema: &crdv1.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &crdv1.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]crdv1.JSONSchemaProps{
|
||||
"spec": {
|
||||
Type: "object",
|
||||
XPreserveUnknownFields: pointer.Bool(true),
|
||||
Properties: map[string]crdv1.JSONSchemaProps{
|
||||
"key": {Type: "string"},
|
||||
}},
|
||||
"status": {
|
||||
Type: "object",
|
||||
XPreserveUnknownFields: pointer.Bool(true),
|
||||
Properties: map[string]crdv1.JSONSchemaProps{
|
||||
"key": {Type: "string"},
|
||||
"app-hash": {Type: "string"},
|
||||
}}}}}},
|
||||
},
|
||||
Scope: crdv1.NamespaceScoped,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.Background(), &crd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
By("Created a crd for appconfig dependency test")
|
||||
|
||||
var mapping *meta.RESTMapping
|
||||
Eventually(func() error {
|
||||
mapping, err = k8sClient.RESTMapper().RESTMapping(schema.GroupKind{
|
||||
Group: "example.com",
|
||||
Kind: "Foo",
|
||||
}, "v1")
|
||||
return err
|
||||
}, time.Second*30, time.Millisecond*500).Should(BeNil())
|
||||
Expect(mapping.Resource.Resource).Should(Equal("foo"))
|
||||
|
||||
reconciler = NewReconciler(mgr)
|
||||
componentHandler = &ComponentHandler{Client: k8sClient, RevisionLimit: 100}
|
||||
|
||||
By("Creating workload definition and trait definition")
|
||||
wd := v1alpha2.WorkloadDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo.example.com",
|
||||
Namespace: "vela-system",
|
||||
},
|
||||
Spec: v1alpha2.WorkloadDefinitionSpec{
|
||||
Reference: common.DefinitionReference{
|
||||
Name: "foo.example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
td := v1alpha2.TraitDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo.example.com",
|
||||
Namespace: "vela-system",
|
||||
},
|
||||
Spec: v1alpha2.TraitDefinitionSpec{
|
||||
Reference: common.DefinitionReference{
|
||||
Name: "foo.example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rollout := v1alpha2.TraitDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "rollout-revision",
|
||||
Namespace: "vela-system",
|
||||
},
|
||||
Spec: v1alpha2.TraitDefinitionSpec{
|
||||
Reference: common.DefinitionReference{
|
||||
Name: "foo.example.com",
|
||||
},
|
||||
RevisionEnabled: true,
|
||||
},
|
||||
}
|
||||
definitionNs := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}}
|
||||
Expect(k8sClient.Create(context.Background(), definitionNs.DeepCopy())).Should(BeNil())
|
||||
|
||||
// For some reason, WorkloadDefinition is created as a Cluster scope object
|
||||
Expect(k8sClient.Create(ctx, &wd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
// For some reason, TraitDefinition is created as a Cluster scope object
|
||||
Expect(k8sClient.Create(ctx, &td)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
// rollout trait is used for revisionEnable case test
|
||||
Expect(k8sClient.Create(ctx, &rollout)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
|
||||
crd = crdv1.CustomResourceDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo.example.com",
|
||||
Labels: map[string]string{"crd": "dependency"},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Delete(context.Background(), &crd)).Should(BeNil())
|
||||
By("Deleted the custom resource definition")
|
||||
|
||||
By("Tearing down the test environment")
|
||||
controllerDone()
|
||||
err := testEnv.Stop()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
@@ -1,115 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
"github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
const (
|
||||
// below are the resources that we know how to disable
|
||||
cloneSetDisablePath = "spec.updateStrategy.paused"
|
||||
advancedStatefulSetDisablePath = "spec.updateStrategy.rollingUpdate.paused"
|
||||
deploymentDisablePath = "spec.paused"
|
||||
)
|
||||
|
||||
// SetAppWorkloadInstanceName sets the name of the workload instance depends on the component revision
|
||||
// and the workload kind
|
||||
func SetAppWorkloadInstanceName(componentName string, w *unstructured.Unstructured, revision int, inplaceUpgrade string) {
|
||||
|
||||
if inplaceUpgrade == strconv.FormatBool(true) {
|
||||
klog.InfoS("we reuse the component name for resources that support in-place upgrade",
|
||||
"GVK", w.GroupVersionKind(), "instance name", componentName, oam.AnnotationInplaceUpgrade, true)
|
||||
w.SetName(componentName)
|
||||
return
|
||||
}
|
||||
|
||||
// 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", componentName)
|
||||
w.SetName(componentName)
|
||||
return
|
||||
}
|
||||
}
|
||||
// we assume that the rest of the resources do not support in-place upgrade
|
||||
instanceName := utils.ConstructRevisionName(componentName, int64(revision))
|
||||
klog.InfoS("we encountered an unknown resources, assume that it does not support in-place upgrade",
|
||||
"GVK", w.GroupVersionKind(), "instance name", instanceName)
|
||||
w.SetName(instanceName)
|
||||
|
||||
}
|
||||
|
||||
// prepWorkloadInstanceForRollout prepare the workload before it is emit to the k8s. The current approach is to mark it
|
||||
// as disabled so that it's spec won't take effect immediately. The rollout controller can take over the resources
|
||||
// and enable it on its own since appConfig controller here won't override their change
|
||||
func prepWorkloadInstanceForRollout(workload *unstructured.Unstructured) error {
|
||||
pv := fieldpath.Pave(workload.UnstructuredContent())
|
||||
|
||||
// TODO: we can get the workloadDefinition name from workload.GetLabels()["oam.WorkloadTypeLabel"]
|
||||
// and use a special field like "disablePath" in the definition to allow configurable behavior
|
||||
|
||||
// we hard code the behavior depends on the known workload group/kind for now.
|
||||
if workload.GroupVersionKind().Group == v1alpha1.GroupVersion.Group {
|
||||
switch workload.GetKind() {
|
||||
case reflect.TypeOf(v1alpha1.CloneSet{}).Name():
|
||||
err := pv.SetBool(cloneSetDisablePath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
klog.InfoS("we render a CloneSet workload paused on the first time",
|
||||
"kind", workload.GetKind(), "instance name", workload.GetName())
|
||||
return nil
|
||||
case reflect.TypeOf(v1alpha1.StatefulSet{}).Name():
|
||||
err := pv.SetBool(advancedStatefulSetDisablePath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
klog.InfoS("we render an advanced statefulset workload paused on the first time",
|
||||
"kind", workload.GetKind(), "instance name", workload.GetName())
|
||||
return nil
|
||||
}
|
||||
} else if workload.GroupVersionKind().Group == appsv1.GroupName &&
|
||||
workload.GetKind() == reflect.TypeOf(appsv1.Deployment{}).Name() {
|
||||
err := pv.SetBool(deploymentDisablePath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
klog.InfoS("we render a deployment workload paused on the first time",
|
||||
"kind", workload.GetKind(), "instance name", workload.GetName())
|
||||
return nil
|
||||
}
|
||||
klog.InfoS("we encountered an unknown resource, we don't know how to prepare it",
|
||||
"GVK", workload.GroupVersionKind().String(), "instance name", workload.GetName())
|
||||
return fmt.Errorf("we do not know how to prepare `%s` as it has an unknown type %s", workload.GetName(),
|
||||
workload.GroupVersionKind().String())
|
||||
}
|
||||
@@ -1,157 +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 applicationconfiguration
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
func TestSetAppWorkloadInstanceName(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
compName string
|
||||
w *unstructured.Unstructured
|
||||
revision int
|
||||
expName string
|
||||
inplace string
|
||||
reason string
|
||||
}{
|
||||
"two resources case": {
|
||||
compName: "webservice",
|
||||
revision: 5,
|
||||
w: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "extensions/v1beta1",
|
||||
"kind": "deployment",
|
||||
}},
|
||||
expName: "webservice-v5",
|
||||
reason: "workloadName should be the component with revision",
|
||||
},
|
||||
"one resources case": {
|
||||
compName: "mysql",
|
||||
revision: 2,
|
||||
w: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "apps.kruise.io/v1alpha1",
|
||||
"kind": "CloneSet",
|
||||
}},
|
||||
expName: "mysql",
|
||||
reason: "workloadName should be just the component name if we can do in-place upgrade",
|
||||
},
|
||||
"ignore any existing name": {
|
||||
compName: "mysql",
|
||||
revision: 2,
|
||||
w: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "apps.kruise.io/v1alpha1",
|
||||
"kind": "CloneSet",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "mysql-v1",
|
||||
},
|
||||
}},
|
||||
expName: "mysql",
|
||||
reason: "workloadName set in the template is ignored",
|
||||
},
|
||||
"one resources same name case": {
|
||||
compName: "mysql",
|
||||
revision: 2,
|
||||
w: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "oam.dev/v1alpha1",
|
||||
"kind": "CloneSet",
|
||||
}},
|
||||
expName: "mysql-v2",
|
||||
reason: "we compare not only the kind but also the group name",
|
||||
},
|
||||
"use inplaceUpgrade = true": {
|
||||
compName: "mysql",
|
||||
revision: 2,
|
||||
w: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "oam.dev/v1alpha1",
|
||||
"kind": "CloneSet",
|
||||
}},
|
||||
expName: "mysql",
|
||||
inplace: strconv.FormatBool(true),
|
||||
reason: "we compare not only the kind but also the group name",
|
||||
},
|
||||
"use inplaceUpgrade = other value won't work": {
|
||||
compName: "mysql",
|
||||
revision: 2,
|
||||
w: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "oam.dev/v1alpha1",
|
||||
"kind": "CloneSet",
|
||||
}},
|
||||
expName: "mysql-v2",
|
||||
inplace: "t",
|
||||
reason: "we compare not only the kind but also the group name",
|
||||
},
|
||||
}
|
||||
for name, ti := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
SetAppWorkloadInstanceName(ti.compName, ti.w, ti.revision, ti.inplace)
|
||||
assert.Equal(t, ti.expName, ti.w.GetName(), ti.reason)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepWorkloadInstanceForRollout(t *testing.T) {
|
||||
workload := kruise.CloneSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "CloneSet",
|
||||
APIVersion: "apps.kruise.io/v1alpha1",
|
||||
},
|
||||
Spec: kruise.CloneSetSpec{
|
||||
Template: v1.PodTemplateSpec{
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
w, _ := util.Object2Unstructured(workload)
|
||||
assert.True(t, prepWorkloadInstanceForRollout(w) == nil)
|
||||
value, exist, err := unstructured.NestedBool(w.Object, "spec", "updateStrategy", "paused")
|
||||
assert.True(t, exist)
|
||||
assert.True(t, err == nil)
|
||||
assert.True(t, value)
|
||||
// Test statefulset
|
||||
workload.Kind = "StatefulSet"
|
||||
w, _ = util.Object2Unstructured(workload)
|
||||
assert.True(t, prepWorkloadInstanceForRollout(w) == nil)
|
||||
value, exist, err = unstructured.NestedBool(w.Object, "spec", "updateStrategy", "rollingUpdate", "paused")
|
||||
assert.True(t, exist)
|
||||
assert.True(t, err == nil)
|
||||
assert.True(t, value)
|
||||
// Test deployment
|
||||
workload.Kind = "Deployment"
|
||||
workload.APIVersion = "apps/v1"
|
||||
w, _ = util.Object2Unstructured(workload)
|
||||
assert.True(t, prepWorkloadInstanceForRollout(w) == nil)
|
||||
value, exist, err = unstructured.NestedBool(w.Object, "spec", "paused")
|
||||
assert.True(t, exist)
|
||||
assert.True(t, err == nil)
|
||||
assert.True(t, value)
|
||||
// Test other
|
||||
workload.Kind = "StatefulSet"
|
||||
w, _ = util.Object2Unstructured(workload)
|
||||
assert.True(t, strings.Contains(prepWorkloadInstanceForRollout(w).Error(), "we do not know how to prepare"))
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
/*
|
||||
Copyright 2019 The Crossplane 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 scopes provides scope related controllers.
|
||||
package scopes
|
||||
@@ -1,562 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Crossplane 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 healthscope
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
core "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"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/kubevela/workflow/pkg/cue/process"
|
||||
terraformtypes "github.com/oam-dev/terraform-controller/api/types"
|
||||
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
oamtypes "github.com/oam-dev/kubevela/apis/types"
|
||||
af "github.com/oam-dev/kubevela/pkg/appfile"
|
||||
velaprocess "github.com/oam-dev/kubevela/pkg/cue/process"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
const (
|
||||
infoFmtUnknownWorkload = "APIVersion %v Kind %v workload is unknown for HealthScope "
|
||||
infoFmtReady = "Ready:%d/%d "
|
||||
errHealthCheck = "error occurs in health check "
|
||||
errGetVersioningWorkloads = "error occurs when get versioning peer workloads refs"
|
||||
|
||||
defaultTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// HealthStatus represents health status strings.
|
||||
type HealthStatus = v1alpha2.HealthStatus
|
||||
|
||||
const (
|
||||
// StatusHealthy represents healthy status.
|
||||
StatusHealthy = v1alpha2.StatusHealthy
|
||||
// StatusUnhealthy represents unhealthy status.
|
||||
StatusUnhealthy = v1alpha2.StatusUnhealthy
|
||||
// StatusUnknown represents unknown status.
|
||||
StatusUnknown = v1alpha2.StatusUnknown
|
||||
)
|
||||
|
||||
var (
|
||||
kindDeployment = reflect.TypeOf(apps.Deployment{}).Name()
|
||||
kindService = reflect.TypeOf(core.Service{}).Name()
|
||||
kindStatefulSet = reflect.TypeOf(apps.StatefulSet{}).Name()
|
||||
kindDaemonSet = reflect.TypeOf(apps.DaemonSet{}).Name()
|
||||
)
|
||||
|
||||
// AppHealthCondition holds health status of an application
|
||||
type AppHealthCondition = v1alpha2.AppHealthCondition
|
||||
|
||||
// WorkloadHealthCondition holds health status of a workload
|
||||
type WorkloadHealthCondition = v1alpha2.WorkloadHealthCondition
|
||||
|
||||
// TraitHealthCondition holds health status of a trait
|
||||
type TraitHealthCondition = v1alpha2.TraitHealthCondition
|
||||
|
||||
// ScopeHealthCondition holds health condition of a scope
|
||||
type ScopeHealthCondition = v1alpha2.ScopeHealthCondition
|
||||
|
||||
// A WorloadHealthChecker checks health status of specified resource
|
||||
// and saves status into an HealthCondition object.
|
||||
type WorloadHealthChecker interface {
|
||||
Check(context.Context, client.Client, core.ObjectReference, string) *WorkloadHealthCondition
|
||||
}
|
||||
|
||||
// WorkloadHealthCheckFn checks health status of specified resource
|
||||
// and saves status into an HealthCondition object.
|
||||
type WorkloadHealthCheckFn func(context.Context, client.Client, core.ObjectReference, string) *WorkloadHealthCondition
|
||||
|
||||
// Check the health status of specified resource
|
||||
func (fn WorkloadHealthCheckFn) Check(ctx context.Context, c client.Client, tr core.ObjectReference, ns string) *WorkloadHealthCondition {
|
||||
r := fn(ctx, c, tr, ns)
|
||||
if r == nil {
|
||||
return r
|
||||
}
|
||||
// check all workloads of a version-enabled component
|
||||
peerRefs, err := getVersioningPeerWorkloadRefs(ctx, c, tr, ns)
|
||||
if err != nil {
|
||||
r.HealthStatus = StatusUnhealthy
|
||||
r.Diagnosis = fmt.Sprintf("%s %s:%s",
|
||||
r.Diagnosis,
|
||||
errGetVersioningWorkloads,
|
||||
err.Error())
|
||||
return r
|
||||
}
|
||||
|
||||
if len(peerRefs) > 0 {
|
||||
var peerHCs PeerHealthConditions
|
||||
for _, peerRef := range peerRefs {
|
||||
if peerHC := fn(ctx, c, peerRef, ns); peerHC != nil {
|
||||
peerHCs = append(peerHCs, *peerHC.DeepCopy())
|
||||
}
|
||||
}
|
||||
peerHCs.MergePeerWorkloadsConditions(r)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// CheckDeploymentHealth checks health condition of Deployment
|
||||
func CheckDeploymentHealth(ctx context.Context, client client.Client, ref core.ObjectReference, namespace string) *WorkloadHealthCondition {
|
||||
if ref.GroupVersionKind() != apps.SchemeGroupVersion.WithKind(kindDeployment) {
|
||||
return nil
|
||||
}
|
||||
r := &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
TargetWorkload: ref,
|
||||
}
|
||||
|
||||
unstructuredDeployment := &unstructured.Unstructured{}
|
||||
unstructuredDeployment.SetGroupVersionKind(apps.SchemeGroupVersion.WithKind(kindDeployment))
|
||||
deploymentRef := types.NamespacedName{Namespace: namespace, Name: ref.Name}
|
||||
if err := client.Get(ctx, deploymentRef, unstructuredDeployment); err != nil {
|
||||
r.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return r
|
||||
}
|
||||
|
||||
deployment := new(apps.Deployment)
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredDeployment.Object, deployment); err != nil {
|
||||
r.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return r
|
||||
}
|
||||
|
||||
r.ComponentName = getComponentNameFromLabel(deployment)
|
||||
r.TargetWorkload.UID = deployment.GetUID()
|
||||
|
||||
requiredReplicas := int32(0)
|
||||
if deployment.Spec.Replicas != nil {
|
||||
requiredReplicas = *deployment.Spec.Replicas
|
||||
}
|
||||
|
||||
r.Diagnosis = fmt.Sprintf(infoFmtReady, deployment.Status.ReadyReplicas, requiredReplicas)
|
||||
|
||||
// Health criteria
|
||||
if deployment.Status.ReadyReplicas != requiredReplicas {
|
||||
return r
|
||||
}
|
||||
r.HealthStatus = StatusHealthy
|
||||
return r
|
||||
}
|
||||
|
||||
// CheckStatefulsetHealth checks health condition of StatefulSet
|
||||
func CheckStatefulsetHealth(ctx context.Context, client client.Client, ref core.ObjectReference, namespace string) *WorkloadHealthCondition {
|
||||
if ref.GroupVersionKind() != apps.SchemeGroupVersion.WithKind(kindStatefulSet) {
|
||||
return nil
|
||||
}
|
||||
r := &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
TargetWorkload: ref,
|
||||
}
|
||||
|
||||
unstructuredStatefulSet := &unstructured.Unstructured{}
|
||||
unstructuredStatefulSet.SetGroupVersionKind(apps.SchemeGroupVersion.WithKind(kindStatefulSet))
|
||||
statefulSetRef := types.NamespacedName{Namespace: namespace, Name: ref.Name}
|
||||
if err := client.Get(ctx, statefulSetRef, unstructuredStatefulSet); err != nil {
|
||||
r.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return r
|
||||
}
|
||||
|
||||
statefulSet := new(apps.StatefulSet)
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredStatefulSet.Object, statefulSet); err != nil {
|
||||
r.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return r
|
||||
}
|
||||
|
||||
r.ComponentName = getComponentNameFromLabel(statefulSet)
|
||||
r.TargetWorkload.UID = statefulSet.GetUID()
|
||||
requiredReplicas := int32(0)
|
||||
if statefulSet.Spec.Replicas != nil {
|
||||
requiredReplicas = *statefulSet.Spec.Replicas
|
||||
}
|
||||
r.Diagnosis = fmt.Sprintf(infoFmtReady, statefulSet.Status.ReadyReplicas, requiredReplicas)
|
||||
|
||||
// Health criteria
|
||||
if statefulSet.Status.ReadyReplicas != requiredReplicas {
|
||||
return r
|
||||
}
|
||||
r.HealthStatus = StatusHealthy
|
||||
return r
|
||||
}
|
||||
|
||||
// CheckDaemonsetHealth checks health condition of DaemonSet
|
||||
func CheckDaemonsetHealth(ctx context.Context, client client.Client, ref core.ObjectReference, namespace string) *WorkloadHealthCondition {
|
||||
if ref.GroupVersionKind() != apps.SchemeGroupVersion.WithKind(kindDaemonSet) {
|
||||
return nil
|
||||
}
|
||||
r := &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
TargetWorkload: ref,
|
||||
}
|
||||
|
||||
unstructuredDaemonSet := &unstructured.Unstructured{}
|
||||
unstructuredDaemonSet.SetGroupVersionKind(apps.SchemeGroupVersion.WithKind(kindDaemonSet))
|
||||
daemonSetRef := types.NamespacedName{Namespace: namespace, Name: ref.Name}
|
||||
if err := client.Get(ctx, daemonSetRef, unstructuredDaemonSet); err != nil {
|
||||
r.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return r
|
||||
}
|
||||
|
||||
daemonSet := new(apps.DaemonSet)
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredDaemonSet.Object, daemonSet); err != nil {
|
||||
r.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return r
|
||||
}
|
||||
|
||||
r.ComponentName = getComponentNameFromLabel(daemonSet)
|
||||
r.TargetWorkload.UID = daemonSet.GetUID()
|
||||
r.Diagnosis = fmt.Sprintf(infoFmtReady, daemonSet.Status.NumberReady, daemonSet.Status.DesiredNumberScheduled)
|
||||
|
||||
// Health criteria
|
||||
if daemonSet.Status.NumberUnavailable != 0 {
|
||||
return r
|
||||
}
|
||||
r.HealthStatus = StatusHealthy
|
||||
return r
|
||||
}
|
||||
|
||||
// CheckByHealthCheckTrait checks health condition through HealthCheckTrait.
|
||||
func CheckByHealthCheckTrait(ctx context.Context, c client.Client, wlRef core.ObjectReference, ns string) *WorkloadHealthCondition {
|
||||
// TODO(roywang) implement HealthCheckTrait feature
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckUnknownWorkload handles unknown type workloads.
|
||||
func CheckUnknownWorkload(ctx context.Context, c client.Client, wlRef core.ObjectReference, ns string) *WorkloadHealthCondition {
|
||||
healthCondition := &WorkloadHealthCondition{
|
||||
TargetWorkload: wlRef,
|
||||
HealthStatus: StatusUnknown,
|
||||
Diagnosis: fmt.Sprintf(infoFmtUnknownWorkload, wlRef.APIVersion, wlRef.Kind),
|
||||
}
|
||||
|
||||
wl := &unstructured.Unstructured{}
|
||||
wl.SetGroupVersionKind(wlRef.GroupVersionKind())
|
||||
if err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: wlRef.Name}, wl); err != nil {
|
||||
healthCondition.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return healthCondition
|
||||
}
|
||||
healthCondition.ComponentName = getComponentNameFromLabel(wl)
|
||||
|
||||
// for unknown workloads, just show status instead of precise diagnosis
|
||||
wlStatus, _, _ := unstructured.NestedMap(wl.UnstructuredContent(), "status")
|
||||
wlStatusR, err := json.Marshal(wlStatus)
|
||||
if err != nil {
|
||||
healthCondition.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return healthCondition
|
||||
}
|
||||
healthCondition.WorkloadStatus = string(wlStatusR)
|
||||
return healthCondition
|
||||
}
|
||||
|
||||
func getComponentNameFromLabel(o metav1.Object) string {
|
||||
if o == nil {
|
||||
return ""
|
||||
}
|
||||
compName, exist := o.GetLabels()[oam.LabelAppComponent]
|
||||
if !exist {
|
||||
compName = ""
|
||||
}
|
||||
return compName
|
||||
}
|
||||
|
||||
func getAppConfigNameFromLabel(o metav1.Object) string {
|
||||
if o == nil {
|
||||
return ""
|
||||
}
|
||||
appName, exist := o.GetLabels()[oam.LabelAppName]
|
||||
if !exist {
|
||||
appName = ""
|
||||
}
|
||||
return appName
|
||||
}
|
||||
|
||||
func getVersioningPeerWorkloadRefs(ctx context.Context, c client.Reader, wlRef core.ObjectReference, ns string) ([]core.ObjectReference, error) {
|
||||
o := &unstructured.Unstructured{}
|
||||
o.SetGroupVersionKind(wlRef.GroupVersionKind())
|
||||
if err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: wlRef.Name}, o); err != nil && !apierrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
compName := getComponentNameFromLabel(o)
|
||||
appName := getAppConfigNameFromLabel(o)
|
||||
if compName == "" || appName == "" {
|
||||
// if missing these labels, cannot get peer workloads
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
peerRefs := []core.ObjectReference{}
|
||||
l := &unstructured.UnstructuredList{}
|
||||
l.SetGroupVersionKind(wlRef.GroupVersionKind())
|
||||
|
||||
opts := []client.ListOption{
|
||||
client.InNamespace(ns),
|
||||
client.MatchingLabels{
|
||||
oam.LabelAppComponent: compName,
|
||||
oam.LabelAppName: appName},
|
||||
}
|
||||
if err := c.List(ctx, l, opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, obj := range l.Items {
|
||||
if obj.GetName() == o.GetName() {
|
||||
continue
|
||||
}
|
||||
tmpRef := core.ObjectReference{}
|
||||
tmpRef.SetGroupVersionKind(obj.GroupVersionKind())
|
||||
tmpRef.Name = obj.GetName()
|
||||
peerRefs = append(peerRefs, tmpRef)
|
||||
}
|
||||
return peerRefs, nil
|
||||
}
|
||||
|
||||
// PeerHealthConditions refers to a slice of health condition of worloads
|
||||
// belonging to one version-enabled component
|
||||
type PeerHealthConditions []WorkloadHealthCondition
|
||||
|
||||
func (p PeerHealthConditions) Len() int { return len(p) }
|
||||
func (p PeerHealthConditions) Less(i, j int) bool {
|
||||
// sort by revision number in descending order
|
||||
return extractRevision(p[i].TargetWorkload.Name) > extractRevision(p[j].TargetWorkload.Name)
|
||||
}
|
||||
func (p PeerHealthConditions) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// exract revision number from revision name in format: <comp-name>-v<revision number>
|
||||
// any non-qualified format should return 0
|
||||
func extractRevision(c string) int {
|
||||
i, _ := strconv.ParseInt(c[strings.LastIndex(c, "v")+1:], 10, 0)
|
||||
return int(i)
|
||||
}
|
||||
|
||||
// MergePeerWorkloadsConditions merge health conditions of all peer workloads into basic
|
||||
func (p PeerHealthConditions) MergePeerWorkloadsConditions(basic *WorkloadHealthCondition) {
|
||||
if basic == nil || len(p) == 0 {
|
||||
return
|
||||
}
|
||||
// copy to keep idempotent
|
||||
peerHCs := make(PeerHealthConditions, len(p))
|
||||
copy(peerHCs, p)
|
||||
//nolint:makezero
|
||||
peerHCs = append(peerHCs, *basic.DeepCopy())
|
||||
|
||||
// sort by revision number in descending order
|
||||
sort.Sort(peerHCs)
|
||||
|
||||
for _, peerHC := range peerHCs {
|
||||
if peerHC.HealthStatus == StatusUnhealthy {
|
||||
// if ANY peer workload is unhealthy
|
||||
// then the overall condition is unhealthy
|
||||
basic.HealthStatus = StatusUnhealthy
|
||||
}
|
||||
}
|
||||
// re-format diagnosis/workloadStatus to show multiple workloads'
|
||||
if basic.HealthStatus == StatusUnknown {
|
||||
basic.WorkloadStatus = fmt.Sprintf("%s:%s", peerHCs[0].TargetWorkload.Name, peerHCs[0].WorkloadStatus)
|
||||
for _, peerHC := range peerHCs[1:] {
|
||||
basic.WorkloadStatus = fmt.Sprintf("%s %s:%s",
|
||||
basic.WorkloadStatus,
|
||||
peerHC.TargetWorkload.Name,
|
||||
peerHC.WorkloadStatus)
|
||||
}
|
||||
} else {
|
||||
basic.Diagnosis = fmt.Sprintf("%s:%s", peerHCs[0].TargetWorkload.Name, peerHCs[0].Diagnosis)
|
||||
for i, peerHC := range peerHCs[1:] {
|
||||
if i > 0 && peerHC.Diagnosis == fmt.Sprintf(infoFmtReady, 0, 0) {
|
||||
// skip timeworn ones
|
||||
continue
|
||||
}
|
||||
basic.Diagnosis = fmt.Sprintf("%s %s:%s",
|
||||
basic.Diagnosis,
|
||||
peerHC.TargetWorkload.Name,
|
||||
peerHC.Diagnosis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CUEBasedHealthCheck check workload and traits health through CUE-based health checking approach.
|
||||
func CUEBasedHealthCheck(ctx context.Context, c client.Client, wlRef WorkloadReference, ns string, appfile *af.Appfile) (*WorkloadHealthCondition, []*TraitHealthCondition) {
|
||||
wlHealth := &WorkloadHealthCondition{
|
||||
TargetWorkload: wlRef.ObjectReference,
|
||||
}
|
||||
|
||||
o := &unstructured.Unstructured{}
|
||||
o.SetGroupVersionKind(wlRef.GroupVersionKind())
|
||||
if err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: wlRef.Name}, o); err != nil {
|
||||
wlHealth.HealthStatus = StatusUnhealthy
|
||||
wlHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return wlHealth, nil
|
||||
}
|
||||
compName := getComponentNameFromLabel(o)
|
||||
wlHealth.ComponentName = compName
|
||||
|
||||
var wl *af.Workload
|
||||
for _, v := range appfile.Workloads {
|
||||
if v.Name == compName {
|
||||
wl = v
|
||||
break
|
||||
}
|
||||
}
|
||||
if wl == nil {
|
||||
// almost impossible
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var pCtx process.Context
|
||||
|
||||
// if error occurs when check workload health, it's not allowed to check traits
|
||||
// because CUE-based health checking replies on valid process context
|
||||
okToCheckTrait := false
|
||||
|
||||
func() {
|
||||
|
||||
switch wl.CapabilityCategory {
|
||||
case oamtypes.TerraformCategory:
|
||||
ctx := context.Background()
|
||||
pCtx = af.NewBasicContext(af.GenerateContextDataFromAppFile(appfile, wl.Name), wl.Params)
|
||||
var configuration terraformapi.Configuration
|
||||
if err := c.Get(ctx, client.ObjectKey{Name: wl.Name, Namespace: ns}, &configuration); err != nil {
|
||||
wlHealth.HealthStatus = StatusUnhealthy
|
||||
wlHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
}
|
||||
if configuration.Status.Apply.State != terraformtypes.Available {
|
||||
wlHealth.HealthStatus = StatusUnhealthy
|
||||
} else {
|
||||
wlHealth.HealthStatus = StatusHealthy
|
||||
}
|
||||
wlHealth.Diagnosis = configuration.Status.Apply.Message
|
||||
okToCheckTrait = true
|
||||
default:
|
||||
pCtx = velaprocess.NewContext(af.GenerateContextDataFromAppFile(appfile, wl.Name))
|
||||
pCtx.SetCtx(ctx)
|
||||
if wl.CapabilityCategory != oamtypes.CUECategory {
|
||||
templateStr, err := af.GenerateCUETemplate(wl)
|
||||
if err != nil {
|
||||
wlHealth.HealthStatus = StatusUnhealthy
|
||||
wlHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return
|
||||
}
|
||||
wl.FullTemplate.TemplateStr = templateStr
|
||||
}
|
||||
|
||||
if err := wl.EvalContext(pCtx); err != nil {
|
||||
wlHealth.HealthStatus = StatusUnhealthy
|
||||
wlHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return
|
||||
}
|
||||
// if workload has no CUE-based health template, skip check workload,
|
||||
// but still okay to check traits because process context is ready
|
||||
if len(wl.FullTemplate.Health) == 0 {
|
||||
wlHealth = nil
|
||||
okToCheckTrait = true
|
||||
return
|
||||
}
|
||||
accessor := util.NewApplicationResourceNamespaceAccessor(ns, "")
|
||||
templateContext, err := wl.GetTemplateContext(pCtx, c, accessor)
|
||||
if err != nil {
|
||||
wlHealth.HealthStatus = StatusUnhealthy
|
||||
wlHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return
|
||||
}
|
||||
isHealthy, err := wl.EvalHealth(templateContext)
|
||||
if err != nil {
|
||||
wlHealth.HealthStatus = StatusUnhealthy
|
||||
wlHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
return
|
||||
}
|
||||
if isHealthy {
|
||||
wlHealth.HealthStatus = StatusHealthy
|
||||
} else {
|
||||
// TODO(wonderflow): we should add a custom way to let the template say why it's unhealthy, only a bool flag is not enough
|
||||
wlHealth.HealthStatus = StatusUnhealthy
|
||||
}
|
||||
wlHealth.CustomStatusMsg, err = wl.EvalStatus(templateContext)
|
||||
if err != nil {
|
||||
wlHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
}
|
||||
okToCheckTrait = true
|
||||
}
|
||||
}()
|
||||
|
||||
traits := make([]*v1alpha2.TraitHealthCondition, len(wl.Traits))
|
||||
for i, tr := range wl.Traits {
|
||||
tHealth := &v1alpha2.TraitHealthCondition{
|
||||
Type: tr.Name,
|
||||
}
|
||||
if !okToCheckTrait {
|
||||
tHealth.HealthStatus = StatusUnknown
|
||||
tHealth.Diagnosis = "error occurs in checking workload health"
|
||||
traits[i] = tHealth
|
||||
continue
|
||||
}
|
||||
|
||||
if len(tr.FullTemplate.Health) == 0 {
|
||||
tHealth.HealthStatus = StatusHealthy
|
||||
tHealth.Diagnosis = "no CUE-based health check template"
|
||||
traits[i] = tHealth
|
||||
continue
|
||||
}
|
||||
if err := tr.EvalContext(pCtx); err != nil {
|
||||
tHealth.HealthStatus = StatusUnhealthy
|
||||
tHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
traits[i] = tHealth
|
||||
continue
|
||||
}
|
||||
accessor := util.NewApplicationResourceNamespaceAccessor("", ns)
|
||||
templateContext, err := tr.GetTemplateContext(pCtx, c, accessor)
|
||||
if err != nil {
|
||||
tHealth.HealthStatus = StatusUnhealthy
|
||||
tHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
traits[i] = tHealth
|
||||
continue
|
||||
}
|
||||
isHealthy, err := tr.EvalHealth(templateContext)
|
||||
if err != nil {
|
||||
tHealth.HealthStatus = StatusUnhealthy
|
||||
tHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
traits[i] = tHealth
|
||||
continue
|
||||
}
|
||||
if isHealthy {
|
||||
tHealth.HealthStatus = StatusHealthy
|
||||
} else {
|
||||
// TODO(wonderflow): we should add a custom way to let the template say why it's unhealthy, only a bool flag is not enough
|
||||
tHealth.HealthStatus = StatusUnhealthy
|
||||
}
|
||||
tHealth.CustomStatusMsg, err = tr.EvalStatus(templateContext)
|
||||
if err != nil {
|
||||
tHealth.Diagnosis = errors.Wrap(err, errHealthCheck).Error()
|
||||
}
|
||||
traits[i] = tHealth
|
||||
}
|
||||
return wlHealth, traits
|
||||
}
|
||||
-657
@@ -1,657 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Crossplane 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 healthscope
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/resource"
|
||||
ctrlrec "github.com/kubevela/pkg/controller/reconciler"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/klog/v2"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/kubevela/workflow/pkg/cue/packages"
|
||||
|
||||
commonapis "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
af "github.com/oam-dev/kubevela/pkg/appfile"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/common"
|
||||
controller "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/policy/envbinding"
|
||||
)
|
||||
|
||||
const (
|
||||
longWait = 10 * time.Second
|
||||
)
|
||||
|
||||
// Reconcile error strings.
|
||||
const (
|
||||
errGetHealthScope = "cannot get health scope"
|
||||
errUpdateHealthScopeStatus = "cannot update health scope status"
|
||||
)
|
||||
|
||||
// Reconcile event reasons.
|
||||
const (
|
||||
reasonHealthCheck = "HealthCheck"
|
||||
)
|
||||
|
||||
// WorkloadReference refer to a multi-env workload
|
||||
type WorkloadReference struct {
|
||||
corev1.ObjectReference
|
||||
clusterName string
|
||||
envName string
|
||||
}
|
||||
|
||||
// AppInfo contains app's name and app's env
|
||||
type AppInfo struct {
|
||||
appName string
|
||||
envName string
|
||||
}
|
||||
|
||||
// Setup adds a controller that reconciles HealthScope.
|
||||
func Setup(mgr ctrl.Manager, args controller.Args) error {
|
||||
name := "oam/" + strings.ToLower(v1alpha2.HealthScopeGroupKind)
|
||||
r := NewReconciler(mgr, WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))))
|
||||
r.pd = args.PackageDiscover
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named(name).
|
||||
For(&v1alpha2.HealthScope{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
// A Reconciler reconciles OAM Scopes by keeping track of the health status of components.
|
||||
type Reconciler struct {
|
||||
client client.Client
|
||||
pd *packages.PackageDiscover
|
||||
record event.Recorder
|
||||
// traitChecker represents checker fetching health condition from HealthCheckTrait
|
||||
traitChecker WorloadHealthChecker
|
||||
// checkers represents a set of built-in checkers
|
||||
checkers []WorloadHealthChecker
|
||||
// unknownChecker represents checker handling workloads that
|
||||
// cannot be hanlded by traitChecker nor built-in checkers
|
||||
unknownChecker WorloadHealthChecker
|
||||
}
|
||||
|
||||
// A ReconcilerOption configures a Reconciler.
|
||||
type ReconcilerOption func(*Reconciler)
|
||||
|
||||
// WithRecorder specifies how the Reconciler should record events.
|
||||
func WithRecorder(er event.Recorder) ReconcilerOption {
|
||||
return func(r *Reconciler) {
|
||||
r.record = er
|
||||
}
|
||||
}
|
||||
|
||||
// WithTraitChecker adds health checker based on HealthCheckTrait
|
||||
func WithTraitChecker(c WorloadHealthChecker) ReconcilerOption {
|
||||
return func(r *Reconciler) {
|
||||
r.traitChecker = c
|
||||
}
|
||||
}
|
||||
|
||||
// WithChecker adds workload health checker
|
||||
func WithChecker(c WorloadHealthChecker) ReconcilerOption {
|
||||
return func(r *Reconciler) {
|
||||
if r.checkers == nil {
|
||||
r.checkers = make([]WorloadHealthChecker, 0)
|
||||
}
|
||||
r.checkers = append(r.checkers, c)
|
||||
}
|
||||
}
|
||||
|
||||
// NewReconciler returns a Reconciler that reconciles HealthScope by keeping track of its healthstatus.
|
||||
func NewReconciler(m ctrl.Manager, o ...ReconcilerOption) *Reconciler {
|
||||
r := &Reconciler{
|
||||
client: m.GetClient(),
|
||||
record: event.NewNopRecorder(),
|
||||
traitChecker: WorkloadHealthCheckFn(CheckByHealthCheckTrait),
|
||||
checkers: []WorloadHealthChecker{
|
||||
WorkloadHealthCheckFn(CheckDeploymentHealth),
|
||||
WorkloadHealthCheckFn(CheckStatefulsetHealth),
|
||||
WorkloadHealthCheckFn(CheckDaemonsetHealth),
|
||||
},
|
||||
unknownChecker: WorkloadHealthCheckFn(CheckUnknownWorkload),
|
||||
}
|
||||
for _, ro := range o {
|
||||
ro(r)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Reconcile an OAM HealthScope by keeping track of its health status.
|
||||
func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
|
||||
ctx, cancel := ctrlrec.NewReconcileContext(ctx)
|
||||
defer cancel()
|
||||
klog.InfoS("Reconcile healthScope", "healthScope", klog.KRef(req.Namespace, req.Name))
|
||||
|
||||
hs := &v1alpha2.HealthScope{}
|
||||
if err := r.client.Get(ctx, req.NamespacedName, hs); err != nil {
|
||||
return reconcile.Result{}, errors.Wrap(resource.IgnoreNotFound(err), errGetHealthScope)
|
||||
}
|
||||
|
||||
interval := longWait
|
||||
if hs.Spec.ProbeInterval != nil {
|
||||
interval = time.Duration(*hs.Spec.ProbeInterval) * time.Second
|
||||
}
|
||||
|
||||
if interval <= 0 {
|
||||
interval = longWait
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
klog.InfoS("healthScope", "uid", hs.GetUID(), "version", hs.GetResourceVersion())
|
||||
|
||||
scopeCondition, appConditions := r.GetScopeHealthStatus(ctx, hs)
|
||||
klog.V(common.LogDebug).InfoS("Successfully ran health check", "scope", hs.Name)
|
||||
r.record.Event(hs, event.Normal(reasonHealthCheck, "Successfully ran health check"))
|
||||
|
||||
elapsed := time.Since(start)
|
||||
hs.Status.ScopeHealthCondition = scopeCondition
|
||||
hs.Status.AppHealthConditions = appConditions
|
||||
|
||||
if err := r.patchHealthStatusToApplications(ctx, appConditions, hs); err != nil {
|
||||
return reconcile.Result{}, errors.Wrap(err, "cannot patch health status to application")
|
||||
}
|
||||
|
||||
requeueAfter := interval - elapsed
|
||||
if requeueAfter <= time.Second { // prevent underflow
|
||||
requeueAfter = time.Second
|
||||
}
|
||||
return reconcile.Result{RequeueAfter: requeueAfter}, errors.Wrap(r.UpdateStatus(ctx, hs), errUpdateHealthScopeStatus)
|
||||
}
|
||||
|
||||
// GetScopeHealthStatus get the status of the healthscope based on workload resources.
|
||||
func (r *Reconciler) GetScopeHealthStatus(ctx context.Context, healthScope *v1alpha2.HealthScope) (ScopeHealthCondition, []*AppHealthCondition) {
|
||||
klog.InfoS("Get scope health status", "name", healthScope.GetName())
|
||||
scopeCondition := ScopeHealthCondition{
|
||||
HealthStatus: StatusHealthy, // if no workload referenced, scope is healthy by default
|
||||
}
|
||||
|
||||
wlRefs := make([]WorkloadReference, 0)
|
||||
if len(healthScope.Spec.WorkloadReferences) > 0 {
|
||||
for _, ref := range healthScope.Spec.WorkloadReferences {
|
||||
wlRefs = append(wlRefs, WorkloadReference{
|
||||
ObjectReference: ref,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for _, app := range healthScope.Spec.AppRefs {
|
||||
wlRefs = append(wlRefs, r.createWorkloadRefs(ctx, app, healthScope.GetNamespace())...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(wlRefs) == 0 {
|
||||
return scopeCondition, []*AppHealthCondition{}
|
||||
}
|
||||
|
||||
timeout := defaultTimeout
|
||||
if healthScope.Spec.ProbeTimeout != nil {
|
||||
timeout = time.Duration(*healthScope.Spec.ProbeTimeout) * time.Second
|
||||
}
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
appfiles, appInfos := r.CollectAppfilesAndAppNames(ctx, wlRefs, healthScope.GetNamespace())
|
||||
|
||||
type wlHealthResult struct {
|
||||
name string
|
||||
envName string
|
||||
w *WorkloadHealthCondition
|
||||
}
|
||||
// process workloads concurrently
|
||||
wlHealthResultsC := make(chan wlHealthResult, len(wlRefs))
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(wlRefs))
|
||||
|
||||
for _, workloadRef := range wlRefs {
|
||||
go func(resRef WorkloadReference) {
|
||||
defer wg.Done()
|
||||
var (
|
||||
wlHealthCondition *WorkloadHealthCondition
|
||||
traitConditions []*TraitHealthCondition
|
||||
)
|
||||
|
||||
subCtx := multicluster.ContextWithClusterName(ctxWithTimeout, resRef.clusterName)
|
||||
ns := resRef.Namespace
|
||||
if ns == "" {
|
||||
ns = healthScope.GetNamespace()
|
||||
}
|
||||
if appfile, ok := appfiles[resRef]; ok {
|
||||
wlHealthCondition, traitConditions = CUEBasedHealthCheck(subCtx, r.client, resRef, ns, appfile)
|
||||
if wlHealthCondition != nil {
|
||||
klog.V(common.LogDebug).InfoS("Get health condition from CUE-based health check", "workload", resRef, "healthCondition", wlHealthCondition)
|
||||
wlHealthCondition.Traits = traitConditions
|
||||
wlHealthResultsC <- wlHealthResult{
|
||||
name: appInfos[resRef].appName,
|
||||
envName: appInfos[resRef].envName,
|
||||
w: wlHealthCondition,
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
wlHealthCondition = r.traitChecker.Check(subCtx, r.client, resRef.ObjectReference, ns)
|
||||
if wlHealthCondition != nil {
|
||||
klog.V(common.LogDebug).InfoS("Get health condition from health check trait ", "workload", resRef, "healthCondition", wlHealthCondition)
|
||||
wlHealthCondition.Traits = traitConditions
|
||||
wlHealthResultsC <- wlHealthResult{
|
||||
name: appInfos[resRef].appName,
|
||||
envName: appInfos[resRef].envName,
|
||||
w: wlHealthCondition,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, checker := range r.checkers {
|
||||
wlHealthCondition = checker.Check(subCtx, r.client, resRef.ObjectReference, ns)
|
||||
if wlHealthCondition != nil {
|
||||
klog.V(common.LogDebug).InfoS("Get health condition from built-in checker", "workload", resRef, "healthCondition", wlHealthCondition)
|
||||
// found matched checker and get health condition
|
||||
wlHealthCondition.Traits = traitConditions
|
||||
wlHealthResultsC <- wlHealthResult{
|
||||
name: appInfos[resRef].appName,
|
||||
envName: appInfos[resRef].envName,
|
||||
w: wlHealthCondition,
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
// handle unknown workload
|
||||
klog.V(common.LogDebug).InfoS("Get unknown workload", "workload", resRef)
|
||||
wlHealthCondition = r.unknownChecker.Check(subCtx, r.client, resRef.ObjectReference, ns)
|
||||
wlHealthCondition.Traits = traitConditions
|
||||
wlHealthResultsC <- wlHealthResult{
|
||||
name: appInfos[resRef].appName,
|
||||
envName: appInfos[resRef].envName,
|
||||
w: wlHealthCondition,
|
||||
}
|
||||
}(workloadRef)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(wlHealthResultsC)
|
||||
}()
|
||||
|
||||
appHealthConditions := make([]*AppHealthCondition, 0)
|
||||
var healthyCount, unhealthyCount, unknownCount int64
|
||||
for wlC := range wlHealthResultsC {
|
||||
switch wlC.w.HealthStatus { //nolint:exhaustive
|
||||
case StatusHealthy:
|
||||
healthyCount++
|
||||
case StatusUnhealthy:
|
||||
unhealthyCount++
|
||||
case StatusUnknown:
|
||||
unknownCount++
|
||||
default:
|
||||
unknownCount++
|
||||
}
|
||||
appended := false
|
||||
for _, a := range appHealthConditions {
|
||||
if a.AppName == wlC.name && a.EnvName == wlC.envName {
|
||||
a.Components = append(a.Components, wlC.w)
|
||||
appended = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !appended {
|
||||
appHealth := &AppHealthCondition{
|
||||
AppName: wlC.name,
|
||||
EnvName: wlC.envName,
|
||||
Components: []*v1alpha2.WorkloadHealthCondition{wlC.w},
|
||||
}
|
||||
appHealthConditions = append(appHealthConditions, appHealth)
|
||||
}
|
||||
}
|
||||
if unhealthyCount > 0 || unknownCount > 0 {
|
||||
// ANY unhealthy or unknown worloads make the whole scope unhealthy
|
||||
scopeCondition.HealthStatus = StatusUnhealthy
|
||||
}
|
||||
scopeCondition.Total = int64(len(wlRefs))
|
||||
scopeCondition.HealthyWorkloads = healthyCount
|
||||
scopeCondition.UnhealthyWorkloads = unhealthyCount
|
||||
scopeCondition.UnknownWorkloads = unknownCount
|
||||
|
||||
return scopeCondition, appHealthConditions
|
||||
}
|
||||
|
||||
// CollectAppfilesAndAppNames retrieve appfiles and app names for CUEBasedHealthCheck
|
||||
func (r *Reconciler) CollectAppfilesAndAppNames(ctx context.Context, refs []WorkloadReference, ns string) (map[WorkloadReference]*af.Appfile, map[WorkloadReference]AppInfo) {
|
||||
appfiles := map[WorkloadReference]*af.Appfile{}
|
||||
appNames := map[WorkloadReference]AppInfo{}
|
||||
|
||||
tmps := map[AppInfo]*af.Appfile{}
|
||||
for _, ref := range refs {
|
||||
u := &unstructured.Unstructured{}
|
||||
u.SetGroupVersionKind(ref.GroupVersionKind())
|
||||
|
||||
refNs := ref.Namespace
|
||||
if refNs == "" {
|
||||
refNs = ns
|
||||
}
|
||||
subCtx := multicluster.ContextWithClusterName(ctx, ref.clusterName)
|
||||
if err := r.client.Get(subCtx, client.ObjectKey{Name: ref.Name, Namespace: refNs}, u); err != nil {
|
||||
// no need to check error in this function
|
||||
// HealthCheckFn will handle all errors latter
|
||||
continue
|
||||
}
|
||||
|
||||
appInfo := AppInfo{
|
||||
appName: u.GetLabels()[oam.LabelAppName],
|
||||
envName: ref.envName,
|
||||
}
|
||||
if appfile, ok := tmps[appInfo]; ok {
|
||||
appfiles[ref] = appfile
|
||||
appNames[ref] = appInfo
|
||||
continue
|
||||
}
|
||||
|
||||
// create new appfile
|
||||
appfile, err := r.createAppfile(ctx, appInfo.appName, ns, appInfo.envName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
tmps[appInfo] = appfile
|
||||
|
||||
appfiles[ref] = appfile
|
||||
appNames[ref] = appInfo
|
||||
}
|
||||
return appfiles, appNames
|
||||
}
|
||||
|
||||
// UpdateStatus updates v1alpha2.HealthScope's Status with retry.RetryOnConflict
|
||||
func (r *Reconciler) UpdateStatus(ctx context.Context, hs *v1alpha2.HealthScope, opts ...client.SubResourceUpdateOption) error {
|
||||
status := hs.DeepCopy().Status
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
|
||||
if err = r.client.Get(ctx, types.NamespacedName{Namespace: hs.Namespace, Name: hs.Name}, hs); err != nil {
|
||||
return
|
||||
}
|
||||
hs.Status = status
|
||||
return r.client.Status().Update(ctx, hs, opts...)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Reconciler) patchHealthStatusToApplications(ctx context.Context, appHealthConditions []*AppHealthCondition, hs *v1alpha2.HealthScope) error {
|
||||
multiClusterAppCondition := make(map[string][]*AppHealthCondition)
|
||||
for _, appHealth := range appHealthConditions {
|
||||
multiClusterAppCondition[appHealth.AppName] = append(multiClusterAppCondition[appHealth.AppName], appHealth)
|
||||
}
|
||||
|
||||
for appName, healthConditions := range multiClusterAppCondition {
|
||||
if appName == "" {
|
||||
// for backward compatibility, skip patching status for HealthScope from v1alpha2.AppConfig
|
||||
continue
|
||||
}
|
||||
app := &v1beta1.Application{}
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Name: appName, Namespace: hs.Namespace}, app); err != nil {
|
||||
return err
|
||||
}
|
||||
if app.Status.Workflow == nil {
|
||||
continue
|
||||
}
|
||||
if !app.Status.Workflow.Finished && !app.Status.Workflow.Suspend {
|
||||
continue
|
||||
}
|
||||
copyApp := app.DeepCopy()
|
||||
componentPosition := make(map[string]int)
|
||||
for i, comp := range app.Spec.Components {
|
||||
componentPosition[comp.Name] = i
|
||||
}
|
||||
|
||||
hsRef := corev1.ObjectReference{
|
||||
APIVersion: hs.APIVersion,
|
||||
Kind: hs.Kind,
|
||||
Namespace: hs.Namespace,
|
||||
Name: hs.Name,
|
||||
UID: hs.UID,
|
||||
}
|
||||
compStatus := make([]commonapis.ApplicationComponentStatus, 0)
|
||||
for i := range healthConditions {
|
||||
healthCondition := healthConditions[i]
|
||||
sort.Sort(sortAppCondition{
|
||||
componentPosition,
|
||||
healthCondition,
|
||||
})
|
||||
compStatus = append(compStatus, constructAppCompStatus(healthCondition, hsRef)...)
|
||||
|
||||
}
|
||||
app.Status.Services = compStatus
|
||||
app.Status.SetConditions(condition.Condition{
|
||||
Type: v1beta1.TypeHealthy,
|
||||
Status: corev1.ConditionTrue,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: v1beta1.ReasonHealthy,
|
||||
})
|
||||
for _, compS := range app.Status.Services {
|
||||
if !compS.Healthy {
|
||||
app.Status.SetConditions(condition.Condition{
|
||||
Type: v1beta1.TypeHealthy,
|
||||
Status: corev1.ConditionFalse,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: v1beta1.ReasonUnhealthy,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := r.client.Status().Patch(ctx, app, client.MergeFrom(copyApp)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reconciler) createAppfile(ctx context.Context, appName, ns, envName string) (*af.Appfile, error) {
|
||||
appParser := af.NewApplicationParser(r.client, r.pd)
|
||||
if len(envName) != 0 {
|
||||
app := &v1beta1.Application{}
|
||||
if err := r.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: appName}, app); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patchedApp, err := envbinding.PatchApplicationByEnvBindingEnv(app, "", envName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appParser.GenerateAppFile(ctx, patchedApp)
|
||||
}
|
||||
|
||||
app := &v1beta1.Application{}
|
||||
if err := r.client.Get(ctx, client.ObjectKey{Name: appName, Namespace: ns}, app); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appParser.GenerateAppFile(ctx, app)
|
||||
}
|
||||
|
||||
// convert v1alpha2.AppHealthCondition to v1beta1.ApplicationComponentStatus used in Application status
|
||||
func constructAppCompStatus(appC *AppHealthCondition, hsRef corev1.ObjectReference) []commonapis.ApplicationComponentStatus {
|
||||
r := make([]commonapis.ApplicationComponentStatus, len(appC.Components))
|
||||
for i, comp := range appC.Components {
|
||||
isCompHealthy := true
|
||||
isWorkloadHealthy := comp.HealthStatus == v1alpha2.StatusHealthy
|
||||
msg := comp.CustomStatusMsg
|
||||
if len(msg) == 0 {
|
||||
msg = comp.Diagnosis
|
||||
}
|
||||
r[i] = commonapis.ApplicationComponentStatus{
|
||||
Name: comp.ComponentName,
|
||||
Env: appC.EnvName,
|
||||
WorkloadDefinition: commonapis.WorkloadGVK{
|
||||
APIVersion: comp.TargetWorkload.APIVersion,
|
||||
Kind: comp.TargetWorkload.Kind,
|
||||
},
|
||||
Healthy: isWorkloadHealthy,
|
||||
Message: msg,
|
||||
}
|
||||
if !isWorkloadHealthy {
|
||||
isCompHealthy = false
|
||||
}
|
||||
if len(comp.Traits) > 0 {
|
||||
r[i].Traits = make([]commonapis.ApplicationTraitStatus, len(comp.Traits))
|
||||
for j, tC := range comp.Traits {
|
||||
isTraitHealthy := func() bool { return tC.HealthStatus == v1alpha2.StatusHealthy }()
|
||||
r[i].Traits[j] = commonapis.ApplicationTraitStatus{
|
||||
Type: tC.Type,
|
||||
Healthy: isTraitHealthy,
|
||||
Message: tC.CustomStatusMsg,
|
||||
}
|
||||
if !isTraitHealthy {
|
||||
isCompHealthy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
r[i].Scopes = []corev1.ObjectReference{hsRef}
|
||||
r[i].Healthy = isCompHealthy
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Reconciler) createWorkloadRefs(ctx context.Context, appRef v1alpha2.AppReference, ns string) []WorkloadReference {
|
||||
wlRefs := make([]WorkloadReference, 0)
|
||||
|
||||
application := &v1beta1.Application{}
|
||||
if err := r.client.Get(ctx, types.NamespacedName{Namespace: ns, Name: appRef.AppName}, application); err != nil {
|
||||
klog.ErrorS(err, "Failed to get application")
|
||||
return wlRefs
|
||||
}
|
||||
|
||||
// ugly implementation, should be reworked in future
|
||||
decisionsMap := map[string]string{}
|
||||
var decisions []struct {
|
||||
Cluster string
|
||||
Env string
|
||||
}
|
||||
policyStatus, err := envbinding.GetEnvBindingPolicyStatus(application, "")
|
||||
if err == nil && policyStatus != nil {
|
||||
for _, env := range policyStatus.Envs {
|
||||
for _, placement := range env.Placements {
|
||||
if placement.Namespace != "" {
|
||||
decisionsMap[placement.Cluster+"."+placement.Namespace] = env.Env
|
||||
} else {
|
||||
decisionsMap[placement.Cluster] = env.Env
|
||||
}
|
||||
decisions = append(decisions, struct {
|
||||
Cluster string
|
||||
Env string
|
||||
}{
|
||||
Cluster: placement.Cluster,
|
||||
Env: env.Env,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(appRef.CompReferences) != 0 {
|
||||
for _, decision := range decisions {
|
||||
for _, comp := range appRef.CompReferences {
|
||||
wlRefs = append(wlRefs, WorkloadReference{
|
||||
ObjectReference: comp.Workload,
|
||||
clusterName: decision.Cluster,
|
||||
envName: decision.Env,
|
||||
})
|
||||
}
|
||||
}
|
||||
return wlRefs
|
||||
}
|
||||
|
||||
if application.Status.AppliedResources != nil {
|
||||
resources := application.Status.AppliedResources
|
||||
for _, rs := range resources {
|
||||
if rs.Creator == commonapis.WorkflowResourceCreator {
|
||||
o := new(unstructured.Unstructured)
|
||||
o.SetKind(rs.Kind)
|
||||
o.SetAPIVersion(rs.APIVersion)
|
||||
if err := r.client.Get(multicluster.ContextWithClusterName(ctx, rs.Cluster), client.ObjectKey{
|
||||
Name: rs.Name,
|
||||
Namespace: rs.Namespace,
|
||||
}, o); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if labels := o.GetLabels(); labels != nil {
|
||||
var envName string
|
||||
if _envName, ok := decisionsMap[rs.Cluster+"."+rs.Namespace]; ok {
|
||||
envName = _envName
|
||||
} else {
|
||||
envName = decisionsMap[rs.Cluster]
|
||||
}
|
||||
if labels[oam.WorkloadTypeLabel] != "" {
|
||||
wlRefs = append(wlRefs, WorkloadReference{
|
||||
ObjectReference: rs.ObjectReference,
|
||||
clusterName: rs.Cluster,
|
||||
envName: envName,
|
||||
})
|
||||
} else if labels[oam.TraitTypeLabel] != "" && labels[oam.LabelManageWorkloadTrait] == "true" {
|
||||
// this means this trait is a manage-Workload trait, get workload GVK and name for trait's annotation
|
||||
objectRef := corev1.ObjectReference{}
|
||||
err := json.Unmarshal([]byte(o.GetAnnotations()[oam.AnnotationWorkloadGVK]), &objectRef)
|
||||
if err != nil {
|
||||
// don't break whole check process due to this error
|
||||
continue
|
||||
}
|
||||
if o.GetAnnotations() != nil && len(o.GetAnnotations()[oam.AnnotationWorkloadName]) != 0 {
|
||||
objectRef.Name = o.GetAnnotations()[oam.AnnotationWorkloadName]
|
||||
} else {
|
||||
// use component name as default
|
||||
objectRef.Name = labels[oam.LabelAppComponent]
|
||||
}
|
||||
wlRefs = append(wlRefs, WorkloadReference{
|
||||
ObjectReference: objectRef,
|
||||
clusterName: rs.Cluster,
|
||||
envName: envName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return wlRefs
|
||||
}
|
||||
|
||||
type sortAppCondition struct {
|
||||
componentPosition map[string]int
|
||||
appCondition *AppHealthCondition
|
||||
}
|
||||
|
||||
func (s sortAppCondition) Len() int { return len(s.appCondition.Components) }
|
||||
func (s sortAppCondition) Swap(i, j int) {
|
||||
s.appCondition.Components[i], s.appCondition.Components[j] = s.appCondition.Components[j], s.appCondition.Components[i]
|
||||
}
|
||||
func (s sortAppCondition) Less(i, j int) bool {
|
||||
idx1 := s.componentPosition[s.appCondition.Components[i].ComponentName]
|
||||
idx2 := s.componentPosition[s.appCondition.Components[j].ComponentName]
|
||||
return idx1 < idx2
|
||||
}
|
||||
-347
@@ -1,347 +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 healthscope
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
|
||||
"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/oam/mock"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var (
|
||||
errNotFound = errors.New("HealthScope not found")
|
||||
// errGetResources = errors.New("cannot get resources")
|
||||
)
|
||||
|
||||
func TestHealthScope(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "HealthScope Suite")
|
||||
}
|
||||
|
||||
var _ = Describe("HealthScope Controller Reconcile Test", func() {
|
||||
mockMgr := &mock.Manager{
|
||||
Client: &test.MockClient{},
|
||||
}
|
||||
MockHealthyChecker := WorkloadHealthCheckFn(
|
||||
func(context.Context, client.Client, corev1.ObjectReference, string) *WorkloadHealthCondition {
|
||||
return &WorkloadHealthCondition{HealthStatus: StatusHealthy}
|
||||
})
|
||||
MockUnhealthyChecker := WorkloadHealthCheckFn(
|
||||
func(context.Context, client.Client, corev1.ObjectReference, string) *WorkloadHealthCondition {
|
||||
return &WorkloadHealthCondition{HealthStatus: StatusUnhealthy}
|
||||
})
|
||||
reconciler := NewReconciler(mockMgr,
|
||||
WithRecorder(event.NewNopRecorder()),
|
||||
WithChecker(MockHealthyChecker),
|
||||
)
|
||||
|
||||
hs := v1alpha2.HealthScope{Spec: v1alpha2.HealthScopeSpec{WorkloadReferences: []corev1.ObjectReference{
|
||||
// add one wlRef to trigger mockChecker
|
||||
{
|
||||
APIVersion: "mock",
|
||||
Kind: "mock",
|
||||
},
|
||||
}}}
|
||||
|
||||
BeforeEach(func() {
|
||||
logf.Log.Info("Set up resources before an unit test")
|
||||
// remove built-in checkers then fulfill mock checkers
|
||||
reconciler.checkers = []WorloadHealthChecker{}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
logf.Log.Info("Clean up resources after an unit test")
|
||||
})
|
||||
|
||||
It("Test HealthScope Not Found", func() {
|
||||
reconciler.client = &test.MockClient{
|
||||
MockGet: func(ctx context.Context,
|
||||
key client.ObjectKey, obj client.Object) error {
|
||||
return errNotFound
|
||||
},
|
||||
}
|
||||
result, err := reconciler.Reconcile(context.TODO(), reconcile.Request{})
|
||||
Expect(result).Should(Equal(reconcile.Result{}))
|
||||
Expect(err).Should(util.BeEquivalentToError(errors.Wrap(errNotFound, errGetHealthScope)))
|
||||
})
|
||||
|
||||
It("Test Reconcile UpdateHealthStatus Error", func() {
|
||||
reconciler.checkers = append(reconciler.checkers, MockHealthyChecker)
|
||||
reconciler.client = &test.MockClient{
|
||||
MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if o, ok := obj.(*v1alpha2.HealthScope); ok {
|
||||
*o = hs
|
||||
}
|
||||
if o, ok := obj.(*v1beta1.Application); ok {
|
||||
*o = v1beta1.Application{}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockStatusUpdate: func(_ context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
|
||||
return errMockErr
|
||||
},
|
||||
MockStatusPatch: func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{})
|
||||
Expect(err).Should(util.BeEquivalentToError(errors.Wrap(errMockErr, errUpdateHealthScopeStatus)))
|
||||
})
|
||||
|
||||
It("Test Reconcile Success with healthy scope", func() {
|
||||
reconciler.checkers = append(reconciler.checkers, MockHealthyChecker)
|
||||
reconciler.client = &test.MockClient{
|
||||
MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if o, ok := obj.(*v1alpha2.HealthScope); ok {
|
||||
*o = hs
|
||||
}
|
||||
if o, ok := obj.(*v1beta1.Application); ok {
|
||||
*o = v1beta1.Application{}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockStatusUpdate: func(_ context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
|
||||
return nil
|
||||
},
|
||||
MockStatusPatch: func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{})
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test Reconcile Success with unhealthy scope", func() {
|
||||
reconciler.checkers = append(reconciler.checkers, MockUnhealthyChecker)
|
||||
reconciler.client = &test.MockClient{
|
||||
MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if o, ok := obj.(*v1alpha2.HealthScope); ok {
|
||||
*o = hs
|
||||
}
|
||||
if o, ok := obj.(*v1beta1.Application); ok {
|
||||
*o = v1beta1.Application{}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockStatusUpdate: func(_ context.Context, obj client.Object, opts ...client.SubResourceUpdateOption) error {
|
||||
return nil
|
||||
},
|
||||
MockStatusPatch: func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{})
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Test GetScopeHealthStatus", func() {
|
||||
ctx := context.Background()
|
||||
mockMgr := &mock.Manager{
|
||||
Client: &test.MockClient{},
|
||||
}
|
||||
reconciler := NewReconciler(mockMgr,
|
||||
WithRecorder(event.NewNopRecorder()),
|
||||
)
|
||||
reconciler.client = test.NewMockClient()
|
||||
|
||||
hs := v1alpha2.HealthScope{}
|
||||
|
||||
var deployRef, svcRef corev1.ObjectReference
|
||||
deployRef.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind(kindDeployment))
|
||||
deployRef.Name = "deploy"
|
||||
svcRef.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind(kindService))
|
||||
|
||||
hDeploy := appsv1.Deployment{
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: &varInt1,
|
||||
},
|
||||
Status: appsv1.DeploymentStatus{
|
||||
ReadyReplicas: 1, // healthy
|
||||
},
|
||||
}
|
||||
hDeploy.SetName("deploy")
|
||||
hDeploy.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind(kindDeployment))
|
||||
|
||||
uhGeneralRef := corev1.ObjectReference{
|
||||
APIVersion: "unknown",
|
||||
Kind: "unknown",
|
||||
Name: "unhealthyGeneral",
|
||||
}
|
||||
uhDeploy := hDeploy
|
||||
uhDeploy.Status.ReadyReplicas = 0 // unhealthy
|
||||
uhGeneralWL := &unstructured.Unstructured{Object: make(map[string]interface{})}
|
||||
fieldpath.Pave(uhGeneralWL.Object).SetValue("status.readyReplicas", 0) // healthy
|
||||
fieldpath.Pave(uhGeneralWL.Object).SetValue("metadata.name", "unhealthyGeneral") // healthy
|
||||
unsupporttedWL := &unstructured.Unstructured{Object: make(map[string]interface{})}
|
||||
fieldpath.Pave(unsupporttedWL.Object).SetValue("status.unknown", 1) // healthy
|
||||
|
||||
BeforeEach(func() {
|
||||
logf.Log.Info("Set up resources before an unit test")
|
||||
hs.Spec.WorkloadReferences = []corev1.ObjectReference{}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
logf.Log.Info("Clean up resources after an unit test")
|
||||
})
|
||||
|
||||
// use Deployment checker
|
||||
It("Test healthy scope", func() {
|
||||
tests := []struct {
|
||||
caseName string
|
||||
hsWorkloadRefs []corev1.ObjectReference
|
||||
mockGetFn test.MockGetFn
|
||||
wantScopeCondition ScopeHealthCondition
|
||||
}{
|
||||
{
|
||||
caseName: "1 supportted workload(deploy)",
|
||||
hsWorkloadRefs: []corev1.ObjectReference{deployRef},
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "deploy" {
|
||||
deployObj, err := util.Object2Unstructured(hDeploy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *deployObj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantScopeCondition: ScopeHealthCondition{
|
||||
HealthStatus: StatusHealthy,
|
||||
Total: int64(1),
|
||||
HealthyWorkloads: int64(1),
|
||||
UnhealthyWorkloads: 0,
|
||||
UnknownWorkloads: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
By("Running: " + tc.caseName)
|
||||
mockClient := &test.MockClient{
|
||||
MockGet: tc.mockGetFn,
|
||||
}
|
||||
reconciler.client = mockClient
|
||||
hs.Spec.WorkloadReferences = tc.hsWorkloadRefs
|
||||
result, _ := reconciler.GetScopeHealthStatus(ctx, &hs)
|
||||
Expect(result).ShouldNot(BeNil())
|
||||
Expect(result).Should(Equal(tc.wantScopeCondition))
|
||||
}
|
||||
})
|
||||
|
||||
// use Deployment checker
|
||||
It("Test unhealthy scope", func() {
|
||||
tests := []struct {
|
||||
caseName string
|
||||
hsWorkloadRefs []corev1.ObjectReference
|
||||
mockGetFn test.MockGetFn
|
||||
wantScopeCondition ScopeHealthCondition
|
||||
}{
|
||||
{
|
||||
caseName: "1 unhealthy workload",
|
||||
hsWorkloadRefs: []corev1.ObjectReference{deployRef},
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
// return err when get svc of cw, then check fails
|
||||
if key.Name == "deploy" {
|
||||
deployObj, err := util.Object2Unstructured(uhDeploy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *deployObj
|
||||
return nil
|
||||
}
|
||||
return errMockErr
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantScopeCondition: ScopeHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
Total: int64(1),
|
||||
HealthyWorkloads: 0,
|
||||
UnhealthyWorkloads: int64(1),
|
||||
UnknownWorkloads: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "1 unsupportted workloads",
|
||||
hsWorkloadRefs: []corev1.ObjectReference{uhGeneralRef},
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "deploy" {
|
||||
deployObj, err := util.Object2Unstructured(hDeploy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *deployObj
|
||||
return nil
|
||||
}
|
||||
*o = *unsupporttedWL
|
||||
}
|
||||
return nil
|
||||
},
|
||||
wantScopeCondition: ScopeHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
Total: int64(1),
|
||||
HealthyWorkloads: 0,
|
||||
UnhealthyWorkloads: 0,
|
||||
UnknownWorkloads: int64(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
By("Running: " + tc.caseName)
|
||||
mockClient := &test.MockClient{
|
||||
MockGet: tc.mockGetFn,
|
||||
}
|
||||
reconciler.client = mockClient
|
||||
hs.Spec.WorkloadReferences = tc.hsWorkloadRefs
|
||||
result, _ := reconciler.GetScopeHealthStatus(ctx, &hs)
|
||||
Expect(result).ShouldNot(BeNil())
|
||||
Expect(result).Should(Equal(tc.wantScopeCondition))
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,596 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Crossplane 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 healthscope
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
apps "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "ns"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
errMockErr = errors.New("get error")
|
||||
varInt1 = int32(1)
|
||||
)
|
||||
|
||||
func TestCheckDeploymentHealth(t *testing.T) {
|
||||
mockClient := test.NewMockClient()
|
||||
deployRef := corev1.ObjectReference{}
|
||||
deployRef.SetGroupVersionKind(apps.SchemeGroupVersion.WithKind(kindDeployment))
|
||||
deployRef.Name = "deploy"
|
||||
|
||||
tests := []struct {
|
||||
caseName string
|
||||
mockGetFn test.MockGetFn
|
||||
wlRef corev1.ObjectReference
|
||||
expect *WorkloadHealthCondition
|
||||
}{
|
||||
{
|
||||
caseName: "not matched checker",
|
||||
wlRef: corev1.ObjectReference{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
caseName: "healthy workload",
|
||||
wlRef: deployRef,
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "deploy" {
|
||||
deployObj, err := util.Object2Unstructured(apps.Deployment{
|
||||
Spec: apps.DeploymentSpec{
|
||||
Replicas: &varInt1,
|
||||
},
|
||||
Status: apps.DeploymentStatus{
|
||||
ReadyReplicas: 1, // healthy
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *deployObj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusHealthy,
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "unhealthy for deployment not ready",
|
||||
wlRef: deployRef,
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "deploy" {
|
||||
deployObj, err := util.Object2Unstructured(apps.Deployment{
|
||||
Spec: apps.DeploymentSpec{
|
||||
Replicas: &varInt1,
|
||||
},
|
||||
Status: apps.DeploymentStatus{
|
||||
ReadyReplicas: 0, // unhealthy
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *deployObj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "unhealthy for deployment not found",
|
||||
wlRef: deployRef,
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
return errMockErr
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
func(t *testing.T) {
|
||||
mockClient.MockGet = tc.mockGetFn
|
||||
result := CheckDeploymentHealth(ctx, mockClient, tc.wlRef, namespace)
|
||||
if tc.expect == nil {
|
||||
assert.Nil(t, result, tc.caseName)
|
||||
} else {
|
||||
assert.Equal(t, tc.expect.HealthStatus, result.HealthStatus, tc.caseName)
|
||||
}
|
||||
}(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStatefulsetHealth(t *testing.T) {
|
||||
mockClient := test.NewMockClient()
|
||||
stsRef := corev1.ObjectReference{}
|
||||
stsRef.SetGroupVersionKind(apps.SchemeGroupVersion.WithKind(kindStatefulSet))
|
||||
stsRef.Name = "sts"
|
||||
|
||||
tests := []struct {
|
||||
caseName string
|
||||
mockGetFn test.MockGetFn
|
||||
wlRef corev1.ObjectReference
|
||||
expect *WorkloadHealthCondition
|
||||
}{
|
||||
{
|
||||
caseName: "not matched checker",
|
||||
wlRef: corev1.ObjectReference{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
caseName: "healthy workload",
|
||||
wlRef: stsRef,
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "sts" {
|
||||
stsObj, err := util.Object2Unstructured(apps.StatefulSet{
|
||||
Spec: apps.StatefulSetSpec{
|
||||
Replicas: &varInt1,
|
||||
},
|
||||
Status: apps.StatefulSetStatus{
|
||||
ReadyReplicas: 1, // healthy
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *stsObj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusHealthy,
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "unhealthy for statefulset not ready",
|
||||
wlRef: stsRef,
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "sts" {
|
||||
stsObj, err := util.Object2Unstructured(apps.StatefulSet{
|
||||
Spec: apps.StatefulSetSpec{
|
||||
Replicas: &varInt1,
|
||||
},
|
||||
Status: apps.StatefulSetStatus{
|
||||
ReadyReplicas: 0, // unhealthy
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *stsObj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "unhealthy for statefulset not found",
|
||||
wlRef: stsRef,
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
return errMockErr
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
func(t *testing.T) {
|
||||
mockClient.MockGet = tc.mockGetFn
|
||||
result := CheckStatefulsetHealth(ctx, mockClient, tc.wlRef, namespace)
|
||||
if tc.expect == nil {
|
||||
assert.Nil(t, result, tc.caseName)
|
||||
} else {
|
||||
assert.Equal(t, tc.expect.HealthStatus, result.HealthStatus, tc.caseName)
|
||||
}
|
||||
}(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDaemonsetHealth(t *testing.T) {
|
||||
mockClient := test.NewMockClient()
|
||||
dstRef := corev1.ObjectReference{}
|
||||
dstRef.SetGroupVersionKind(apps.SchemeGroupVersion.WithKind(kindDaemonSet))
|
||||
dstRef.Name = "dst"
|
||||
|
||||
tests := []struct {
|
||||
caseName string
|
||||
mockGetFn test.MockGetFn
|
||||
wlRef corev1.ObjectReference
|
||||
expect *WorkloadHealthCondition
|
||||
}{
|
||||
{
|
||||
caseName: "not matched checker",
|
||||
wlRef: corev1.ObjectReference{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
caseName: "healthy workload",
|
||||
wlRef: dstRef,
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "dst" {
|
||||
dstObj, err := util.Object2Unstructured(apps.DaemonSet{
|
||||
Status: apps.DaemonSetStatus{
|
||||
NumberUnavailable: 0, // healthy
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *dstObj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusHealthy,
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "unhealthy for daemonset not ready",
|
||||
wlRef: dstRef,
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "dst" {
|
||||
dstObj, err := util.Object2Unstructured(apps.DaemonSet{
|
||||
Status: apps.DaemonSetStatus{
|
||||
NumberUnavailable: 1, // unhealthy
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *dstObj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "unhealthy for daemonset not found",
|
||||
wlRef: dstRef,
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
return errMockErr
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
func(t *testing.T) {
|
||||
mockClient.MockGet = tc.mockGetFn
|
||||
result := CheckDaemonsetHealth(ctx, mockClient, tc.wlRef, namespace)
|
||||
if tc.expect == nil {
|
||||
assert.Nil(t, result, tc.caseName)
|
||||
} else {
|
||||
assert.Equal(t, tc.expect.HealthStatus, result.HealthStatus, tc.caseName)
|
||||
}
|
||||
}(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckUnknownWorkload(t *testing.T) {
|
||||
mockError := errors.New("mock error")
|
||||
mockClient := test.NewMockClient()
|
||||
unknownWL := corev1.ObjectReference{}
|
||||
tests := []struct {
|
||||
caseName string
|
||||
mockGetFn test.MockGetFn
|
||||
expect *WorkloadHealthCondition
|
||||
}{
|
||||
{
|
||||
caseName: "cannot get workload",
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
return mockError
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnknown,
|
||||
Diagnosis: errors.Wrap(mockError, errHealthCheck).Error(),
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "unknown workload with status",
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
o, _ := obj.(*unstructured.Unstructured)
|
||||
*o = unstructured.Unstructured{}
|
||||
o.Object = make(map[string]interface{})
|
||||
fieldpath.Pave(o.Object).SetValue("status.unknown", 1)
|
||||
return nil
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnknown,
|
||||
Diagnosis: fmt.Sprintf(infoFmtUnknownWorkload, "", ""),
|
||||
WorkloadStatus: "{\"unknown\":1}",
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "unknown workload without status",
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
o, _ := obj.(*unstructured.Unstructured)
|
||||
*o = unstructured.Unstructured{}
|
||||
return nil
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnknown,
|
||||
Diagnosis: fmt.Sprintf(infoFmtUnknownWorkload, "", ""),
|
||||
WorkloadStatus: "null",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
func(t *testing.T) {
|
||||
mockClient.MockGet = tc.mockGetFn
|
||||
result := CheckUnknownWorkload(ctx, mockClient, unknownWL, namespace)
|
||||
if tc.expect == nil {
|
||||
assert.Nil(t, result, tc.caseName)
|
||||
} else {
|
||||
assert.Equal(t, tc.expect, result, tc.caseName)
|
||||
}
|
||||
}(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckVersionEnabledComponent(t *testing.T) {
|
||||
deployRef := corev1.ObjectReference{}
|
||||
deployRef.SetGroupVersionKind(apps.SchemeGroupVersion.WithKind(kindDeployment))
|
||||
deployRef.Name = "main-workload"
|
||||
deployObj := apps.Deployment{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "main-workload",
|
||||
},
|
||||
Spec: apps.DeploymentSpec{
|
||||
Replicas: &varInt1,
|
||||
},
|
||||
Status: apps.DeploymentStatus{
|
||||
ReadyReplicas: 1, // healthy
|
||||
}}
|
||||
|
||||
peerDeployObj := apps.Deployment{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "peer-workload",
|
||||
},
|
||||
Spec: apps.DeploymentSpec{
|
||||
Replicas: &varInt1,
|
||||
},
|
||||
Status: apps.DeploymentStatus{
|
||||
ReadyReplicas: 1, // healthy
|
||||
}}
|
||||
|
||||
mockClient := test.NewMockClient()
|
||||
tests := []struct {
|
||||
caseName string
|
||||
mockGetFn test.MockGetFn
|
||||
mockListFn test.MockListFn
|
||||
expect *WorkloadHealthCondition
|
||||
}{
|
||||
{
|
||||
caseName: "peer workload is healthy",
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "main-workload" {
|
||||
deploy, err := util.Object2Unstructured(deployObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *deploy
|
||||
} else if key.Name == "peer-workload" {
|
||||
peerDeploy, err := util.Object2Unstructured(peerDeployObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *peerDeploy
|
||||
} else {
|
||||
o.SetLabels(map[string]string{
|
||||
oam.LabelAppComponent: "test-comp",
|
||||
oam.LabelAppName: "test-app",
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
mockListFn: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
|
||||
l, _ := list.(*unstructured.UnstructuredList)
|
||||
u := unstructured.Unstructured{}
|
||||
u.SetAPIVersion("apps/v1")
|
||||
u.SetKind("Deployment")
|
||||
u.SetName("peer-workload")
|
||||
l.Items = []unstructured.Unstructured{u}
|
||||
return nil
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusHealthy,
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "peer workload is unhealthy",
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "main-workload" {
|
||||
deploy, err := util.Object2Unstructured(deployObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *deploy
|
||||
o.SetLabels(map[string]string{
|
||||
oam.LabelAppComponent: "test-comp",
|
||||
oam.LabelAppName: "test-app",
|
||||
})
|
||||
} else if key.Name == "peer-workload" {
|
||||
peerDeployCopy := peerDeployObj.DeepCopy()
|
||||
peerDeployCopy.Status.ReadyReplicas = int32(0)
|
||||
peerDeploy, err := util.Object2Unstructured(peerDeployCopy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *peerDeploy
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
mockListFn: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
|
||||
l, _ := list.(*unstructured.UnstructuredList)
|
||||
u := unstructured.Unstructured{}
|
||||
u.SetAPIVersion("apps/v1")
|
||||
u.SetKind("Deployment")
|
||||
u.SetName("peer-workload")
|
||||
l.Items = []unstructured.Unstructured{u}
|
||||
return nil
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "error occurs when get peer workload",
|
||||
mockGetFn: func(ctx context.Context, key types.NamespacedName, obj client.Object) error {
|
||||
switch o := obj.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
if key.Name == "main-workload" {
|
||||
deploy, err := util.Object2Unstructured(deployObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *deploy
|
||||
o.SetLabels(map[string]string{
|
||||
oam.LabelAppComponent: "test-comp",
|
||||
oam.LabelAppName: "test-app",
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
},
|
||||
mockListFn: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
|
||||
return errMockErr
|
||||
},
|
||||
expect: &WorkloadHealthCondition{
|
||||
HealthStatus: StatusUnhealthy,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
func(t *testing.T) {
|
||||
mockClient.MockGet = tc.mockGetFn
|
||||
mockClient.MockList = tc.mockListFn
|
||||
checker := WorkloadHealthCheckFn(CheckDeploymentHealth)
|
||||
result := checker.Check(ctx, mockClient, deployRef, namespace)
|
||||
if tc.expect == nil {
|
||||
assert.Nil(t, result, tc.caseName)
|
||||
} else {
|
||||
assert.Equal(t, tc.expect.HealthStatus, result.HealthStatus, tc.caseName)
|
||||
}
|
||||
}(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerHealthConditionsSort(t *testing.T) {
|
||||
tests := []struct {
|
||||
caseName string
|
||||
d []string
|
||||
w []string
|
||||
}{
|
||||
{
|
||||
caseName: "all has qualified revision name",
|
||||
d: []string{"comp-v1", "comp-v2", "comp-v12"},
|
||||
w: []string{"comp-v12", "comp-v2", "comp-v1"},
|
||||
},
|
||||
{
|
||||
caseName: "part has qualified revision name",
|
||||
d: []string{"comp-v1", "comp", "comp-v2", "comp-v12"},
|
||||
w: []string{"comp-v12", "comp-v2", "comp-v1", "comp"},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
func(t *testing.T) {
|
||||
data := make(PeerHealthConditions, len(tc.d))
|
||||
want := make(PeerHealthConditions, len(tc.w))
|
||||
for i, v := range tc.d {
|
||||
data[i] = WorkloadHealthCondition{
|
||||
TargetWorkload: corev1.ObjectReference{Name: v},
|
||||
}
|
||||
}
|
||||
for i, v := range tc.w {
|
||||
want[i] = WorkloadHealthCondition{
|
||||
TargetWorkload: corev1.ObjectReference{Name: v},
|
||||
}
|
||||
}
|
||||
sort.Sort(data)
|
||||
if diff := cmp.Diff(data, want); diff != "" {
|
||||
t.Errorf("didn't get expected sorted result %s", diff)
|
||||
}
|
||||
}(t)
|
||||
}
|
||||
|
||||
}
|
||||
+5
-457
@@ -51,9 +51,7 @@ import (
|
||||
wfTypes "github.com/kubevela/workflow/pkg/types"
|
||||
|
||||
"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"
|
||||
stdv1alpha1 "github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
||||
velatypes "github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/testutil"
|
||||
@@ -388,9 +386,9 @@ var _ = Describe("Test Application Controller", func() {
|
||||
importWd := &v1beta1.WorkloadDefinition{}
|
||||
importWdJson, _ := yaml.YAMLToJSON([]byte(wDImportYaml))
|
||||
|
||||
importTd := &v1alpha2.TraitDefinition{}
|
||||
importTd := &v1beta1.TraitDefinition{}
|
||||
|
||||
webserverwd := &v1alpha2.ComponentDefinition{}
|
||||
webserverwd := &v1beta1.ComponentDefinition{}
|
||||
webserverwdJson, _ := yaml.YAMLToJSON([]byte(webComponentDefYaml))
|
||||
|
||||
BeforeEach(func() {
|
||||
@@ -536,89 +534,6 @@ var _ = Describe("Test Application Controller", func() {
|
||||
Expect(k8sClient.Delete(ctx, appwithNoTrait)).Should(BeNil())
|
||||
})
|
||||
|
||||
It("app with health policy and custom status for workload", func() {
|
||||
By("change workload and trait definition with health policy")
|
||||
ncd := &v1beta1.ComponentDefinition{}
|
||||
cDDefJson, _ := yaml.YAMLToJSON([]byte(cdDefWithHealthStatusYaml))
|
||||
Expect(json.Unmarshal(cDDefJson, ncd)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, ncd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
ntd := &v1beta1.TraitDefinition{}
|
||||
tDDefJson, _ := yaml.YAMLToJSON([]byte(tDDefWithHealthStatusYaml))
|
||||
Expect(json.Unmarshal(tDDefJson, ntd)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, ntd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
compName := "myweb-health-status"
|
||||
appWithTraitHealthStatus := appWithTrait.DeepCopy()
|
||||
appWithTraitHealthStatus.Name = "app-trait-health-status"
|
||||
|
||||
By("create the new namespace")
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "vela-test-with-health-status",
|
||||
},
|
||||
}
|
||||
appWithTraitHealthStatus.SetNamespace(ns.Name)
|
||||
Expect(k8sClient.Create(ctx, ns)).Should(BeNil())
|
||||
|
||||
app := appWithTraitHealthStatus.DeepCopy()
|
||||
app.Spec.Components[0].Name = compName
|
||||
app.Spec.Components[0].Type = "nworker"
|
||||
app.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox3","lives":"3","enemies":"alien"}`)}
|
||||
app.Spec.Components[0].Traits[0].Type = "ingress"
|
||||
app.Spec.Components[0].Traits[0].Properties = &runtime.RawExtension{Raw: []byte(`{"domain":"example.com","http":{"/":80}}`)}
|
||||
|
||||
By("apply appfile")
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
appKey := client.ObjectKey{
|
||||
Name: app.Name,
|
||||
Namespace: app.Namespace,
|
||||
}
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey})
|
||||
|
||||
deploy := &v1.Deployment{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: ns.Name, Name: "myweb-health-status"}, deploy)).Should(Succeed())
|
||||
deploy.Status.Replicas = 1
|
||||
deploy.Status.ReadyReplicas = 1
|
||||
Expect(k8sClient.Status().Update(ctx, deploy)).Should(Succeed())
|
||||
svcs := &corev1.ServiceList{}
|
||||
Expect(k8sClient.List(ctx, svcs, client.InNamespace(ns.Name))).Should(Succeed())
|
||||
Expect(len(svcs.Items)).Should(Equal(1))
|
||||
clusterIP := svcs.Items[0].Spec.ClusterIP
|
||||
|
||||
By("Check App running successfully")
|
||||
checkApp := &v1beta1.Application{}
|
||||
Eventually(func() string {
|
||||
_, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: appKey})
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
err = k8sClient.Get(ctx, appKey, checkApp)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if checkApp.Status.Phase != common.ApplicationRunning {
|
||||
fmt.Println(checkApp.Status.Conditions)
|
||||
}
|
||||
return string(checkApp.Status.Phase)
|
||||
}, 5*time.Second, time.Second).Should(BeEquivalentTo(common.ApplicationRunning))
|
||||
Expect(checkApp.Status.Services).Should(BeEquivalentTo([]common.ApplicationComponentStatus{
|
||||
{
|
||||
Name: compName,
|
||||
Namespace: app.Namespace,
|
||||
WorkloadDefinition: ncd.Spec.Workload.Definition,
|
||||
Healthy: true,
|
||||
Message: "type: busybox3,\t enemies:alien",
|
||||
Traits: []common.ApplicationTraitStatus{
|
||||
{
|
||||
Type: "ingress",
|
||||
Healthy: true,
|
||||
Message: fmt.Sprintf("type: ClusterIP,\t clusterIP:%s,\t ports:80,\t domainexample.com", clusterIP),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
|
||||
})
|
||||
|
||||
It("app with a component refer to an existing WorkloadDefinition", func() {
|
||||
appRefertoWd := appwithNoTrait.DeepCopy()
|
||||
appRefertoWd.Spec.Components[0] = common.ApplicationComponent{
|
||||
@@ -840,135 +755,7 @@ var _ = Describe("Test Application Controller", func() {
|
||||
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
|
||||
})
|
||||
|
||||
PIt("Test rollout trait all related definition features", func() {
|
||||
rolloutTdDef, err := yaml.YAMLToJSON([]byte(rolloutTraitDefinition))
|
||||
Expect(err).Should(BeNil())
|
||||
rolloutTrait := &v1beta1.TraitDefinition{}
|
||||
Expect(json.Unmarshal([]byte(rolloutTdDef), rolloutTrait)).Should(BeNil())
|
||||
ns := corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-with-rollout-trait",
|
||||
},
|
||||
}
|
||||
rolloutTrait.SetNamespace(ns.Name)
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, rolloutTrait)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
app := &v1beta1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-with-rollout",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: "myweb1",
|
||||
Type: "worker",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)},
|
||||
Traits: []common.ApplicationTrait{
|
||||
{
|
||||
Type: "rollout",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
appKey := types.NamespacedName{Namespace: ns.Name, Name: app.Name}
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey})
|
||||
checkApp := &v1beta1.Application{}
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
Expect(checkApp.Status.Phase).Should(BeEquivalentTo(common.ApplicationRunning))
|
||||
checkRollout := &stdv1alpha1.Rollout{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, checkRollout)).Should(BeNil())
|
||||
By("verify targetRevision will be filled with real compRev by context.ComponentRevName")
|
||||
Expect(checkRollout.Spec.TargetRevisionName).Should(BeEquivalentTo("myweb1-v1"))
|
||||
deploy := &v1.Deployment{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, deploy)).Should(util.NotFoundMatcher{})
|
||||
|
||||
By("update component targetComponentRev will change")
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","2000"],"image":"nginx"}`)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
testutil.ReconcileOnce(reconciler, reconcile.Request{NamespacedName: appKey})
|
||||
testutil.ReconcileOnce(reconciler, reconcile.Request{NamespacedName: appKey})
|
||||
checkApp = &v1beta1.Application{}
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
Expect(checkApp.Status.Phase).Should(BeEquivalentTo(common.ApplicationRunning))
|
||||
checkRollout = &stdv1alpha1.Rollout{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, checkRollout)).Should(BeNil())
|
||||
By("verify targetRevision will be filled with newest")
|
||||
Expect(checkRollout.Spec.TargetRevisionName).Should(BeEquivalentTo("myweb1-v2"))
|
||||
deploy = &v1.Deployment{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, deploy)).Should(util.NotFoundMatcher{})
|
||||
|
||||
By("check update rollout trait won't generate new appRevision")
|
||||
checkApp.Spec.Components[0].Traits[0].Properties = &runtime.RawExtension{Raw: []byte(`{"targetRevision":"myweb1-v3"}`)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
testutil.ReconcileOnce(reconciler, reconcile.Request{NamespacedName: appKey})
|
||||
checkApp = &v1beta1.Application{}
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
Expect(checkApp.Status.LatestRevision.Name).Should(BeEquivalentTo("app-with-rollout-v3"))
|
||||
checkRollout = &stdv1alpha1.Rollout{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, checkRollout)).Should(BeNil())
|
||||
Expect(checkRollout.Spec.TargetRevisionName).Should(BeEquivalentTo("myweb1-v3"))
|
||||
})
|
||||
|
||||
PIt("Test context revision can be supported by specify externalRevision ", func() {
|
||||
rolloutTdDef, err := yaml.YAMLToJSON([]byte(rolloutTraitDefinition))
|
||||
Expect(err).Should(BeNil())
|
||||
rolloutTrait := &v1beta1.TraitDefinition{}
|
||||
externalRevision := "my-test-revision-v1"
|
||||
Expect(json.Unmarshal([]byte(rolloutTdDef), rolloutTrait)).Should(BeNil())
|
||||
ns := corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-specify-external-revision",
|
||||
},
|
||||
}
|
||||
rolloutTrait.SetNamespace(ns.Name)
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, rolloutTrait)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
app := &v1beta1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-with-rollout",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: "myweb1",
|
||||
Type: "worker",
|
||||
ExternalRevision: externalRevision,
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)},
|
||||
Traits: []common.ApplicationTrait{
|
||||
{
|
||||
Type: "rollout",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
appKey := types.NamespacedName{Namespace: ns.Name, Name: app.Name}
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey})
|
||||
checkApp := &v1beta1.Application{}
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
Expect(checkApp.Status.Phase).Should(BeEquivalentTo(common.ApplicationRunning))
|
||||
checkRollout := &stdv1alpha1.Rollout{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, checkRollout)).Should(BeNil())
|
||||
By("verify targetRevision will be filled with real compRev by context.Revision")
|
||||
Expect(checkRollout.Spec.TargetRevisionName).Should(BeEquivalentTo(externalRevision))
|
||||
})
|
||||
|
||||
It("Test context revision can be supported by in workload ", func() {
|
||||
PIt("Test context revision can be supported by in workload ", func() {
|
||||
compDef, err := yaml.YAMLToJSON([]byte(workloadWithContextRevision))
|
||||
Expect(err).Should(BeNil())
|
||||
component := &v1beta1.ComponentDefinition{}
|
||||
@@ -1012,128 +799,6 @@ var _ = Describe("Test Application Controller", func() {
|
||||
Expect(deploy.Spec.Template.Labels["app.oam.dev/revision"]).Should(BeEquivalentTo("myweb1-v1"))
|
||||
})
|
||||
|
||||
It("Test context revision can be supported by in workload when specified componentRevision", func() {
|
||||
compDef, err := yaml.YAMLToJSON([]byte(workloadWithContextRevision))
|
||||
Expect(err).Should(BeNil())
|
||||
component := &v1beta1.ComponentDefinition{}
|
||||
Expect(json.Unmarshal([]byte(compDef), component)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, component)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
externalRevision := "my-component-rev-v1"
|
||||
ns := corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-workload-context-revision-specify-revision",
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(BeNil())
|
||||
app := &v1beta1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-test-context-revision",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: "myweb1",
|
||||
Type: "worker-revision",
|
||||
ExternalRevision: externalRevision,
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
appKey := types.NamespacedName{Namespace: ns.Name, Name: app.Name}
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey})
|
||||
checkApp := &v1beta1.Application{}
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
Expect(checkApp.Status.Phase).Should(BeEquivalentTo(common.ApplicationRunning))
|
||||
deploy := &v1.Deployment{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, deploy)).Should(BeNil())
|
||||
By("verify targetRevision will be filled with real compRev by context.Revision")
|
||||
Expect(len(deploy.Spec.Template.Labels)).Should(BeEquivalentTo(2))
|
||||
Expect(deploy.Spec.Template.Labels["app.oam.dev/revision"]).Should(BeEquivalentTo(externalRevision))
|
||||
})
|
||||
|
||||
PIt("Test rollout trait in workflow", func() {
|
||||
rolloutTdDef, err := yaml.YAMLToJSON([]byte(rolloutTraitDefinition))
|
||||
Expect(err).Should(BeNil())
|
||||
rolloutTrait := &v1beta1.TraitDefinition{}
|
||||
Expect(json.Unmarshal([]byte(rolloutTdDef), rolloutTrait)).Should(BeNil())
|
||||
|
||||
wfStepDef, err := yaml.YAMLToJSON([]byte(applyCompWfStepDefinition))
|
||||
Expect(err).Should(BeNil())
|
||||
wfStep := &v1beta1.WorkflowStepDefinition{}
|
||||
Expect(json.Unmarshal([]byte(wfStepDef), wfStep)).Should(BeNil())
|
||||
|
||||
ns := corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-with-rollout-workflow",
|
||||
},
|
||||
}
|
||||
rolloutTrait.SetNamespace(ns.Name)
|
||||
wfStep.SetNamespace(ns.Name)
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, rolloutTrait)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
Expect(k8sClient.Create(ctx, wfStep)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
app := &v1beta1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-with-rollout-workflow",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: "myweb1",
|
||||
Type: "worker",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)},
|
||||
Traits: []common.ApplicationTrait{
|
||||
{
|
||||
Type: "rollout",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Workflow: &v1beta1.Workflow{
|
||||
Steps: []workflowv1alpha1.WorkflowStep{
|
||||
{
|
||||
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
|
||||
Name: "apply",
|
||||
Type: "apply-component",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{"component" : "myweb1"}`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
appKey := types.NamespacedName{Namespace: ns.Name, Name: app.Name}
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey})
|
||||
checkApp := &v1beta1.Application{}
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
Expect(checkApp.Status.Phase).Should(BeEquivalentTo(common.ApplicationRunning))
|
||||
|
||||
By("verify workflow apply component had apply rollout")
|
||||
checkRollout := &stdv1alpha1.Rollout{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, checkRollout)).Should(BeNil())
|
||||
By("verify targetRevision will be filled with real compRev by context.ComponentRevName")
|
||||
Expect(checkRollout.Spec.TargetRevisionName).Should(BeEquivalentTo("myweb1-v1"))
|
||||
|
||||
By("verify workflow apply component didn't apply workload")
|
||||
deploy := &v1.Deployment{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: "myweb1", Namespace: ns.Name}, deploy)).Should(util.NotFoundMatcher{})
|
||||
})
|
||||
|
||||
It("application with dag workflow failed after retries", func() {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(&testing.T{}, utilfeature.DefaultFeatureGate, wffeatures.EnableSuspendOnFailure, true)()
|
||||
ns := corev1.Namespace{
|
||||
@@ -4550,7 +4215,7 @@ spec:
|
||||
}
|
||||
`
|
||||
|
||||
tdImportedYaml = `apiVersion: core.oam.dev/v1alpha2
|
||||
tdImportedYaml = `apiVersion: core.oam.dev/v1beta1
|
||||
kind: TraitDefinition
|
||||
metadata:
|
||||
name: ingress-import
|
||||
@@ -4613,7 +4278,7 @@ spec:
|
||||
}
|
||||
}`
|
||||
|
||||
webComponentDefYaml = `apiVersion: core.oam.dev/v1alpha2
|
||||
webComponentDefYaml = `apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
name: webserver
|
||||
@@ -5017,123 +4682,6 @@ spec:
|
||||
cmd?: [...string]
|
||||
}`
|
||||
|
||||
shareFsTraitDefinition = `
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: TraitDefinition
|
||||
metadata:
|
||||
name: share-fs
|
||||
namespace: default
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
outputs: pv: {
|
||||
apiVersion: "v1"
|
||||
kind: "PersistentVolume"
|
||||
metadata: {
|
||||
name: context.name
|
||||
}
|
||||
spec: {
|
||||
accessModes: ["ReadWriteMany"]
|
||||
capacity: storage: "999Gi"
|
||||
persistentVolumeReclaimPolicy: "Retain"
|
||||
csi: {
|
||||
driver: "nasplugin.csi.alibabacloud.com"
|
||||
volumeAttributes: {
|
||||
host: nasConn.MountTargetDomain
|
||||
path: "/"
|
||||
vers: "3.0"
|
||||
}
|
||||
volumeHandle: context.name
|
||||
}
|
||||
}
|
||||
}
|
||||
outputs: pvc: {
|
||||
apiVersion: "v1"
|
||||
kind: "PersistentVolumeClaim"
|
||||
metadata: {
|
||||
name: parameter.pvcName
|
||||
}
|
||||
spec: {
|
||||
accessModes: ["ReadWriteMany"]
|
||||
resources: {
|
||||
requests: {
|
||||
storage: "999Gi"
|
||||
}
|
||||
}
|
||||
volumeName: context.name
|
||||
}
|
||||
}
|
||||
parameter: {
|
||||
pvcName: string
|
||||
// +insertSecretTo=nasConn
|
||||
nasSecret: string
|
||||
}
|
||||
nasConn: {
|
||||
MountTargetDomain: string
|
||||
}
|
||||
`
|
||||
rolloutTraitDefinition = `
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: TraitDefinition
|
||||
metadata:
|
||||
name: rollout
|
||||
namespace: default
|
||||
spec:
|
||||
manageWorkload: true
|
||||
skipRevisionAffect: true
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
outputs: rollout: {
|
||||
apiVersion: "standard.oam.dev/v1alpha1"
|
||||
kind: "Rollout"
|
||||
metadata: {
|
||||
name: context.name
|
||||
namespace: context.namespace
|
||||
}
|
||||
spec: {
|
||||
targetRevisionName: parameter.targetRevision
|
||||
componentName: "myweb1"
|
||||
rolloutPlan: {
|
||||
rolloutStrategy: "IncreaseFirst"
|
||||
rolloutBatches:[
|
||||
{ replicas: 3}]
|
||||
targetSize: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parameter: {
|
||||
targetRevision: *context.revision|string
|
||||
}
|
||||
`
|
||||
applyCompWfStepDefinition = `
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: WorkflowStepDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: Apply components and traits for your workflow steps
|
||||
name: apply-component
|
||||
namespace: vela-system
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
import (
|
||||
"vela/op"
|
||||
)
|
||||
|
||||
// apply components and traits
|
||||
apply: op.#ApplyComponent & {
|
||||
component: parameter.component
|
||||
}
|
||||
parameter: {
|
||||
// +usage=Declare the name of the component
|
||||
component: string
|
||||
}
|
||||
`
|
||||
|
||||
k8sObjectsComponentDefinitionYaml = `
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
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 assemble
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
var _ = Describe("Test Assemble Options", func() {
|
||||
It("test assemble", func() {
|
||||
var (
|
||||
compName = "test-comp"
|
||||
namespace = "default"
|
||||
)
|
||||
|
||||
appRev := &v1beta1.ApplicationRevision{}
|
||||
b, err := os.ReadFile("./testdata/apprevision.yaml")
|
||||
/* appRevision test data is generated based on below application
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: test-assemble
|
||||
spec:
|
||||
components:
|
||||
- name: test-comp
|
||||
type: webservice
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
port: 8000
|
||||
traits:
|
||||
- type: ingress
|
||||
properties:
|
||||
domain: localhost
|
||||
http:
|
||||
"/": 8000
|
||||
*/
|
||||
Expect(err).Should(BeNil())
|
||||
err = yaml.Unmarshal(b, appRev)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
ao := NewAppManifests(appRev, appParser)
|
||||
workloads, traits, _, err := ao.GroupAssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("Verify amount of result resources")
|
||||
allResources, err := ao.AssembledManifests()
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(allResources)).Should(Equal(3))
|
||||
|
||||
By("Verify amount of result grouped resources")
|
||||
Expect(len(workloads)).Should(Equal(1))
|
||||
Expect(len(traits[compName])).Should(Equal(2))
|
||||
|
||||
By("Verify workload metadata (name, namespace, labels, annotations, ownerRef)")
|
||||
wl := workloads[compName]
|
||||
Expect(wl.GetName()).Should(Equal(compName))
|
||||
Expect(wl.GetNamespace()).Should(Equal(namespace))
|
||||
labels := wl.GetLabels()
|
||||
labelKeys := make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
labelKeys = append(labelKeys, k)
|
||||
}
|
||||
Expect(labelKeys).Should(ContainElements(
|
||||
oam.LabelAppName,
|
||||
oam.LabelAppRevision,
|
||||
oam.LabelAppRevisionHash,
|
||||
oam.LabelAppComponent,
|
||||
oam.LabelAppComponentRevision,
|
||||
oam.WorkloadTypeLabel,
|
||||
oam.LabelOAMResourceType))
|
||||
Expect(len(wl.GetAnnotations())).Should(Equal(1))
|
||||
|
||||
By("Verify trait metadata (name, namespace, labels, annotations, ownerRef)")
|
||||
trait := traits[compName][0]
|
||||
Expect(trait.GetName()).Should(ContainSubstring(compName))
|
||||
Expect(trait.GetNamespace()).Should(Equal(namespace))
|
||||
labels = trait.GetLabels()
|
||||
labelKeys = make([]string, 0, len(labels))
|
||||
for k := range labels {
|
||||
labelKeys = append(labelKeys, k)
|
||||
}
|
||||
Expect(labelKeys).Should(ContainElements(
|
||||
oam.LabelAppName,
|
||||
oam.LabelAppRevision,
|
||||
oam.LabelAppRevisionHash,
|
||||
oam.LabelAppComponent,
|
||||
oam.LabelAppComponentRevision,
|
||||
oam.TraitTypeLabel,
|
||||
oam.LabelOAMResourceType))
|
||||
Expect(len(wl.GetAnnotations())).Should(Equal(1))
|
||||
})
|
||||
})
|
||||
-3
@@ -38,7 +38,6 @@ import (
|
||||
|
||||
"github.com/kubevela/workflow/pkg/cue/packages"
|
||||
|
||||
"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/pkg/appfile"
|
||||
@@ -81,8 +80,6 @@ var _ = BeforeSuite(func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(cfg).ToNot(BeNil())
|
||||
|
||||
err = v1alpha2.SchemeBuilder.AddToScheme(testScheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = v1alpha1.SchemeBuilder.AddToScheme(testScheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
err = v1beta1.SchemeBuilder.AddToScheme(testScheme)
|
||||
-16
@@ -25,8 +25,6 @@ spec:
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
port: 8000
|
||||
scopes:
|
||||
healthscopes.core.oam.dev: sample-health-scope
|
||||
traits:
|
||||
- properties:
|
||||
domain: localhost
|
||||
@@ -141,20 +139,6 @@ spec:
|
||||
kind: Deployment
|
||||
type: deployments.apps
|
||||
status: {}
|
||||
scopeGVK:
|
||||
healthscopes.core.oam.dev/v1beta1:
|
||||
group: core.oam.dev
|
||||
version: v1beta1
|
||||
kind: HealthScope
|
||||
scopeDefinitions:
|
||||
healthscopes.core.oam.dev:
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ScopeDefinition
|
||||
metadata: { }
|
||||
spec:
|
||||
definitionRef:
|
||||
name: healthscopes.core.oam.dev
|
||||
version: v1beta1
|
||||
resourcesConfigMap:
|
||||
name: test-assemble-v1
|
||||
traitDefinitions:
|
||||
+5
-5
@@ -179,7 +179,7 @@ var _ = Describe("Test Application with GC options", func() {
|
||||
|
||||
rtList := &v1beta1.ResourceTrackerList{}
|
||||
Expect(k8sClient.List(ctx, rtList, listOpts...))
|
||||
Expect(len(rtList.Items)).Should(Equal(7))
|
||||
Expect(len(rtList.Items)).Should(Equal(6))
|
||||
|
||||
By("delete one resourceTracker to test the gc of legacy resources")
|
||||
testRT := &v1beta1.ResourceTracker{
|
||||
@@ -257,7 +257,7 @@ var _ = Describe("Test Application with GC options", func() {
|
||||
By("check the resourceTrackers number")
|
||||
newRTList := &v1beta1.ResourceTrackerList{}
|
||||
Expect(k8sClient.List(ctx, newRTList, listOpts...))
|
||||
Expect(len(newRTList.Items)).Should(Equal(4))
|
||||
Expect(len(newRTList.Items)).Should(Equal(3))
|
||||
|
||||
By("delete all resources")
|
||||
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
|
||||
@@ -351,7 +351,7 @@ var _ = Describe("Test Application with GC options", func() {
|
||||
|
||||
rtList := &v1beta1.ResourceTrackerList{}
|
||||
Expect(k8sClient.List(ctx, rtList, listOpts...))
|
||||
Expect(len(rtList.Items)).Should(Equal(7))
|
||||
Expect(len(rtList.Items)).Should(Equal(6))
|
||||
|
||||
By("delete one resourceTracker to test the gc of legacy resources")
|
||||
testRT := &v1beta1.ResourceTracker{
|
||||
@@ -466,7 +466,7 @@ var _ = Describe("Test Application with GC options", func() {
|
||||
|
||||
rtList := &v1beta1.ResourceTrackerList{}
|
||||
Expect(k8sClient.List(ctx, rtList, listOpts...))
|
||||
Expect(len(rtList.Items)).Should(Equal(2))
|
||||
Expect(len(rtList.Items)).Should(Equal(1))
|
||||
|
||||
By("delete all resources")
|
||||
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
|
||||
@@ -556,7 +556,7 @@ var _ = Describe("Test Application with GC options", func() {
|
||||
|
||||
rtList := &v1beta1.ResourceTrackerList{}
|
||||
Expect(k8sClient.List(ctx, rtList, listOpts...)).Should(BeNil())
|
||||
Expect(len(rtList.Items)).Should(Equal(2))
|
||||
Expect(len(rtList.Items)).Should(Equal(1))
|
||||
workerList := &v1.DeploymentList{}
|
||||
Expect(k8sClient.List(ctx, workerList, listOpts...)).Should(BeNil())
|
||||
Expect(len(workerList.Items)).Should(Equal(3))
|
||||
+3
-4
@@ -26,6 +26,7 @@ import (
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
|
||||
configprovider "github.com/oam-dev/kubevela/pkg/config/provider"
|
||||
ctrlutil "github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/apply"
|
||||
|
||||
@@ -52,7 +53,7 @@ import (
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/appfile"
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/assemble"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/application/assemble"
|
||||
velaprocess "github.com/oam-dev/kubevela/pkg/cue/process"
|
||||
"github.com/oam-dev/kubevela/pkg/monitor/metrics"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
@@ -462,6 +463,7 @@ func (h *AppHandler) prepareWorkloadAndManifests(ctx context.Context,
|
||||
}
|
||||
// cluster info are secrets stored in the control plane cluster
|
||||
ctxData.ClusterVersion = multicluster.GetVersionInfoFromObject(pkgmulticluster.WithCluster(ctx, types.ClusterLocalName), h.r.Client, ctxData.Cluster)
|
||||
ctxData.CompRevision, _ = ctrlutil.ComputeSpecHash(comp)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "GenerateComponentManifest")
|
||||
@@ -469,9 +471,6 @@ func (h *AppHandler) prepareWorkloadAndManifests(ctx context.Context,
|
||||
if err := af.SetOAMContract(manifest); err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "SetOAMContract")
|
||||
}
|
||||
if err := h.HandleComponentsRevision(contextWithComponent(ctx, &comp), []*types.ComponentManifest{manifest}); err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "HandleComponentsRevision")
|
||||
}
|
||||
return wl, manifest, nil
|
||||
}
|
||||
|
||||
-81
@@ -170,87 +170,6 @@ var _ = Describe("Test Application workflow generator", func() {
|
||||
Expect(taskRunner[1].Name()).Should(BeEquivalentTo("myweb2"))
|
||||
})
|
||||
|
||||
It("Test render component", func() {
|
||||
cd := &oamcore.ComponentDefinition{}
|
||||
td := &oamcore.TraitDefinition{}
|
||||
|
||||
defJson, err := yaml.YAMLToJSON([]byte(componentDefYaml))
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(json.Unmarshal(defJson, cd)).Should(BeNil())
|
||||
cd.SetNamespace("vela-system")
|
||||
Expect(k8sClient.Create(ctx, cd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
rolloutTdDef, err := yaml.YAMLToJSON([]byte(rolloutTraitDefinition))
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(json.Unmarshal(rolloutTdDef, td)).Should(BeNil())
|
||||
td.SetNamespace("vela-system")
|
||||
Expect(k8sClient.Create(ctx, td)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
app := &oamcore.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-test",
|
||||
Namespace: namespaceName,
|
||||
},
|
||||
Spec: oamcore.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: "myweb1",
|
||||
Type: "worker",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)},
|
||||
Traits: []common.ApplicationTrait{
|
||||
{
|
||||
Type: "rollout",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
af, err := appParser.GenerateAppFile(ctx, app)
|
||||
Expect(err).Should(BeNil())
|
||||
_, err = af.GeneratePolicyManifests(context.Background())
|
||||
Expect(err).Should(BeNil())
|
||||
apprev := &oamcore.ApplicationRevision{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-test-v1",
|
||||
Namespace: namespaceName,
|
||||
},
|
||||
Spec: oamcore.ApplicationRevisionSpec{
|
||||
ApplicationRevisionCompressibleFields: oamcore.ApplicationRevisionCompressibleFields{
|
||||
Application: *app.DeepCopy(),
|
||||
ComponentDefinitions: make(map[string]*oamcore.ComponentDefinition),
|
||||
WorkloadDefinitions: make(map[string]oamcore.WorkloadDefinition),
|
||||
TraitDefinitions: make(map[string]*oamcore.TraitDefinition),
|
||||
ScopeDefinitions: make(map[string]oamcore.ScopeDefinition),
|
||||
},
|
||||
},
|
||||
}
|
||||
apprev.Spec.ComponentDefinitions["worker"] = cd.DeepCopy()
|
||||
apprev.Spec.TraitDefinitions["rollout"] = td.DeepCopy()
|
||||
Expect(k8sClient.Create(ctx, apprev)).Should(BeNil())
|
||||
|
||||
handler, err := NewAppHandler(ctx, reconciler, app, appParser)
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
renderFunc := handler.renderComponentFunc(appParser, apprev, af)
|
||||
comp := common.ApplicationComponent{
|
||||
Name: "myweb1",
|
||||
Type: "worker",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)},
|
||||
Traits: []common.ApplicationTrait{
|
||||
{
|
||||
Type: "rollout",
|
||||
},
|
||||
},
|
||||
}
|
||||
_, _, err = renderFunc(ctx, comp, nil, "", "", "")
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test generate application workflow with dependsOn", func() {
|
||||
app := &oamcore.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
-258
@@ -18,15 +18,11 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/kubevela/pkg/util/k8s"
|
||||
"github.com/pkg/errors"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -46,23 +42,16 @@ import (
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"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"
|
||||
helmapi "github.com/oam-dev/kubevela/pkg/appfile/helm/flux2apis"
|
||||
"github.com/oam-dev/kubevela/pkg/auth"
|
||||
"github.com/oam-dev/kubevela/pkg/cache"
|
||||
"github.com/oam-dev/kubevela/pkg/component"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/cue/process"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
"github.com/oam-dev/kubevela/pkg/monitor/metrics"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
"github.com/oam-dev/kubevela/pkg/policy/envbinding"
|
||||
pkgutils "github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
@@ -80,31 +69,6 @@ const (
|
||||
ManifestKeyScopes = "Scopes"
|
||||
)
|
||||
|
||||
const rolloutTraitName = "rollout"
|
||||
|
||||
// contextWithComponent records ApplicationComponent in context
|
||||
func contextWithComponent(ctx context.Context, component *common.ApplicationComponent) context.Context {
|
||||
return context.WithValue(ctx, ComponentContextKey, component)
|
||||
}
|
||||
|
||||
// componentInContext extract ApplicationComponent from context
|
||||
func componentInContext(ctx context.Context) *common.ApplicationComponent {
|
||||
comp, _ := ctx.Value(ComponentContextKey).(*common.ApplicationComponent)
|
||||
return comp
|
||||
}
|
||||
|
||||
func _containsRolloutTrait(ctx context.Context) bool {
|
||||
comp := componentInContext(ctx)
|
||||
if comp != nil {
|
||||
for _, trait := range comp.Traits {
|
||||
if trait.Type == rolloutTraitName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
// DisableAllComponentRevision disable component revision creation
|
||||
DisableAllComponentRevision = false
|
||||
@@ -130,13 +94,6 @@ func replicaKeyFromContext(ctx context.Context) string {
|
||||
return key
|
||||
}
|
||||
|
||||
func (h *AppHandler) getComponentRevisionNamespace(ctx context.Context) string {
|
||||
if ns, ok := ctx.Value(ComponentNamespaceContextKey).(string); ok && ns != "" {
|
||||
return ns
|
||||
}
|
||||
return h.app.Namespace
|
||||
}
|
||||
|
||||
func (h *AppHandler) createResourcesConfigMap(ctx context.Context,
|
||||
appRev *v1beta1.ApplicationRevision,
|
||||
comps []*types.ComponentManifest,
|
||||
@@ -576,152 +533,6 @@ func deepEqualAppInRevision(old, new *v1beta1.ApplicationRevision) bool {
|
||||
return deepEqualAppSpec(&old.Spec.Application, &new.Spec.Application)
|
||||
}
|
||||
|
||||
// HandleComponentsRevision manages Component revisions
|
||||
// 1. if update component create a new component Revision
|
||||
// 2. check all componentTrait rely on componentRevName, if yes fill it
|
||||
func (h *AppHandler) HandleComponentsRevision(ctx context.Context, compManifests []*types.ComponentManifest) error {
|
||||
if DisableAllComponentRevision {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, cm := range compManifests {
|
||||
|
||||
// external revision specified
|
||||
if len(cm.ExternalRevision) != 0 {
|
||||
if err := h.handleComponentRevisionNameSpecified(ctx, cm); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := h.handleComponentRevisionNameUnspecified(ctx, cm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleComponentRevisionNameSpecified create controllerRevision which use specified revisionName.
|
||||
// If the controllerRevision already exist, we just return
|
||||
func (h *AppHandler) handleComponentRevisionNameSpecified(ctx context.Context, comp *types.ComponentManifest) error {
|
||||
revisionName := comp.ExternalRevision
|
||||
cr := &appsv1.ControllerRevision{}
|
||||
|
||||
if err := h.r.Client.Get(auth.ContextWithUserInfo(ctx, h.app), client.ObjectKey{Namespace: h.getComponentRevisionNamespace(ctx), Name: revisionName}, cr); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return errors.Wrapf(err, "failed to get controllerRevision:%s", revisionName)
|
||||
}
|
||||
|
||||
// we should create one
|
||||
hash, err := ComputeComponentRevisionHash(comp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
comp.RevisionHash = hash
|
||||
comp.RevisionName = revisionName
|
||||
if err := h.createControllerRevision(ctx, comp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// when controllerRevision not exist handle replace context.RevisionName
|
||||
for _, trait := range comp.Traits {
|
||||
if err := replaceComponentRevisionContext(trait, comp.RevisionName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
comp.RevisionHash = cr.GetLabels()[oam.LabelComponentRevisionHash]
|
||||
comp.RevisionName = revisionName
|
||||
|
||||
for _, trait := range comp.Traits {
|
||||
if err := replaceComponentRevisionContext(trait, comp.RevisionName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleComponentRevisionNameUnspecified create new controllerRevision when external revision name unspecified
|
||||
func (h *AppHandler) handleComponentRevisionNameUnspecified(ctx context.Context, comp *types.ComponentManifest) error {
|
||||
hash, err := ComputeComponentRevisionHash(comp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
comp.RevisionHash = hash
|
||||
|
||||
crList := &appsv1.ControllerRevisionList{}
|
||||
listOpts := []client.ListOption{client.MatchingLabels{
|
||||
oam.LabelControllerRevisionComponent: pkgutils.EscapeResourceNameToLabelValue(comp.Name),
|
||||
}, client.InNamespace(h.getComponentRevisionNamespace(ctx))}
|
||||
if err := h.r.List(auth.ContextWithUserInfo(ctx, h.app), crList, listOpts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var maxRevisionNum int64
|
||||
needNewRevision := true
|
||||
for _, existingCR := range crList.Items {
|
||||
if existingCR.Revision > maxRevisionNum {
|
||||
maxRevisionNum = existingCR.Revision
|
||||
}
|
||||
if existingCR.GetLabels()[oam.LabelComponentRevisionHash] == comp.RevisionHash {
|
||||
existingComp, err := util.RawExtension2Component(existingCR.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// let componentManifest2Component func replace context.Name's placeHolder to guarantee content of them to be same.
|
||||
comp.RevisionName = existingCR.GetName()
|
||||
currentComp, err := componentManifest2Component(comp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// further check whether it's truly identical, even hash value is equal
|
||||
if checkComponentSpecEqual(existingComp, currentComp) {
|
||||
comp.RevisionName = existingCR.GetName()
|
||||
// found identical revision already exisits
|
||||
// skip creating new one
|
||||
needNewRevision = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if needNewRevision {
|
||||
comp.RevisionName = utils.ConstructRevisionName(comp.Name, maxRevisionNum+1)
|
||||
if err := h.createControllerRevision(ctx, comp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, trait := range comp.Traits {
|
||||
if err := replaceComponentRevisionContext(trait, comp.RevisionName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkComponentSpecEqual(a, b *v1alpha2.Component) bool {
|
||||
if reflect.DeepEqual(a, b) {
|
||||
return true
|
||||
}
|
||||
au, err := util.RawExtension2Unstructured(&a.Spec.Workload)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
bu, err := util.RawExtension2Unstructured(&b.Spec.Workload)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(au.Object["spec"], bu.Object["spec"]) {
|
||||
return false
|
||||
}
|
||||
return reflect.DeepEqual(a.Spec.Helm, b.Spec.Helm)
|
||||
}
|
||||
|
||||
// ComputeComponentRevisionHash to compute component hash
|
||||
func ComputeComponentRevisionHash(comp *types.ComponentManifest) (string, error) {
|
||||
compRevisionHash := struct {
|
||||
@@ -751,64 +562,6 @@ func ComputeComponentRevisionHash(comp *types.ComponentManifest) (string, error)
|
||||
return utils.ComputeSpecHash(&compRevisionHash)
|
||||
}
|
||||
|
||||
// createControllerRevision records snapshot of a component
|
||||
func (h *AppHandler) createControllerRevision(ctx context.Context, cm *types.ComponentManifest) error {
|
||||
comp, err := componentManifest2Component(cm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
revision, _ := utils.ExtractRevision(cm.RevisionName)
|
||||
cr := &appsv1.ControllerRevision{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: cm.RevisionName,
|
||||
Namespace: h.getComponentRevisionNamespace(ctx),
|
||||
Labels: map[string]string{
|
||||
oam.LabelAppComponent: pkgutils.EscapeResourceNameToLabelValue(cm.Name),
|
||||
oam.LabelAppCluster: multicluster.ClusterNameInContext(ctx),
|
||||
oam.LabelAppEnv: envbinding.EnvNameInContext(ctx),
|
||||
oam.LabelControllerRevisionComponent: pkgutils.EscapeResourceNameToLabelValue(cm.Name),
|
||||
oam.LabelComponentRevisionHash: cm.RevisionHash,
|
||||
},
|
||||
},
|
||||
Revision: int64(revision),
|
||||
Data: *util.Object2RawExtension(comp),
|
||||
}
|
||||
common.NewOAMObjectReferenceFromObject(cm.StandardWorkload).AddLabelsToObject(cr)
|
||||
if !utilfeature.DefaultMutableFeatureGate.Enabled(features.LegacyComponentRevision) && !_containsRolloutTrait(ctx) {
|
||||
return nil
|
||||
}
|
||||
return h.resourceKeeper.DispatchComponentRevision(ctx, cr)
|
||||
}
|
||||
|
||||
func componentManifest2Component(cm *types.ComponentManifest) (*v1alpha2.Component, error) {
|
||||
c := &v1alpha2.Component{}
|
||||
c.SetGroupVersionKind(v1alpha2.ComponentGroupVersionKind)
|
||||
c.SetName(cm.Name)
|
||||
wl := &unstructured.Unstructured{}
|
||||
if cm.StandardWorkload != nil {
|
||||
// use revision name replace compRev placeHolder
|
||||
if err := replaceComponentRevisionContext(cm.StandardWorkload, cm.RevisionName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wl = cm.StandardWorkload.DeepCopy()
|
||||
util.RemoveLabels(wl, []string{oam.LabelAppRevision})
|
||||
}
|
||||
c.Spec.Workload = *util.Object2RawExtension(wl)
|
||||
if len(cm.PackagedWorkloadResources) > 0 {
|
||||
helm := &common.Helm{}
|
||||
for _, helmResource := range cm.PackagedWorkloadResources {
|
||||
if helmResource.GetKind() == helmapi.HelmReleaseGVK.Kind {
|
||||
helm.Release = *util.Object2RawExtension(helmResource)
|
||||
}
|
||||
if helmResource.GetKind() == helmapi.HelmRepositoryGVK.Kind {
|
||||
helm.Repository = *util.Object2RawExtension(helmResource)
|
||||
}
|
||||
}
|
||||
c.Spec.Helm = helm
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// FinalizeAndApplyAppRevision finalise AppRevision object and apply it
|
||||
func (h *AppHandler) FinalizeAndApplyAppRevision(ctx context.Context) error {
|
||||
if DisableAllApplicationRevision {
|
||||
@@ -900,17 +653,6 @@ func (h *AppHandler) UpdateAppLatestRevisionStatus(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceComponentRevisionContext(u *unstructured.Unstructured, compRevName string) error {
|
||||
str := string(util.JSONMarshal(u))
|
||||
if strings.Contains(str, process.ComponentRevisionPlaceHolder) {
|
||||
newStr := strings.ReplaceAll(str, process.ComponentRevisionPlaceHolder, compRevName)
|
||||
if err := json.Unmarshal([]byte(newStr), u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateApplicationRevisionStatus update application revision status
|
||||
func (h *AppHandler) UpdateApplicationRevisionStatus(ctx context.Context, appRev *v1beta1.ApplicationRevision, wfStatus *common.WorkflowStatus) {
|
||||
if appRev == nil || DisableAllApplicationRevision {
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
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"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/oam/testutil"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("Test application controller clean up ", func() {
|
||||
ctx := context.TODO()
|
||||
var namespace string
|
||||
var ns v1.Namespace
|
||||
|
||||
cd := &v1beta1.ComponentDefinition{}
|
||||
cdDefJson, _ := yaml.YAMLToJSON([]byte(normalCompDefYaml))
|
||||
|
||||
BeforeEach(func() {
|
||||
namespace = randomNamespaceName("clean-up-revision-test")
|
||||
ns = v1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, &ns)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
Expect(json.Unmarshal(cdDefJson, cd)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, cd.DeepCopy())).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
By("[TEST] Clean up resources after an integration test")
|
||||
Expect(k8sClient.Delete(ctx, &ns)).Should(SatisfyAny(BeNil()))
|
||||
})
|
||||
|
||||
It("Test clean up appRevision", func() {
|
||||
appName := "app-1"
|
||||
appKey := types.NamespacedName{Namespace: namespace, Name: appName}
|
||||
app := getApp(appName, namespace, "normal-worker")
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
checkApp := new(v1beta1.Application)
|
||||
for i := 0; i < appRevisionLimit+1; i++ {
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, i)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, ctrl.Request{NamespacedName: appKey})
|
||||
}
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(namespace),
|
||||
client.MatchingLabels{
|
||||
oam.LabelAppName: appName,
|
||||
},
|
||||
}
|
||||
appRevisionList := new(v1beta1.ApplicationRevisionList)
|
||||
Eventually(func() error {
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
|
||||
By("create new appRevision will remove appRevison1")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the oldest revision")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
|
||||
By("update app again will gc appRevision2")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property = fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 7)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: appName + "-v2"}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the revision-2")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test clean up appDeployment using appRevision", func() {
|
||||
appName := "app-4"
|
||||
appKey := types.NamespacedName{Namespace: namespace, Name: appName}
|
||||
app := getApp(appName, namespace, "normal-worker")
|
||||
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationAppRollout, "true")
|
||||
metav1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationRollingComponent, "comp1")
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
checkApp := new(v1beta1.Application)
|
||||
for i := 0; i < appRevisionLimit+1; i++ {
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, i)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, ctrl.Request{NamespacedName: appKey})
|
||||
}
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(namespace),
|
||||
client.MatchingLabels{
|
||||
oam.LabelAppName: appName,
|
||||
},
|
||||
}
|
||||
appRevisionList := new(v1beta1.ApplicationRevisionList)
|
||||
Eventually(func() error {
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
|
||||
By("create new appRevision will remove appRevison1")
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
property := fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
_, err := reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
Eventually(func() error {
|
||||
if _, err = reconciler.Reconcile(context.TODO(), ctrl.Request{NamespacedName: appKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+1 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+1, len(appRevisionList.Items))
|
||||
}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the oldest revision")
|
||||
}
|
||||
if res, err := util.CheckAppRevision(appRevisionList.Items, []int{2, 3, 4, 5, 6, 7}); err != nil || !res {
|
||||
return fmt.Errorf("appRevision collection mismatch")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*10, time.Second*2).Should(BeNil())
|
||||
})
|
||||
})
|
||||
-170
@@ -30,23 +30,16 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"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/standard.oam.dev/v1alpha1"
|
||||
oamtypes "github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/appfile"
|
||||
"github.com/oam-dev/kubevela/pkg/cue/process"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
@@ -218,7 +211,6 @@ var _ = Describe("test generate revision ", func() {
|
||||
comps, err = generatedAppfile.GenerateComponentManifests()
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(handler.PrepareCurrentAppRevision(ctx, generatedAppfile)).Should(Succeed())
|
||||
Expect(handler.HandleComponentsRevision(ctx, comps)).Should(Succeed())
|
||||
Expect(handler.FinalizeAndApplyAppRevision(ctx)).Should(Succeed())
|
||||
Expect(handler.ProduceArtifacts(context.Background(), comps, nil)).Should(Succeed())
|
||||
Expect(handler.UpdateAppLatestRevisionStatus(ctx)).Should(Succeed())
|
||||
@@ -249,22 +241,6 @@ var _ = Describe("test generate revision ", func() {
|
||||
Expect(ctrlOwner).ShouldNot(BeNil())
|
||||
Expect(ctrlOwner.Kind).Should(Equal(v1beta1.ApplicationKind))
|
||||
Expect(len(curAppRevision.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
Expect(curAppRevision.GetOwnerReferences()[0].Kind).Should(Equal(v1alpha2.ApplicationKind))
|
||||
|
||||
By("Verify component revision")
|
||||
expectCompRevName := "express-server-v1"
|
||||
Expect(comps[0].RevisionName).Should(Equal(expectCompRevName))
|
||||
gotCR := &appsv1.ControllerRevision{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: expectCompRevName, Namespace: namespaceName}, gotCR)).Should(Succeed())
|
||||
Expect(gotCR.Revision).Should(Equal(int64(1)))
|
||||
gotComp, err := util.RawExtension2Component(gotCR.Data)
|
||||
Expect(err).Should(BeNil())
|
||||
expectWorkload := comps[0].StandardWorkload.DeepCopy()
|
||||
util.RemoveLabels(expectWorkload, []string{oam.LabelAppRevision, oam.LabelAppRevisionHash, oam.LabelAppComponentRevision})
|
||||
var gotWL = unstructured.Unstructured{}
|
||||
err = json.Unmarshal(gotComp.Spec.Workload.Raw, &gotWL)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(&gotWL, expectWorkload)).Should(BeEmpty())
|
||||
|
||||
By("Apply the application again without any spec change")
|
||||
annoKey2 := "testKey2"
|
||||
@@ -273,7 +249,6 @@ var _ = Describe("test generate revision ", func() {
|
||||
comps, err = generatedAppfile.GenerateComponentManifests()
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(handler.PrepareCurrentAppRevision(ctx, generatedAppfile)).Should(Succeed())
|
||||
Expect(handler.HandleComponentsRevision(ctx, comps)).Should(Succeed())
|
||||
Expect(handler.FinalizeAndApplyAppRevision(ctx)).Should(Succeed())
|
||||
Expect(handler.ProduceArtifacts(context.Background(), comps, nil)).Should(Succeed())
|
||||
Eventually(
|
||||
@@ -298,18 +273,6 @@ var _ = Describe("test generate revision ", func() {
|
||||
time.Second*5, time.Millisecond*500).Should(BeNil())
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(curAppRevision.GetLabels()[oam.LabelAppRevisionHash]).Should(Equal(appHash1))
|
||||
gotComp, err = util.RawExtension2Component(gotCR.Data)
|
||||
Expect(err).Should(BeNil())
|
||||
expectWorkload = comps[0].StandardWorkload.DeepCopy()
|
||||
util.RemoveLabels(expectWorkload, []string{oam.LabelAppRevision, oam.LabelAppRevisionHash, oam.LabelAppComponentRevision})
|
||||
Expect(cmp.Diff(gotComp.Spec.Workload, *util.Object2RawExtension(expectWorkload))).Should(BeEmpty())
|
||||
|
||||
By("Verify component revision is not changed")
|
||||
expectCompRevName = "express-server-v1"
|
||||
Expect(comps[0].RevisionName).Should(Equal(expectCompRevName))
|
||||
gotCR = &appsv1.ControllerRevision{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: expectCompRevName, Namespace: namespaceName}, gotCR)).Should(Succeed())
|
||||
Expect(gotCR.Revision).Should(Equal(int64(1)))
|
||||
|
||||
By("Change the application and apply again")
|
||||
// bump the image tag
|
||||
@@ -325,7 +288,6 @@ var _ = Describe("test generate revision ", func() {
|
||||
Expect(err).Should(Succeed())
|
||||
handler.app = &app
|
||||
Expect(handler.PrepareCurrentAppRevision(ctx, generatedAppfile)).Should(Succeed())
|
||||
Expect(handler.HandleComponentsRevision(ctx, comps)).Should(Succeed())
|
||||
Expect(handler.FinalizeAndApplyAppRevision(ctx)).Should(Succeed())
|
||||
Expect(handler.ProduceArtifacts(context.Background(), comps, nil)).Should(Succeed())
|
||||
Expect(handler.UpdateAppLatestRevisionStatus(ctx)).Should(Succeed())
|
||||
@@ -356,18 +318,6 @@ var _ = Describe("test generate revision ", func() {
|
||||
Expect(curAppRevision.GetLabels()[oam.LabelAppRevisionHash]).Should(Equal(appHash2))
|
||||
Expect(curApp.Status.LatestRevision.RevisionHash).Should(Equal(appHash2))
|
||||
|
||||
By("Verify component revision is changed")
|
||||
expectCompRevName = "express-server-v2"
|
||||
Expect(comps[0].RevisionName).Should(Equal(expectCompRevName))
|
||||
gotCR = &appsv1.ControllerRevision{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: expectCompRevName, Namespace: namespaceName}, gotCR)).Should(Succeed())
|
||||
Expect(gotCR.Revision).Should(Equal(int64(2)))
|
||||
gotComp, err = util.RawExtension2Component(gotCR.Data)
|
||||
Expect(err).Should(BeNil())
|
||||
expectWorkload = comps[0].StandardWorkload.DeepCopy()
|
||||
util.RemoveLabels(expectWorkload, []string{oam.LabelAppRevision, oam.LabelAppRevisionHash, oam.LabelAppComponentRevision})
|
||||
Expect(cmp.Diff(gotComp.Spec.Workload, *util.Object2RawExtension(expectWorkload))).Should(BeEmpty())
|
||||
|
||||
By("Change the application same as v1 and apply again")
|
||||
// bump the image tag
|
||||
app.ResourceVersion = curApp.ResourceVersion
|
||||
@@ -382,7 +332,6 @@ var _ = Describe("test generate revision ", func() {
|
||||
Expect(err).Should(Succeed())
|
||||
handler.app = &app
|
||||
Expect(handler.PrepareCurrentAppRevision(ctx, generatedAppfile)).Should(Succeed())
|
||||
Expect(handler.HandleComponentsRevision(ctx, comps)).Should(Succeed())
|
||||
Expect(handler.FinalizeAndApplyAppRevision(ctx)).Should(Succeed())
|
||||
Expect(handler.ProduceArtifacts(context.Background(), comps, nil)).Should(Succeed())
|
||||
Expect(handler.UpdateAppLatestRevisionStatus(ctx)).Should(Succeed())
|
||||
@@ -412,23 +361,6 @@ var _ = Describe("test generate revision ", func() {
|
||||
Expect(appHash2).ShouldNot(Equal(appHash3))
|
||||
Expect(curAppRevision.GetLabels()[oam.LabelAppRevisionHash]).Should(Equal(appHash3))
|
||||
Expect(curApp.Status.LatestRevision.RevisionHash).Should(Equal(appHash3))
|
||||
|
||||
By("Verify no new component revision (v3) is created")
|
||||
gotCR = &appsv1.ControllerRevision{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "express-server-v3", Namespace: namespaceName}, gotCR)).Should(util.NotFoundMatcher{})
|
||||
|
||||
By("Verify component revision is set back to v1")
|
||||
expectCompRevName = "express-server-v1"
|
||||
Expect(comps[0].RevisionName).Should(Equal(expectCompRevName))
|
||||
gotCR = &appsv1.ControllerRevision{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: expectCompRevName, Namespace: namespaceName}, gotCR)).Should(Succeed())
|
||||
Expect(gotCR.Revision).Should(Equal(int64(1)))
|
||||
gotComp, err = util.RawExtension2Component(gotCR.Data)
|
||||
Expect(err).Should(BeNil())
|
||||
expectWorkload = comps[0].StandardWorkload.DeepCopy()
|
||||
util.RemoveLabels(expectWorkload, []string{oam.LabelAppRevision, oam.LabelAppRevisionHash, oam.LabelAppComponentRevision})
|
||||
expectWorkload.SetAnnotations(map[string]string{"testKey1": "true"})
|
||||
Expect(cmp.Diff(gotComp.Spec.Workload, *util.Object2RawExtension(expectWorkload))).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("Test App with rollout template", func() {
|
||||
@@ -471,7 +403,6 @@ var _ = Describe("test generate revision ", func() {
|
||||
Expect(ctrlOwner).ShouldNot(BeNil())
|
||||
Expect(ctrlOwner.Kind).Should(Equal(v1beta1.ApplicationKind))
|
||||
Expect(len(curAppRevision.GetOwnerReferences())).Should(BeEquivalentTo(1))
|
||||
Expect(curAppRevision.GetOwnerReferences()[0].Kind).Should(Equal(v1alpha2.ApplicationKind))
|
||||
|
||||
By("Apply the application again without any spec change but remove the rollout annotation")
|
||||
annoKey2 := "testKey2"
|
||||
@@ -620,107 +551,6 @@ var _ = Describe("test generate revision ", func() {
|
||||
Expect(curAppRevision.GetAnnotations()[annoKey1]).Should(BeEmpty())
|
||||
Expect(curAppRevision.GetAnnotations()[annoKey2]).Should(Equal("true"))
|
||||
})
|
||||
|
||||
It("Test specified component revision name", func() {
|
||||
By("Specify component revision name but revision does not exist")
|
||||
externalRevisionName1 := "specified-revision-v1"
|
||||
app.Spec.Components[0].ExternalRevision = externalRevisionName1
|
||||
Expect(k8sClient.Update(ctx, &app)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
appParser := appfile.NewApplicationParser(reconciler.Client, reconciler.pd)
|
||||
ctx = util.SetNamespaceInCtx(ctx, app.Namespace)
|
||||
generatedAppfile, err := appParser.GenerateAppFile(ctx, &app)
|
||||
Expect(err).Should(Succeed())
|
||||
comps, err = generatedAppfile.GenerateComponentManifests()
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(handler.PrepareCurrentAppRevision(ctx, generatedAppfile)).Should(Succeed())
|
||||
Expect(handler.HandleComponentsRevision(ctx, comps)).Should(Succeed())
|
||||
Expect(handler.FinalizeAndApplyAppRevision(ctx)).Should(Succeed())
|
||||
Expect(handler.ProduceArtifacts(context.Background(), comps, nil)).Should(Succeed())
|
||||
Expect(handler.UpdateAppLatestRevisionStatus(ctx)).Should(Succeed())
|
||||
|
||||
curApp := &v1beta1.Application{}
|
||||
Eventually(
|
||||
func() error {
|
||||
return handler.r.Get(ctx,
|
||||
types.NamespacedName{Namespace: ns.Name, Name: app.Name},
|
||||
curApp)
|
||||
},
|
||||
time.Second*10, time.Millisecond*500).Should(BeNil())
|
||||
Expect(curApp.Status.LatestRevision.Revision).Should(BeEquivalentTo(1))
|
||||
Expect(comps[0].RevisionName).Should(Equal(externalRevisionName1))
|
||||
gotCR := &appsv1.ControllerRevision{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: externalRevisionName1, Namespace: namespaceName}, gotCR)).Should(Succeed())
|
||||
Expect(gotCR.Revision).Should(Equal(int64(1)))
|
||||
gotComp, err := util.RawExtension2Component(gotCR.Data)
|
||||
Expect(err).Should(BeNil())
|
||||
expectWorkload := comps[0].StandardWorkload.DeepCopy()
|
||||
util.RemoveLabels(expectWorkload, []string{oam.LabelAppRevision, oam.LabelAppRevisionHash, oam.LabelAppComponentRevision})
|
||||
var gotWL = unstructured.Unstructured{}
|
||||
err = json.Unmarshal(gotComp.Spec.Workload.Raw, &gotWL)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(&gotWL, expectWorkload)).Should(BeEmpty())
|
||||
|
||||
By("Specify component revision name and revision already exist")
|
||||
externalRevisionName2 := "specified-revision-v2"
|
||||
newCR := gotCR.DeepCopy()
|
||||
newCR.Name = externalRevisionName2
|
||||
newCR.ResourceVersion = ""
|
||||
Expect(k8sClient.Create(ctx, newCR)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
app.Spec.Components[0].ExternalRevision = externalRevisionName2
|
||||
Expect(k8sClient.Update(ctx, &app)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
generatedAppfile, err = appParser.GenerateAppFile(ctx, &app)
|
||||
Expect(err).Should(Succeed())
|
||||
comps, err = generatedAppfile.GenerateComponentManifests()
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(handler.PrepareCurrentAppRevision(ctx, generatedAppfile)).Should(Succeed())
|
||||
Expect(handler.HandleComponentsRevision(ctx, comps)).Should(Succeed())
|
||||
Expect(handler.FinalizeAndApplyAppRevision(ctx)).Should(Succeed())
|
||||
Expect(handler.ProduceArtifacts(context.Background(), comps, nil)).Should(Succeed())
|
||||
Expect(handler.UpdateAppLatestRevisionStatus(ctx)).Should(Succeed())
|
||||
|
||||
Expect(comps[0].RevisionName).Should(Equal(externalRevisionName2))
|
||||
Expect(comps[0].RevisionHash).Should(Equal(gotCR.Labels[oam.LabelComponentRevisionHash]))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Test ReplaceComponentRevisionContext func", func() {
|
||||
It("Test replace", func() {
|
||||
rollout := v1alpha1.Rollout{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1alpha1",
|
||||
Kind: "Rollout",
|
||||
},
|
||||
Spec: v1alpha1.RolloutSpec{
|
||||
TargetRevisionName: process.ComponentRevisionPlaceHolder,
|
||||
},
|
||||
}
|
||||
u, err := util.Object2Unstructured(rollout)
|
||||
Expect(err).Should(BeNil())
|
||||
err = replaceComponentRevisionContext(u, "comp-rev1")
|
||||
Expect(err).Should(BeNil())
|
||||
jsRes, err := u.MarshalJSON()
|
||||
Expect(err).Should(BeNil())
|
||||
err = json.Unmarshal(jsRes, &rollout)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(rollout.Spec.TargetRevisionName).Should(BeEquivalentTo("comp-rev1"))
|
||||
})
|
||||
|
||||
It("Test replace return error", func() {
|
||||
rollout := v1alpha1.Rollout{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1alpha1",
|
||||
Kind: "Rollout",
|
||||
},
|
||||
Spec: v1alpha1.RolloutSpec{
|
||||
TargetRevisionName: process.ComponentRevisionPlaceHolder,
|
||||
},
|
||||
}
|
||||
u, err := util.Object2Unstructured(rollout)
|
||||
Expect(err).Should(BeNil())
|
||||
By("test replace with a bad revision")
|
||||
err = replaceComponentRevisionContext(u, "comp-rev1-\\}")
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Test PrepareCurrentAppRevision", func() {
|
||||
-14
@@ -27,7 +27,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/event"
|
||||
"github.com/go-logr/logr"
|
||||
terraformv1beta2 "github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -49,7 +48,6 @@ import (
|
||||
|
||||
"github.com/kubevela/workflow/pkg/cue/packages"
|
||||
|
||||
"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/pkg/appfile"
|
||||
@@ -77,15 +75,6 @@ func TestAPIs(t *testing.T) {
|
||||
RunSpecs(t, "Controller Suite")
|
||||
}
|
||||
|
||||
type NoOpReconciler struct {
|
||||
Log logr.Logger
|
||||
}
|
||||
|
||||
func (r *NoOpReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
r.Log.Info("received a request", "object name", req.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
@@ -109,9 +98,6 @@ var _ = BeforeSuite(func() {
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(cfg).ToNot(BeNil())
|
||||
|
||||
err = v1alpha2.SchemeBuilder.AddToScheme(testScheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
err = v1alpha1.SchemeBuilder.AddToScheme(testScheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ import (
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
oamctrl "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
|
||||
coredef "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/core"
|
||||
coredef "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/core"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
"github.com/oam-dev/kubevela/version"
|
||||
+1
-1
@@ -421,7 +421,7 @@ spec:
|
||||
It("Applying Terraform ComponentDefinition", func() {
|
||||
By("Apply ComponentDefinition")
|
||||
var validComponentDefinition = `
|
||||
apiVersion: core.oam.dev/v1alpha2
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
name: alibaba-rds-test
|
||||
+1
-1
@@ -38,7 +38,7 @@ import (
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
apistypes "github.com/oam-dev/kubevela/apis/types"
|
||||
coredef "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/core"
|
||||
coredef "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/core"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/testutil"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
+1
-1
@@ -35,7 +35,7 @@ import (
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
oamctrl "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
|
||||
coredef "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/core"
|
||||
coredef "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/core"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
"github.com/oam-dev/kubevela/version"
|
||||
+1
-1
@@ -35,7 +35,7 @@ import (
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
oamctrl "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
|
||||
coredef "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/core"
|
||||
coredef "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/core"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
"github.com/oam-dev/kubevela/version"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user