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:
Somefive
2023-06-01 09:32:49 +08:00
committed by GitHub
parent d6788f12dd
commit dd899c2b39
284 changed files with 517 additions and 66926 deletions
-26
View File
@@ -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())
})
@@ -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: []
-32
View File
@@ -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(""))
})
})
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))
})
})
@@ -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{}
})
}
@@ -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)
}
@@ -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
}
@@ -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)
}
@@ -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
@@ -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
}
@@ -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
}
@@ -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)
}
}
@@ -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))
})
})
@@ -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)
@@ -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:
@@ -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))
@@ -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
}
@@ -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{
@@ -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())
})
})
@@ -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() {
@@ -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())
@@ -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"
@@ -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
@@ -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"
@@ -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"
@@ -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