mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
garbage collection mechanism for AppRevision (#1501)
* WIP refactor gc func WIP write gc code WIP update chart,cmd args add cleanupRevison func into garbageCollection add more test finish unit-test refactor use func implements Complete e2e test WIP rewrite some logic add func test and rewirte context pointing to func fix cilint refactor some function fix typo fix ci error change gc logic after change gc number, fix all test add check appRevision collection WIP finish most code * add rollout batch to fix rollout e2e-tet * fix component name in rollout * ignore gc error, just log the error
This commit is contained in:
2
Makefile
2
Makefile
@@ -156,7 +156,7 @@ docker-push:
|
||||
e2e-setup:
|
||||
helm install --create-namespace -n flux-system helm-flux http://oam.dev/catalog/helm-flux2-0.1.0.tgz
|
||||
helm install kruise https://github.com/openkruise/kruise/releases/download/v0.7.0/kruise-chart.tgz
|
||||
helm upgrade --install --create-namespace --namespace vela-system --set image.pullPolicy=IfNotPresent --set image.repository=vela-core-test --set image.tag=$(GIT_COMMIT) --wait kubevela ./charts/vela-core
|
||||
helm upgrade --install --create-namespace --namespace vela-system --set image.pullPolicy=IfNotPresent --set image.repository=vela-core-test --set applicationRevisionLimit=5 --set image.tag=$(GIT_COMMIT) --wait kubevela ./charts/vela-core
|
||||
ginkgo version
|
||||
ginkgo -v -r e2e/setup
|
||||
kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=vela-core,app.kubernetes.io/instance=kubevela -n vela-system --timeout=600s
|
||||
|
||||
@@ -121,6 +121,7 @@ spec:
|
||||
- "--disable-caps={{ .Values.disableCaps }}"
|
||||
{{ end }}
|
||||
- "--system-definition-namespace={{ .Values.systemDefinitionNamespace }}"
|
||||
- "--application-revision-limit={{ .Values.applicationRevisionLimit }}"
|
||||
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
|
||||
imagePullPolicy: {{ quote .Values.image.pullPolicy }}
|
||||
resources:
|
||||
|
||||
@@ -80,3 +80,5 @@ admissionWebhooks:
|
||||
|
||||
|
||||
systemDefinitionNamespace: vela-system
|
||||
|
||||
applicationRevisionLimit: 10
|
||||
|
||||
@@ -89,6 +89,8 @@ func main() {
|
||||
flag.BoolVar(&logDebug, "log-debug", false, "Enable debug logs for development purpose")
|
||||
flag.IntVar(&controllerArgs.RevisionLimit, "revision-limit", 50,
|
||||
"RevisionLimit is the maximum number of revisions that will be maintained. The default value is 50.")
|
||||
flag.IntVar(&controllerArgs.AppRevisionLimit, "application-revision-limit", 10,
|
||||
"application-revision-limit is the maximum number of application useless revisions that will be maintained, if the useless revisions exceed this number, older ones will be GCed first.The default value is 10.")
|
||||
flag.StringVar(&controllerArgs.CustomRevisionHookURL, "custom-revision-hook-url", "",
|
||||
"custom-revision-hook-url is a webhook url which will let KubeVela core to call with applicationConfiguration and component info and return a customized component revision")
|
||||
flag.BoolVar(&controllerArgs.ApplicationConfigurationInstalled, "app-config-installed", true,
|
||||
|
||||
@@ -48,6 +48,10 @@ type Args struct {
|
||||
// The default value is 50.
|
||||
RevisionLimit int
|
||||
|
||||
// AppRevisionLimit is the maximum number of application revisions that will be maintained.
|
||||
// The default value is 10.
|
||||
AppRevisionLimit int
|
||||
|
||||
// ApplyMode indicates whether workloads and traits should be
|
||||
// affected if no spec change is made in the ApplicationConfiguration.
|
||||
ApplyMode ApplyOnceOnlyMode
|
||||
|
||||
@@ -54,11 +54,12 @@ const (
|
||||
// Reconciler reconciles a Application object
|
||||
type Reconciler struct {
|
||||
client.Client
|
||||
dm discoverymapper.DiscoveryMapper
|
||||
pd *definition.PackageDiscover
|
||||
Log logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
applicator apply.Applicator
|
||||
dm discoverymapper.DiscoveryMapper
|
||||
pd *definition.PackageDiscover
|
||||
Log logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
applicator apply.Applicator
|
||||
appRevisionLimit int
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=core.oam.dev,resources=applications,verbs=get;list;watch;create;update;patch;delete
|
||||
@@ -181,11 +182,9 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
app.Status.Services = appCompStatus
|
||||
app.Status.SetConditions(readyCondition("HealthCheck"))
|
||||
app.Status.Phase = common.ApplicationRunning
|
||||
err = handler.garbageCollection(ctx)
|
||||
err = garbageCollection(ctx, handler)
|
||||
if err != nil {
|
||||
applog.Error(err, "[Garbage collection]")
|
||||
app.Status.SetConditions(errorCondition("GarbageCollection", err))
|
||||
return handler.handleErr(err)
|
||||
}
|
||||
// Gather status of components
|
||||
var refComps []v1alpha1.TypedReference
|
||||
@@ -233,12 +232,13 @@ func (r *Reconciler) UpdateStatus(ctx context.Context, app *v1beta1.Application,
|
||||
// Setup adds a controller that reconciles AppRollout.
|
||||
func Setup(mgr ctrl.Manager, args core.Args, _ logging.Logger) error {
|
||||
reconciler := Reconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Log: ctrl.Log.WithName("Application"),
|
||||
Scheme: mgr.GetScheme(),
|
||||
dm: args.DiscoveryMapper,
|
||||
pd: args.PackageDiscover,
|
||||
applicator: apply.NewAPIApplicator(mgr.GetClient()),
|
||||
Client: mgr.GetClient(),
|
||||
Log: ctrl.Log.WithName("Application"),
|
||||
Scheme: mgr.GetScheme(),
|
||||
dm: args.DiscoveryMapper,
|
||||
pd: args.PackageDiscover,
|
||||
applicator: apply.NewAPIApplicator(mgr.GetClient()),
|
||||
appRevisionLimit: args.AppRevisionLimit,
|
||||
}
|
||||
return reconciler.SetupWithManager(mgr)
|
||||
}
|
||||
|
||||
@@ -587,9 +587,26 @@ func (h *appHandler) recodeTrackedResource(resourceName string, resource runtime
|
||||
return nil
|
||||
}
|
||||
|
||||
type garbageCollectFunc func(ctx context.Context, h *appHandler) error
|
||||
|
||||
// 1. collect useless across-namespace resource
|
||||
// 2. collect appRevision
|
||||
func garbageCollection(ctx context.Context, h *appHandler) error {
|
||||
collectFuncs := []garbageCollectFunc{
|
||||
garbageCollectFunc(gcAcrossNamespaceResource),
|
||||
garbageCollectFunc(cleanUpApplicationRevision),
|
||||
}
|
||||
for _, collectFunc := range collectFuncs {
|
||||
if err := collectFunc(ctx, h); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Now if workloads or traits are in the same namespace with application, applicationContext will take over gc workloads and traits.
|
||||
// Here we cover the case in witch a cross namespace component or one of its cross namespace trait is removed from an application.
|
||||
func (h *appHandler) garbageCollection(ctx context.Context) error {
|
||||
func gcAcrossNamespaceResource(ctx context.Context, h *appHandler) error {
|
||||
rt := new(v1beta1.ResourceTracker)
|
||||
err := h.r.Get(ctx, ctypes.NamespacedName{Name: h.generateResourceTrackerName()}, rt)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,9 +18,11 @@ package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -291,3 +293,81 @@ func ComputeAppRevisionHash(appRevision *v1beta1.ApplicationRevision) (string, e
|
||||
// compute the hash of the entire structure
|
||||
return utils.ComputeSpecHash(&appRevisionHash)
|
||||
}
|
||||
|
||||
// cleanUpApplicationRevision check all appRevisions of the application, remove them if the number of them exceed the limit
|
||||
func cleanUpApplicationRevision(ctx context.Context, h *appHandler) error {
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(h.app.Namespace),
|
||||
client.MatchingLabels{oam.LabelAppName: h.app.Name},
|
||||
}
|
||||
appRevisionList := new(v1beta1.ApplicationRevisionList)
|
||||
// controller-runtime will cache all appRevision by default, there is no need to watch or own appRevision in manager
|
||||
if err := h.r.List(ctx, appRevisionList, listOpts...); err != nil {
|
||||
return err
|
||||
}
|
||||
usingRevision, err := gatherUsingAppRevision(ctx, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
needKill := len(appRevisionList.Items) - h.r.appRevisionLimit - len(usingRevision)
|
||||
if needKill <= 0 {
|
||||
return nil
|
||||
}
|
||||
h.logger.Info("application controller cleanup old appRevisions", "needKillNum", needKill)
|
||||
sortedRevision := appRevisionList.Items
|
||||
sort.Sort(historiesByRevision(sortedRevision))
|
||||
|
||||
for _, rev := range sortedRevision {
|
||||
if needKill <= 0 {
|
||||
break
|
||||
}
|
||||
// we shouldn't delete the revision witch appContext pointing to
|
||||
if usingRevision[rev.Name] {
|
||||
continue
|
||||
}
|
||||
if err := h.r.Delete(ctx, rev.DeepCopy()); err != nil && !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
needKill--
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// gatherUsingAppRevision get all using appRevisions include app's status pointing to and appContext point to
|
||||
func gatherUsingAppRevision(ctx context.Context, h *appHandler) (map[string]bool, error) {
|
||||
listOpts := []client.ListOption{
|
||||
client.InNamespace(h.app.Namespace),
|
||||
client.MatchingLabels{oam.LabelAppName: h.app.Name},
|
||||
}
|
||||
usingRevision := map[string]bool{}
|
||||
if h.app.Status.LatestRevision != nil && len(h.app.Status.LatestRevision.Name) != 0 {
|
||||
usingRevision[h.app.Status.LatestRevision.Name] = true
|
||||
}
|
||||
appContextList := new(v1alpha2.ApplicationContextList)
|
||||
err := h.r.List(ctx, appContextList, listOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, appContext := range appContextList.Items {
|
||||
usingRevision[appContext.Spec.ApplicationRevisionName] = true
|
||||
}
|
||||
appDeployUsingRevision, err := utils.CheckAppDeploymentUsingAppRevision(ctx, h.r, h.app.Namespace, h.app.Name)
|
||||
if err != nil {
|
||||
return usingRevision, err
|
||||
}
|
||||
for _, revName := range appDeployUsingRevision {
|
||||
usingRevision[revName] = true
|
||||
}
|
||||
return usingRevision, nil
|
||||
}
|
||||
|
||||
type historiesByRevision []v1beta1.ApplicationRevision
|
||||
|
||||
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 {
|
||||
// the appRevision is generated by vela, the error always is nil, so ignore it
|
||||
ir, _ := util.ExtractRevisionNum(h[i].Name)
|
||||
ij, _ := util.ExtractRevisionNum(h[j].Name)
|
||||
return ir < ij
|
||||
}
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
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/apis/core.oam.dev/common"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "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"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"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"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
var _ = Describe("Test application controller clean up ", func() {
|
||||
ctx := context.TODO()
|
||||
namespace := "clean-up-revision"
|
||||
|
||||
cd := &v1beta1.ComponentDefinition{}
|
||||
cdDefJson, _ := yaml.YAMLToJSON([]byte(normalCompDefYaml))
|
||||
|
||||
BeforeEach(func() {
|
||||
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")
|
||||
})
|
||||
|
||||
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())
|
||||
_, err := reconciler.Reconcile(ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
}
|
||||
appContext := new(v1alpha2.ApplicationContext)
|
||||
Expect(k8sClient.Get(ctx, appKey, appContext)).Should(BeNil())
|
||||
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(ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
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))
|
||||
}
|
||||
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(ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
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))
|
||||
}
|
||||
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 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())
|
||||
_, err := reconciler.Reconcile(ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
}
|
||||
appContext := new(v1alpha2.ApplicationContext)
|
||||
Expect(k8sClient.Get(ctx, appKey, appContext)).Should(util.NotFoundMatcher{})
|
||||
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(ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
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))
|
||||
}
|
||||
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*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(ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
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))
|
||||
}
|
||||
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())
|
||||
_, err := reconciler.Reconcile(ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
}
|
||||
appContext := new(v1alpha2.ApplicationContext)
|
||||
Expect(k8sClient.Get(ctx, appKey, appContext)).Should(util.NotFoundMatcher{})
|
||||
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(ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
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))
|
||||
}
|
||||
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*30, time.Microsecond*300).Should(BeNil())
|
||||
|
||||
By("update create appDeploy check gc logic")
|
||||
appDeploy := &v1beta1.AppDeployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: v1beta1.AppDeploymentKindAPIVersion,
|
||||
Kind: v1beta1.AppDeploymentKind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: "app-deploy",
|
||||
},
|
||||
Spec: v1beta1.AppDeploymentSpec{
|
||||
AppRevisions: []v1beta1.AppRevision{
|
||||
{
|
||||
RevisionName: appName + "-v2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, appDeploy)).Should(BeNil())
|
||||
// give informer some time to cache
|
||||
time.Sleep(2 * time.Second)
|
||||
for i := 7; i < 9; 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())
|
||||
_, err = reconciler.Reconcile(ctrl.Request{NamespacedName: appKey})
|
||||
Expect(err).Should(BeNil())
|
||||
}
|
||||
Eventually(func() error {
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+2 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+2, len(appRevisionList.Items))
|
||||
}
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: appName + "-v3"}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the revision-3")
|
||||
}
|
||||
if res, err := util.CheckAppRevision(appRevisionList.Items, []int{2, 4, 5, 6, 7, 8, 9}); err != nil || !res {
|
||||
return fmt.Errorf("appRevision collection mismatch")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Test gatherUsingAppRevision func", func() {
|
||||
ctx := context.TODO()
|
||||
namespace := "clean-up-revision"
|
||||
|
||||
cd := &v1beta1.ComponentDefinition{}
|
||||
cdDefJson, _ := yaml.YAMLToJSON([]byte(normalCompDefYaml))
|
||||
|
||||
BeforeEach(func() {
|
||||
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")
|
||||
})
|
||||
|
||||
It("get gatherUsingAppRevision func logic", func() {
|
||||
appName := "app-3"
|
||||
app := getApp(appName, namespace, "normal-worker")
|
||||
app.Status.LatestRevision = &common.Revision{
|
||||
Name: appName + "-v1",
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
appContext := getAppContext(namespace, appName+"-ctx", appName+"-v2")
|
||||
appContext.Labels = map[string]string{
|
||||
oam.LabelAppName: appName,
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, appContext)).Should(BeNil())
|
||||
handler := appHandler{
|
||||
r: reconciler,
|
||||
app: app,
|
||||
logger: reconciler.Log.WithValues("application", "gatherUsingAppRevision-func-test"),
|
||||
}
|
||||
Eventually(func() error {
|
||||
using, err := gatherUsingAppRevision(ctx, &handler)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(using) != 2 {
|
||||
return fmt.Errorf("wrong revision number")
|
||||
}
|
||||
if !using[appName+"-v1"] {
|
||||
return fmt.Errorf("revison1 not include")
|
||||
}
|
||||
if !using[appName+"-v2"] {
|
||||
return fmt.Errorf("revison2 not include")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*60, time.Microsecond).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
func getAppContext(namespace, name string, pointingRev string) *v1alpha2.ApplicationContext {
|
||||
return &v1alpha2.ApplicationContext{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: v1alpha2.ApplicationContextKindAPIVersion,
|
||||
Kind: v1alpha2.ApplicationContextKind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: "app-rollout",
|
||||
},
|
||||
Spec: v1alpha2.ApplicationContextSpec{
|
||||
ApplicationRevisionName: pointingRev,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ var testScheme = runtime.NewScheme()
|
||||
var reconciler *Reconciler
|
||||
var stop = make(chan struct{})
|
||||
var ctlManager ctrl.Manager
|
||||
var appRevisionLimit = 5
|
||||
|
||||
// TODO: create a mock client and add UT to cover all the failure cases
|
||||
|
||||
@@ -122,11 +123,12 @@ var _ = BeforeSuite(func(done Done) {
|
||||
pd, err := definition.NewPackageDiscover(cfg)
|
||||
Expect(err).To(BeNil())
|
||||
reconciler = &Reconciler{
|
||||
Client: k8sClient,
|
||||
Log: ctrl.Log.WithName("Application-Test"),
|
||||
Scheme: testScheme,
|
||||
dm: dm,
|
||||
pd: pd,
|
||||
Client: k8sClient,
|
||||
Log: ctrl.Log.WithName("Application-Test"),
|
||||
Scheme: testScheme,
|
||||
dm: dm,
|
||||
pd: pd,
|
||||
appRevisionLimit: appRevisionLimit,
|
||||
}
|
||||
// setup the controller manager since we need the component handler to run in the background
|
||||
ctlManager, err = ctrl.NewManager(cfg, ctrl.Options{
|
||||
|
||||
@@ -294,3 +294,45 @@ func RefreshPackageDiscover(dm discoverymapper.DiscoveryMapper, pd *definition.P
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckAppDeploymentUsingAppRevision get all appDeployments using appRevisions related the app
|
||||
func CheckAppDeploymentUsingAppRevision(ctx context.Context, c client.Reader, appNs string, appName string) ([]string, error) {
|
||||
deployOpts := []client.ListOption{
|
||||
client.InNamespace(appNs),
|
||||
}
|
||||
var res []string
|
||||
ads := new(v1beta1.AppDeploymentList)
|
||||
if err := c.List(ctx, ads, deployOpts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ads.Items) == 0 {
|
||||
return res, nil
|
||||
}
|
||||
relatedRevs := new(v1beta1.ApplicationRevisionList)
|
||||
revOpts := []client.ListOption{
|
||||
client.InNamespace(appNs),
|
||||
client.MatchingLabels{oam.LabelAppName: appName},
|
||||
}
|
||||
if err := c.List(ctx, relatedRevs, revOpts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(relatedRevs.Items) == 0 {
|
||||
return res, nil
|
||||
}
|
||||
revName := map[string]bool{}
|
||||
for _, rev := range relatedRevs.Items {
|
||||
if len(rev.Name) != 0 {
|
||||
revName[rev.Name] = true
|
||||
}
|
||||
}
|
||||
for _, d := range ads.Items {
|
||||
for _, dr := range d.Spec.AppRevisions {
|
||||
if len(dr.RevisionName) != 0 {
|
||||
if revName[dr.RevisionName] {
|
||||
res = append(res, dr.RevisionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
@@ -187,3 +189,24 @@ func UnMarshalStringToTraitDefinition(s string) (*v1beta1.TraitDefinition, error
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// CheckAppRevision check if appRevision list is right
|
||||
func CheckAppRevision(revs []v1beta1.ApplicationRevision, collection []int) (bool, error) {
|
||||
if len(revs) != len(collection) {
|
||||
return false, nil
|
||||
}
|
||||
var revNums []int
|
||||
for _, rev := range revs {
|
||||
num, err := ExtractRevisionNum(rev.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
revNums = append(revNums, num)
|
||||
}
|
||||
sort.Ints(revNums)
|
||||
sort.Ints(collection)
|
||||
if reflect.DeepEqual(revNums, collection) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -219,3 +223,125 @@ func TestErrorMatcher(t *testing.T) {
|
||||
assert.Equal(t, tc.want.negatedFailureMessage, matcher.NegatedFailureMessage(tc.input.input))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAppRevision(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
revs []v1beta1.ApplicationRevision
|
||||
collection []int
|
||||
want bool
|
||||
hasError bool
|
||||
}{
|
||||
"match": {
|
||||
revs: []v1beta1.ApplicationRevision{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v2",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v3",
|
||||
},
|
||||
},
|
||||
},
|
||||
collection: []int{1, 2, 3},
|
||||
want: true,
|
||||
hasError: false,
|
||||
},
|
||||
"lengthNotMatch": {
|
||||
revs: []v1beta1.ApplicationRevision{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v2",
|
||||
},
|
||||
},
|
||||
},
|
||||
collection: []int{1, 2, 3},
|
||||
want: false,
|
||||
hasError: false,
|
||||
},
|
||||
"notMatch": {
|
||||
revs: []v1beta1.ApplicationRevision{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v2",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v4",
|
||||
},
|
||||
},
|
||||
},
|
||||
collection: []int{1, 2, 3},
|
||||
want: false,
|
||||
hasError: false,
|
||||
},
|
||||
"testSort": {
|
||||
revs: []v1beta1.ApplicationRevision{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v3",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v2",
|
||||
},
|
||||
},
|
||||
},
|
||||
collection: []int{3, 2, 1},
|
||||
want: true,
|
||||
hasError: false,
|
||||
},
|
||||
"testErrorName": {
|
||||
revs: []v1beta1.ApplicationRevision{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-v3",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-vs",
|
||||
},
|
||||
},
|
||||
},
|
||||
collection: []int{3, 2, 1},
|
||||
want: false,
|
||||
hasError: true,
|
||||
},
|
||||
}
|
||||
for name, testcase := range testcases {
|
||||
t.Log(fmt.Sprint("Running test: ", name))
|
||||
checkEqual, err := CheckAppRevision(testcase.revs, testcase.collection)
|
||||
hasError := err != nil
|
||||
assert.Equal(t, checkEqual, testcase.want)
|
||||
assert.Equal(t, hasError, testcase.hasError)
|
||||
}
|
||||
}
|
||||
|
||||
432
test/e2e-test/app_revision_clean_up_test.go
Normal file
432
test/e2e-test/app_revision_clean_up_test.go
Normal file
@@ -0,0 +1,432 @@
|
||||
/*
|
||||
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 controllers_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"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/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
var (
|
||||
appRevisionLimit = 5
|
||||
)
|
||||
|
||||
var _ = Describe("Test application controller clean up appRevision", func() {
|
||||
ctx := context.TODO()
|
||||
namespace := "clean-up-revision"
|
||||
|
||||
cd := &v1beta1.ComponentDefinition{}
|
||||
cdDefJson, _ := yaml.YAMLToJSON([]byte(compDefYaml))
|
||||
|
||||
BeforeEach(func() {
|
||||
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, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}, client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed())
|
||||
// guarantee namespace have been deleted
|
||||
Eventually(func() error {
|
||||
ns := new(v1.Namespace)
|
||||
err := k8sClient.Get(ctx, types.NamespacedName{Name: namespace}, ns)
|
||||
if err == nil {
|
||||
return fmt.Errorf("namespace still exist")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*60, time.Microsecond*300).Should(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++ {
|
||||
Eventually(func() error {
|
||||
checkApp = new(v1beta1.Application)
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
if checkApp.Status.LatestRevision == nil || checkApp.Status.LatestRevision.Revision != int64(i+1) {
|
||||
return fmt.Errorf("application point to wrong revision")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
Eventually(func() error {
|
||||
checkApp = new(v1beta1.Application)
|
||||
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)}
|
||||
if err := k8sClient.Update(ctx, checkApp); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
}
|
||||
appContext := new(v1alpha2.ApplicationContext)
|
||||
Expect(k8sClient.Get(ctx, appKey, appContext)).Should(BeNil())
|
||||
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())
|
||||
Eventually(func() error {
|
||||
if err := k8sClient.Get(ctx, appKey, appContext); err != nil {
|
||||
return err
|
||||
}
|
||||
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())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
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))
|
||||
}
|
||||
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*30, time.Microsecond*300).Should(BeNil())
|
||||
|
||||
By("update app again will gc appRevision2")
|
||||
Eventually(func() error {
|
||||
if err := k8sClient.Get(ctx, appKey, checkApp); err != nil {
|
||||
return err
|
||||
}
|
||||
property = fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, 7)
|
||||
checkApp.Spec.Components[0].Properties = runtime.RawExtension{Raw: []byte(property)}
|
||||
if err := k8sClient.Update(ctx, checkApp); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
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))
|
||||
}
|
||||
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; i++ {
|
||||
Eventually(func() error {
|
||||
Expect(k8sClient.Get(ctx, appKey, checkApp)).Should(BeNil())
|
||||
if checkApp.Status.LatestRevision == nil || checkApp.Status.LatestRevision.Revision != int64(i+1) {
|
||||
return fmt.Errorf("application point to wrong revision")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond).Should(BeNil())
|
||||
Eventually(func() error {
|
||||
checkApp = new(v1beta1.Application)
|
||||
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)}
|
||||
if err := k8sClient.Update(ctx, checkApp); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
}
|
||||
appContext := new(v1alpha2.ApplicationContext)
|
||||
Expect(k8sClient.Get(ctx, appKey, appContext)).Should(util.NotFoundMatcher{})
|
||||
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*300, 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"}`, 5)
|
||||
checkApp.Spec.Components[0].Properties = runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
deletedRevison := new(v1beta1.ApplicationRevision)
|
||||
revKey := types.NamespacedName{Namespace: namespace, Name: appName + "-v1"}
|
||||
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, 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*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"}`, 6)
|
||||
checkApp.Spec.Components[0].Properties = runtime.RawExtension{Raw: []byte(property)}
|
||||
Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil())
|
||||
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, 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())
|
||||
appRollout := &v1beta1.AppRollout{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: v1beta1.AppRolloutKindAPIVersion,
|
||||
Kind: v1beta1.AppRolloutKind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: "app-roll-out",
|
||||
},
|
||||
Spec: v1beta1.AppRolloutSpec{
|
||||
TargetAppRevisionName: appName + "-v3",
|
||||
ComponentList: []string{"comp1"},
|
||||
RolloutPlan: v1alpha1.RolloutPlan{
|
||||
RolloutBatches: []v1alpha1.RolloutBatch{
|
||||
{
|
||||
Replicas: intstr.FromInt(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, appRollout)).Should(BeNil())
|
||||
|
||||
By("update app twice will gc appRevision4 not appRevision3")
|
||||
for i := 7; i < 9; i++ {
|
||||
Eventually(func() error {
|
||||
if err := k8sClient.Get(ctx, appKey, checkApp); err != nil {
|
||||
return err
|
||||
}
|
||||
if checkApp.Status.LatestRevision == nil || checkApp.Status.LatestRevision.Revision != int64(i+1) {
|
||||
return fmt.Errorf("application point to wrong revision")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
Eventually(func() error {
|
||||
if err := k8sClient.Get(ctx, appKey, checkApp); err != nil {
|
||||
return err
|
||||
}
|
||||
property = fmt.Sprintf(`{"cmd":["sleep","1000"],"image":"busybox:%d"}`, i)
|
||||
checkApp.Spec.Components[0].Properties = runtime.RawExtension{Raw: []byte(property)}
|
||||
if err := k8sClient.Update(ctx, checkApp); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond).Should(BeNil())
|
||||
}
|
||||
Eventually(func() error {
|
||||
err := k8sClient.List(ctx, appRevisionList, listOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(appRevisionList.Items) != appRevisionLimit+2 {
|
||||
return fmt.Errorf("error appRevison number wants %d, actually %d", appRevisionLimit+2, len(appRevisionList.Items))
|
||||
}
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: appName + "-v4"}
|
||||
err = k8sClient.Get(ctx, revKey, deletedRevison)
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("haven't clean up the revision-4")
|
||||
}
|
||||
existRev := new(v1beta1.ApplicationRevision)
|
||||
revKey = types.NamespacedName{Namespace: namespace, Name: appName + "-v3"}
|
||||
err = k8sClient.Get(ctx, revKey, existRev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res, err := util.CheckAppRevision(appRevisionList.Items, []int{3, 5, 6, 7, 8, 9, 10}); err != nil || !res {
|
||||
return fmt.Errorf("appRevision collection mismatch")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*30, time.Microsecond*300).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
var (
|
||||
compDefYaml = `
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
name: normal-worker
|
||||
namespace: clean-up-revision
|
||||
annotations:
|
||||
definition.oam.dev/description: "Long-running scalable backend worker without network endpoint"
|
||||
spec:
|
||||
workload:
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
extension:
|
||||
healthPolicy: |
|
||||
isHealth: context.output.status.readyReplicas == context.output.status.replicas
|
||||
template: |
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
spec: {
|
||||
replicas: 0
|
||||
template: {
|
||||
metadata: labels: {
|
||||
"app.oam.dev/component": context.name
|
||||
}
|
||||
|
||||
spec: {
|
||||
containers: [{
|
||||
name: context.name
|
||||
image: parameter.image
|
||||
|
||||
if parameter["cmd"] != _|_ {
|
||||
command: parameter.cmd
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
selector:
|
||||
matchLabels:
|
||||
"app.oam.dev/component": context.name
|
||||
}
|
||||
}
|
||||
|
||||
parameter: {
|
||||
// +usage=Which image would you like to use for your service
|
||||
// +short=i
|
||||
image: string
|
||||
|
||||
cmd?: [...string]
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
func getApp(appName, namespace, comptype string) *v1beta1.Application {
|
||||
return &v1beta1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: appName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []v1beta1.ApplicationComponent{
|
||||
{
|
||||
Name: "comp1",
|
||||
Type: comptype,
|
||||
Properties: runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user